Core Concepts
Four ideas you reuse on every call: chainId, keyPath, the V1Response envelope, and the field-semantics legend used throughout the Chain Reference.
chainId — CAIP-19
Every request targets a network by its full CAIP-19 chainId string (a bare CAIP-2 reference such as eip155:1 is not resolved). The authoritative list ships in the connector's playground/chains.json (168 entries for 151 distinct chainIds — some chainIds appear more than once, one entry per account type; the matrix covers mainnets, EVM testnets and Shibuya; the other 23 non-EVM testnets and Kaia Kairos are not in the matrix). Examples:
| Network | chainId |
|---|---|
| Ethereum | eip155:1/slip44:60 |
| Bitcoin | bip122:000000000019d6689c085ae165831e93/slip44:0 |
| Solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501 |
| Cosmos | cosmos:cosmoshub-4/slip44:118 |
| Cardano | cip34:1-764824073 (no slip44 segment) |
The connector treats chainId as an opaque whitelisted string (charset A-Z a-z 0-9 _ : / . + -). It never branches on chain. The chainId must match the listed value exactly, including the /slip44:NN suffix on the chains that carry one (e.g. eip155:1/slip44:60). Not every chainId has one — Cardano's cip34:1-764824073 has no /slip44 segment, and adding one makes the lookup fail. Copy it from playground/chains.json verbatim — the chainId only. 🔴 That file's Cardano entry has the legacy defaultKeyPath m/44'/1815'/0'/0/0 (the separate Cardano (Ledger) entry, same chainId, carries m/1852'/1815'/0'/0/0); sending it verbatim puts you on the transfer-only tree with no rewardAddress and no signData. Use m/1852'/1815'/… — see Cardano. Two of its entries cannot be reached in this release — Horizen and JuChain are rejected with unsupported_method; on DCENT X, BNB Beacon Chain (mainnet and testnet) also fails with 5005.
keyPath — BIP-44
Derivation path m/44'/{coinType}'/{account}'/{change}/{index}. DCENT uses purpose 44' for most chains. There are two exceptions. Cardano: stake and governance surfaces require CIP-1852 (m/1852'/1815'/{account}'/{role}/0) — the legacy m/44'/1815'/… tree is plain-transfer only. See Cardano. Bitcoin: besides the m/44' account, the BIP-49 (wrapped SegWit), BIP-84 (native SegWit) and BIP-86 (Taproot) accounts use m/49', m/84' and m/86'. See Bitcoin. keyPath is required on every sign() call.
The <accountIdx> slot (the {account} segment above) is the account index — increment it to derive additional accounts. Most chains vary it at the account-hardened position m/44'/{coinType}'/<accountIdx>'/0/0; a few differ — Conflux, Solana, Stellar, Hedera and NEAR are 3-level m/44'/{coinType}'/<accountIdx>', Tezos is 4-level m/44'/1729'/<accountIdx>'/0', Filecoin, Constellation and the EVM Vendor Lock-In variants move it to the address slot m/44'/{coinType}'/0'/0/<accountIdx>, the Substrate Ledger accounts use m/44'/{354|810}'/<accountIdx>'/0'/0', and the second Solana account (mainnet only) uses m/44'/501'/<accountIdx>'/0' — the Support Matrix lists every chain's path. BTC-family multi-address chains additionally expose <isChange> (receive 0 / change 1) and <addressIdx>.
V1Response envelope
Every request that goes to the bridge resolves to the same shape (the setters, the Bitcoin builder helpers and unitConverter return their own values) — except argument errors caught in the connector, which throw or reject (see Error Codes):
{
header: {
version: string,
status: 'success' | 'failure',
request_from?: string,
response_from?: string
},
body: {
command?: string, // on success, usually the method name, e.g. 'signTransaction' | 'getAddress' | …; a few methods report the v1 command name instead (e.g. selectAddress → 'select_address', setLabel → 'set_label'); absent on failure
parameter?: {
…
}, // success payload (per method)
error?: {
code: string,
message: string
} // failure
}
}
Success → header.status === 'success' + body.parameter. Failure → body.error. Full per-method table on the V1Response page.
Field-semantics legend
Parameter tables in the Chain Reference annotate every field so you know what you must supply versus what the bridge fills in:
| Annotation | Meaning |
|---|---|
| Required-Real | Must be the device's real address / account / UTXO. A placeholder fails or risks funds. Source: Wallet |
| Auto-filled | Generated by the bridge locally (e.g. a timestamp) — you do not supply it. The bridge does not fetch consensus values over RPC (Polkadot additionally reads runtime metadata for display safety) — so consensus-fresh values (nonce, sequence, blockhash) are never fetched for you: those are Required-Real and must come from your app, or the call fails with param_error. |
| Optional / Fixed | Optional field, or a fixed-format constant. |
| Unit | Amount unit — wei, satoshi, drops, lamports, uatom, lovelace, … |
| Util | dcent.unitConverter(val, decimals) → { num, bignum } — converts a decimal amount string to base units (e.g. wei/satoshi). Use .num for a payload amount/value field: unitConverter('1.5', 18).num → '1500000000000000000'. |
Token descriptors — sending token metadata
Token metadata — symbol and decimals — is what lets the device show a human-readable amount instead of raw base units. The DCENT Web Bridge never looks token metadata up on-chain for you. On the families that accept one (see the table below), send the token as a form-D descriptor under transaction.token. A token on DCENT's supported list is shown with its registered name and decimals — the values you send are not used (on Kaia only on the default m/44'/60' account; on m/44'/8217' and m/44'/137' every token is signed as a plain contract call). For any other token, include decimals (and optionally symbol): the bridge uses that decimals directly so the device shows the amount, and without decimals the request is rejected with -32602. EVM / Kaia are the exception — there an unregistered token (XDC: no form-D at all) is signed as a plain contract call and decimals is not used; on some EVM networks sending decimals for it makes the request fail (see Ethereum).
payload.transaction:
{
"token": {
"contract": "…", // token id: contract address / denom / assetIndex
"to": "…", // recipient
"amount": "1000", // base units — already ×10^decimals, no rescaling
"decimals": 6, // lets the device show the human amount
"symbol": "TKN" // label shown on the device screen
},
"from": "…" // your device account (sender; read-only)
}
| Field | Type | Required | Notes |
|---|---|---|---|
contract | string | Required | Token identifier — contract address, denom, or asset-id (per family; assetId / address are accepted as aliases). Without it the whole token object is ignored and only the top-level transaction fields are signed |
to | string | Required | Recipient address (Constellation reads the top-level destination instead) |
amount | string | Required | Base units — already ×10^decimals; do not rescale (Constellation reads the top-level amount instead) |
decimals | number | Required for a token not on DCENT's supported list (on EVM / Kaia it is not used — XDC has no form-D — an unregistered token is signed as a plain contract call, and on some EVM networks sending it fails; see Ethereum); optional for a registered token | Token decimals — lets the device show the human amount |
symbol | string | Optional | Display symbol shown on the device screen — display metadata only, not used for resolution |
These five fields are the token object. The sender sits alongside it, not inside.
The descriptor lives under transaction.token = { contract, to, amount, decimals, symbol }. amount is in base units — do not rescale it by decimals. A descriptor is accepted per family — see the form-D row of the table below, which includes Hedera, Stellar (issued assets) and Tezos (FA1.2/FA2). The rows and notes that follow cover the other token paths.
A descriptor only supplies display metadata — it never bypasses signing. The device still signs the exact transfer bytes, so the amount and recipient shown on screen must match what you send.
form-D vs form-E — two ways a token transfer reaches the device
The same transfer can arrive in one of several shapes; which one you send decides whether an unregistered token can be signed at all:
| Form | Families | Unregistered token |
|---|---|---|
form-D + decimalsstructured transaction.token | EVM (not XDC) · Kaia · TRON · Cosmos (Coreum) · Algorand · Havah · Stacks · Solana · VeChain · NEAR · Hedera · Constellation · Conflux (CRC-20) · Stellar (issued assets only) · Tezos (FA1.2/FA2 — the only structured path for an unregistered token) (15) | descriptor supported → signs. A registered token is displayed with its registered values (on Kaia only on the default m/44'/60' account); an unregistered token with the supplied decimals — except on EVM / Kaia, where an unregistered token is signed as a plain contract call (and on some EVM networks decimals makes it fail) |
| form-E native encoded tx; no descriptor | EVM · Kaia · TRON · Cosmos · Algorand · Havah · Stacks · VeChain · NEAR · Conflux · Stellar · Tezos · Hedera (all except EVM / Kaia: registered tokens only) (13 — not Constellation, see below; for Solana see the next column) | EVM · Kaia: still signs, but as a plain contract call (graceful fallback). Everything else: rejected — -32602. The bridge signs with no network access, so it cannot look decimals up for you. Solana (VersionedTransaction): the serialized transaction is signed as-is with no token identification, so the bridge does not identify an SPL transfer sent this way; use form-D for that |
| native asset — no registry at all token is a first-class field in the chain's own tx format | XRP / Xahau (issued currency — {currency, issuer, value}) (2) | Not applicable — there is no DCENT registry step to pass or fail. Any issuer the app supplies signs; the IOU value is a decimal string in the transaction itself, so there is no separate decimals field to gate |
| blob passthrough pre-built signing bytes | Stellar ({xdr} envelope — any operation, blind-signed; used for operations the structured shape does not build) · Tezos (unsignedTx, pre-forged — DCENT X accepts only reveal operations and at most one transaction) · Hedera (unsignedTx — fallback for operations beyond a plain transfer, e.g. mint / burn, or associate for a token not on DCENT's supported list) · Cardano ({txCbor} — the only way to move an existing multi-asset token; the structured host-assembled path covers mint/burn only) · Polkadot ({extra.scaleHex} SCALE-encoded call — the app must encode assets.transfer itself; the structured balances.* path moves the native asset, or a registered XC20 on Astar / Shibuya via transaction.token) (5) | For Stellar / Tezos / Hedera: not the deciding factor — these already have a form-D path for plain token transfers, blob passthrough is only for what form-D does not cover. For Polkadot: this is the way to move an Assets-pallet token that is not a registered Astar / Shibuya XC20; for Cardano it is the only way to move any existing native token, registered or not — the app must build the fully-encoded bytes itself (blind-sign, no device symbol/amount display); for Polkadot see the note below |
Constellation sits outside the family list above in spirit but is included in the form-D row — see its own note on the Constellation page for the metagraph-specific caveats (trust model differs between registered and unregistered metagraphs even though both sign).
Polkadot has no form-D descriptor builder. Its structured balances.* path moves the native asset — except on Astar / Shibuya, where a transaction.token naming a registered XC20 asset id makes it an assets.transfer(KeepAlive) of that asset (see the Polkadot page). For any other Assets-pallet token, send the call as a SCALE-hex blob (extra.scaleHex, see the row 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.
The Bitcoin-family coins (Bitcoin, Bitcoin Cash, Litecoin, Dogecoin, Dash, eCash, Zcash, Bitcoin Gold, DigiByte, Ravencoin) and Filecoin have no token-transfer concept in the wire protocol at all (native-asset-only families) — this table does not apply to them.
form-E (encoded) is the chain's native transaction passed through as-is — EVM/Kaia raw calldata (0xa9059cbb…), TRON raw_data, Solana VersionedTransaction, NEAR ft_transfer FunctionCall, VeChain clause { to, data, value }. Rule of thumb: send form-D + decimals whenever the token might not be on DCENT's supported list (on EVM / Kaia, omit decimals — an unregistered token is signed as a plain contract call there; XDC has no form-D) — that is the shape that signs an unregistered token on most families, and on every form-D family except EVM / Kaia and Solana (whose form-E transaction is signed as-is) it is the only structured shape that does. XRP/Xahau skip this question entirely — their tokens were never gated by a DCENT registry. Polkadot's structured balances.* path moves the native asset (or a registered Astar / Shibuya XC20 when transaction.token names it), and Cardano's structured path has no multi-asset output field at all (outputs[].tokenBundle → -32602). Either way there is a blind-sign blob-passthrough escape hatch (see table above), so the token can still move, just without a device-displayed symbol/amount. form-E stays valid for a registered token on the families that support it, and remains how you express anything that is not a plain transfer.
Source decimals from the chain, not a guess. The device renders the on-screen amount by scaling with decimals — a wrong value shows the user a wrong amount, exactly the mis-display that signing only known tokens used to prevent. Read decimals from the token contract's decimals() or a chain RPC. Sourcing it from the chain is what keeps unregistered-token signing as safe as a registered token.