Skip to main content

Bitcoin (BTC)

UTXO-based signing. Provide the previous transactions for the inputs you spend; the device signs each input by its keyPath.

FieldValue
FamilyBitcoin (UTXO)
coinType0
keyPathm/44'/0'/<accountIdx>'/0/0 (also m/49' · m/84' · m/86')
addressFormatlegacy / segwit-native / segwit-wrapped / taproot
chainIdbip122:000000000019d6689c085ae165831e93/slip44:0
chainId · Testnetbip122:000000000933ea01ad0ee984209779ba/slip44:0 (keyPath coinType 1': m/44'/1' · m/49'/1' · m/84'/1' · m/86'/1')

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. Some account types need a higher version than the table — see the note below it.

MethodDCENT Biometric WalletDCENT X
getAddress1.0.0 or higher1.0.0 or higher
signTransaction1.0.0 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. SegWit (bech32) addresses on DCENT's own m/44' account require firmware 1.7.0 or higher. The BIP-49 (m/49'), BIP-84 (m/84') and Taproot (m/86') accounts need DCENT Biometric Wallet firmware 2.36.2 or higher. Taproot is the one exception on DCENT X — it needs 1.0.1 or higher (see below).

addressFormat​

FormatPathStatus
legacy (P2PKH)m/44'/0'/0'/0/0Supported
segwit-native (P2WPKH, bech32)m/44'/0'/0'/0/0 — DCENT's own accountSupported
segwit-native (P2WPKH, bech32)m/84'/0'/0'/0/0 — BIP-84 standard accountSupported — DCENT Biometric Wallet firmware 2.36.2 or higher
segwit-wrapped (P2SH-P2WPKH, BIP-49)m/49'/0'/0'/0/0Supported — DCENT Biometric Wallet firmware 2.36.2 or higher
taproot (P2TR, BIP-86)m/86'/0'/0'/0/0Supported — DCENT Biometric Wallet firmware 2.36.2 or higher, DCENT X firmware 1.0.1 or higher
warning

The multi-step builder needs connector 2.1.0 for Taproot inputs. In 2.0.0, addBitcoinTransactionInput() rejects 'p2tr' with param_error, and the BitcoinWireTxType type leaves it out. addressFormat: 'taproot' and a transaction object passed straight to sign() are not filtered by the connector. Upgrade the package if you use the builder, or if you annotate inputs with BitcoinWireTxType and want 'p2tr' to type-check — nothing else on the dApp side changes.

Taproot needs DCENT X firmware 1.0.1 or higher. Signing a Taproot input sends the previous transaction to the wallet in DCENT's compact form, and 1.0.0 does not recognise that form for P2TR — the request fails with an invalid-format error from the device. This is a wallet-side limit, not a connector one. Update the wallet firmware and no dApp change is needed.

info

Sending to a Taproot address never needed a Taproot account. Put the bc1p… address in outputs[].addresses with txType: 'p2tr' — that works on 2.0.0 as well. What needs a Taproot account is the other direction: spending UTXOs that belong to your own m/86' account.

info

The keyPath picks the account; addressFormat picks the encoding. On m/44' the two encodings share one key: legacy and segwit-native both derive m/44'/0'/0'/0/0 and only differ in how that key is written (P2PKH Base58 vs. P2WPKH bech32). The BIP-49/84/86 purposes are separate accounts with their own keys — m/49' (wrapped SegWit), m/84' (native SegWit) and m/86' (Taproot) each hold different coins from m/44'. Send the keyPath and the addressFormat that belong together; a mismatch such as legacy with m/86' is rejected with -32602.

warning

BIP-49 / BIP-84 accounts need no connector upgrade, but they do need a current wallet. m/49' and m/84' resolve inside the DCENT wallet, not in the connector — so the npm package version does not matter for them. On the DCENT Biometric Wallet they also need firmware 2.36.2 or higher.

getAddress — account address​

Retrieves the Bitcoin address for a keyPath. The returned format follows addressFormat paired with the derivation purpose: legacy (P2PKH) returns a Base58Check address starting with 1, segwit-native (P2WPKH) a bech32 address starting with bc1q, segwit-wrapped (P2SH-P2WPKH, m/49') a Base58Check address starting with 3, and taproot (P2TR, m/86') a bech32m address starting with bc1p. On m/44' the legacy and segwit-native encodings share one key; m/49', m/84' and m/86' are separate accounts.

Parameters​

FieldTypeRequiredSourceNotes
chainIdstringRequiredAppFull CAIP-19 — bip122:000000000019d6689c085ae165831e93/slip44:0
keyPathstringRequiredAppm/44'/0'/0'/0/0 for DCENT's own legacy and segwit-native accounts (addressFormat selects the encoding), m/49' for segwit-wrapped, m/84' for the BIP-84 native SegWit account, m/86' for taproot
addressFormatenumOptionalApplegacy / segwit-native / segwit-wrapped / taproot. Pair it with the matching keyPath purpose — legacy with m/86', or segwit-native with m/49', is rejected with -32602. If omitted, the keyPath purpose selects the account: m/44' returns the legacy (1…) address, m/49' / m/84' / m/86' return that account's address

Request

await dcent.getAddress({
chainId: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
keyPath: "m/44'/0'/0'/0/0",
addressFormat: 'segwit-native'
})

Response

FieldTypeNotes
body.parameter.addressstringThe Bitcoin address of the keyPath account, in the encoding chosen by addressFormat. Bitcoin does not return a separate public key.
• legacy → Base58Check P2PKH (1…)
• segwit-native → bech32 P2WPKH (bc1q…)
• segwit-wrapped → Base58Check P2SH-P2WPKH (3…)
• taproot → bech32m P2TR (bc1p…)
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "getAddress"
},
"body": {
"command": "getAddress",
"parameter": {
"address": "bc1q…"
}
}
}

signTransaction​

Payload​

FieldTypeRequiredSourceNotes
keyPathstringRequiredAppThe account that signs — m/44' (legacy / segwit-native), m/49' (segwit-wrapped), m/84' (BIP-84 native SegWit) or m/86' (taproot)
addressFormatenumOptionalAppSame enum as getAddress. Selects which Bitcoin variant signs. Omit it and the variant is inferred from the keyPath purpose and inputs[].txType (see below) — on m/44' it decides between two encodings that share one key; on m/49' / m/84' / m/86' an input txType of p2pkh, p2wpkh / p2wsh or p2tr that does not match the account is rejected with -32602 (p2sh maps to no format and is not checked — see inputs[].txType below).
• legacy / segwit-native / segwit-wrapped — supported
• taproot — supported (object form; a Taproot input in a PSBT is rejected)
transactionobject | stringRequiredAppThe UTXO payload below, or a PSBT base64 string. A PSBT is limited: a Taproot (P2TR) input is rejected with -32602, every input needs bip32Derivation (otherwise -32602), and an output that pays back to an input's script is sent to the device as an address, but DCENT X reads a change output's to as a key path and rejects an address — send change with the object form
info

Pass the addressFormat you got from getAccountInfo. On m/44', Bitcoin's legacy and native SegWit accounts share one chainId and one keyPath, so neither of those two fields identifies which account is signing. Without addressFormat the signer falls back to reading inputs[].txType, and that signal is missing from PSBT payloads. getAccountInfo returns meta.addressFormat for the m/44', m/49' and m/86' accounts — send it straight back on signTransaction. An m/84' account carries none, because its keyPath alone identifies it.

If you send both and they disagree (for example addressFormat: 'legacy' with txType: 'p2wpkh' inputs), the request is rejected with -32602 rather than one of them silently winning.

payload.transaction:

{
"inputs": [
{
"rawTransaction": "<prev tx hex>",
"index": 0,
"txType": "p2pkh",
"keyPath": "m/44'/0'/0'/0/0",
"sequence": 4294967295
}
],
"outputs": [
{
"txType": "p2pkh",
"amount": "990000",
"addresses": [
"<recipient>"
]
}
]
}
FieldTypeRequiredSourceUnitNotes
inputs[].rawTransactionhexRequired-RealWallet—Full prev-tx hex of the UTXO; must be device-owned. Zcash uses a different format — see Zcash
inputs[].indexnumberRequiredApp—UTXO output index
inputs[].txTypeenumRequiredApp—The script type of the UTXO being spent. When payload.addressFormat is omitted this doubles as the fallback signal for legacy vs native SegWit (on m/44', chainId and keyPath are identical across both variants). Prefer the explicit addressFormat — this fallback cannot express every case: p2sh is ambiguous (legacy multisig vs wrapped SegWit) and PSBT payloads carry no txType at all.
• p2pkh = legacy scriptSig → legacy
• p2wpkh / p2wsh = native SegWit witness → segwit-native
• p2sh = accepted, but on its own it maps to no format, so the account follows keyPath: a m/49' keyPath selects the wrapped-SegWit account, a m/44' keyPath falls back to the legacy account. Wrapped-SegWit UTXOs are p2sh too, so send addressFormat: 'segwit-wrapped' with a m/49' keyPath for those
• p2tr = Taproot witness → taproot. The account is the m/86' one, so send keyPath: "m/86'/0'/0'/0/0" with it
All inputs must share one txType; mixed script types are rejected with -32602.
inputs[].keyPathstringRequired-RealWallet—Path that signs this input
inputs[].sequencenumberOptionalApp—When omitted, Bitcoin mainnet accounts send 0xfffffffd (opt-in RBF); every other currency, including Bitcoin testnet, sends no sequence, and DCENT X then uses 0xffffffff. Also note the first input's value applies to the whole transaction (a first-input value of 0 is not sent, so the device's default applies).
outputs[].txTypeenumRequiredApp—The output script type — a different set from inputs[].txType. p2pkh / p2sh / p2wpkh / p2wsh, plus p2tr for a Taproot recipient and change for the change output.
• p2tr — supported: you can send to a bc1p… address. Use it whenever addresses[0] is Taproot; labelling a bc1p… address as anything else is rejected with param_error.
• change — one change output at most, and its addresses[0] is a derivation path, not an address (see the row below).
outputs[].amountstringRequiredAppsatoshiOutput value
outputs[].addressesstring[]Required-RealApp—Only addresses[0] is signed (later entries are not signed — send a single address), and exactly one non-change output is allowed — more than one destination is rejected with param_error. For the recipient output this is the destination address. For txType: 'change' it is a BIP-32 derivation path on your own account (for example m/44'/0'/0'/1/0, or m/86'/0'/0'/1/0 on a Taproot account) — the device derives the change address from that path itself. Use a path on the account you are signing with (the same purpose, coin type and account as the inputs). Passing an address here is rejected — with -32602 on a Taproot account (or when it is a Taproot address), by the device otherwise.
optionobject | hexZcash onlyApp—Zcash only — every other Bitcoin-family currency omits this field. (Horizen also used it for the BIP-115 replay-protection tail, but the native ZEN chain was shut down by the 2025-07-23 migration to a Base ERC-20, and signTransaction for it is no longer supported.) Zcash: a 16-hex-char string (GroupId + consensus branch ID). The signer has no network access to derive either value itself; omitting it is rejected with -32602. For Zcash, a wrong GroupId, a length other than 16 hex chars, or a branch ID the signer does not recognize is also rejected with -32602 — but a stale branch ID the signer still recognizes (e.g. NU6.2 after NU6.3 activated) is not detected and still signs, so the transaction fails only at broadcast.

Change output — the second output carries a derivation path where the recipient output carries an address. This example spends a Taproot UTXO and sends the change back to the same account's change branch:

{
"inputs": [
{
"rawTransaction": "<prev tx hex>",
"index": 0,
"txType": "p2tr",
"keyPath": "m/86'/0'/0'/0/0"
}
],
"outputs": [
{
"txType": "p2wpkh",
"amount": "10000",
"addresses": [
"<recipient>"
]
},
{
"txType": "change",
"amount": "14590",
"addresses": [
"m/86'/0'/0'/1/0"
]
}
]
}

The fee is implicit: inputs minus outputs. The same change rule applies to every Bitcoin-family chain — only the coinType in the path differs.

Response

{
"header": {
"version": "1.0",
"status": "success",
"response_from": "signTransaction"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "<signed raw tx hex>"
}
}
}
FieldTypeNotes
header.statusstring"success" when the device completes signing
body.commandstring"signTransaction"
body.parameter.signaturehexThe broadcast-ready signed raw transaction hex for Bitcoin — not a bare signature. Submit it with sendrawtransaction.

Variants — multi-step builder​

As an alternative to the single-call form, a stateful builder is available:

const tx = dcent.getBitcoinTransactionObject()
dcent.addBitcoinTransactionInput(tx, prevTxHex, utxoIndex, 'p2pkh', "m/44'/0'/0'/0/0")
dcent.addBitcoinTransactionOutput(tx, 'p2pkh', 990000, recipient)
// then sign

addBitcoinTransactionInput() accepts 'p2tr' (a Taproot UTXO on the m/86' account) from connector 2.1.0; 2.0.0 rejects it with param_error. From 2.1.0 the builder checks only the shape of type — a non-empty string of at most 64 characters that is not a prototype key — and not whether it is a known value. That is left to the wallet, which answers -32602 for a well-formed input type it does not support.

Common Mistakes​

danger

Assuming m/84' and DCENT's own native SegWit account are the same thing — they are two different accounts and hold different coins. DCENT's own native SegWit account lives on m/44' (addressFormat selects the encoding there), while m/84' is the BIP-84 standard account. Both work; pick the one whose coins you mean, and pair m/49' with segwit-wrapped. Dropping addressFormat on signTransaction — on m/44', chainId and keyPath are the same for the legacy and native SegWit accounts, so a signing request without addressFormat falls back to inputs[].txType; send back the meta.addressFormat from getAccountInfo instead. Missing change output — unspent input value becomes miner fee. Amounts not in satoshi. Missing option for Zcash — the signer cannot fetch the consensus branch ID itself; the app must supply it (a 16-hex-char string); omitting it is rejected with -32602. Horizen is discontinued and signTransaction is rejected for it before any of this applies.