# Service Module
The `service` module provides a hardware wallet device discovery and management
service. It polls for connected hardware wallets every 2 seconds and maintains a
shared device map with support for multiple concurrent consumers via
reference-counted start/stop.
## Features
- Automatic device discovery and connection
- Support for multiple concurrent consumers
- Reference-counted service lifecycle management
- Asynchronous device operations via message passing
- BitBox02 pairing configuration support
## Core Types
### `HwiService<Message, Id>`
The main service struct that manages device discovery and maintains the device map.
```rust
use async_hwi::service::{HwiService, SigningDeviceMsg};
use bitcoin::Network;
use crossbeam::channel;
// Define your application message type
#[derive(Clone)]
enum AppMessage {
Device(SigningDeviceMsg),
// ... other app messages
}
impl From<SigningDeviceMsg> for AppMessage {
fn from(msg: SigningDeviceMsg) -> Self {
AppMessage::Device(msg)
}
}
// Create the service
let service: HwiService<AppMessage> = HwiService::new(
Network::Bitcoin,
None, // Uses internal tokio runtime, or pass Some(handle) to use your own
);
```
### `SigningDevice<Message, Id>`
Represents a detected hardware wallet in one of three states:
- **`Supported`**: Device is ready for use
- **`Locked`**: Device requires unlocking (e.g., PIN entry, pairing confirmation)
- **`Unsupported`**: Device detected but cannot be used (wrong version, wrong
network, etc.)
### `SigningDeviceMsg<Id>`
Messages emitted by the service when device state changes:
```rust
pub enum SigningDeviceMsg<Id = ()> {
/// Error (None for polling errors, Some(id) for operation errors)
Error(Option<Id>, String),
/// Device map changed
Update,
/// Extended public key retrieved
XPub(Id, Fingerprint, DerivationPath, Xpub),
/// Device version retrieved
Version(Id, Fingerprint, Version),
/// Wallet registered with optional HMAC
WalletRegistered(Id, Fingerprint, String, Option<[u8; 32]>),
/// Wallet registration check result
WalletIsRegistered(Id, Fingerprint, String, bool),
/// Address displayed on device
AddressDisplayed(Id, Fingerprint, AddressScript),
/// Transaction signed
TransactionSigned(Id, Fingerprint, Psbt),
}
```
## Usage
### Basic Setup
```rust
use async_hwi::service::{HwiService, SigningDevice, SigningDeviceMsg};
use bitcoin::Network;
use crossbeam::channel;
use std::sync::Arc;
#[derive(Clone)]
enum AppMessage {
Device(SigningDeviceMsg),
}
impl From<SigningDeviceMsg> for AppMessage {
fn from(msg: SigningDeviceMsg) -> Self {
AppMessage::Device(msg)
}
}
fn main() {
// Create a channel for receiving device messages
let (sender, receiver) = channel::unbounded();
// Create the service
let service: Arc<HwiService<AppMessage>> = Arc::new(
HwiService::new(Network::Bitcoin, None)
);
// Start the service (reference counted)
service.start(sender);
// Process messages in your application loop
loop {
match receiver.recv() {
Ok(AppMessage::Device(SigningDeviceMsg::Update)) => {
// Device list changed, refresh UI
let devices = service.list();
for (id, device) in devices {
match device {
SigningDevice::Supported(supported) => {
println!("Ready: {} ({:?}) - {}",
id,
supported.kind(),
supported.fingerprint()
);
}
SigningDevice::Locked { id, kind, pairing_code, .. } => {
println!("Locked: {} ({:?})", id, kind);
if let Some(code) = pairing_code {
println!(" Pairing code: {}", code);
}
}
SigningDevice::Unsupported { id, kind, reason, .. } => {
println!("Unsupported: {} ({:?}) - {:?}", id, kind,reason);
}
}
}
}
Ok(AppMessage::Device(SigningDeviceMsg::Error(id, err))) => {
eprintln!("Error (id={:?}): {}", id, err);
}
Ok(AppMessage::Device(msg)) => {
// Handle other device messages
println!("Device message: {:?}", msg);
}
Err(_) => break,
}
}
// Stop the service when done
service.stop();
}
```
### Using Device Operations
Operations on `SupportedDevice` are asynchronous and return results via the message
channel:
```rust
use async_hwi::service::{SigningDevice, SigningDeviceMsg, SupportedDevice};
use bitcoin::bip32::DerivationPath;
use std::str::FromStr;
// Get a supported device from the service
let devices = service.list();
for (id, device) in devices {
if let SigningDevice::Supported(supported) = device {
// Request an extended public key
// Results arrive via SigningDeviceMsg::XPub
let path = DerivationPath::from_str("m/84'/0'/0'").unwrap();
supported.get_extended_pubkey((), &path);
// Register a wallet policy
// Results arrive via SigningDeviceMsg::WalletRegistered
supported.register_wallet(
(),
"My Wallet",
"wsh(sortedmulti(2,@0/**,@1/**))"
);
// Check if wallet is registered
// Results arrive via SigningDeviceMsg::WalletIsRegistered
supported.is_wallet_registered(
(),
"My Wallet",
"wsh(sortedmulti(2,@0/**,@1/**))"
);
// Display an address on the device
// Results arrive via SigningDeviceMsg::AddressDisplayed
use async_hwi::AddressScript;
let path = DerivationPath::from_str("m/86'/0'/0'/0/0").unwrap();
supported.display_address((), &AddressScript::P2TR(path));
// Sign a PSBT
// Results arrive via SigningDeviceMsg::TransactionSigned
// supported.sign_tx((), psbt);
}
}
```
### Using Request IDs
The `Id` type parameter allows tracking which request a response corresponds to:
```rust
use async_hwi::service::{HwiService, SigningDeviceMsg};
#[derive(Clone, Debug)]
struct RequestId(u64);
#[derive(Clone)]
enum AppMessage {
Device(SigningDeviceMsg<RequestId>),
}
impl From<SigningDeviceMsg<RequestId>> for AppMessage {
fn from(msg: SigningDeviceMsg<RequestId>) -> Self {
AppMessage::Device(msg)
}
}
// Create service with custom ID type
let service: HwiService<AppMessage, RequestId> = HwiService::new(Network::Bitcoin,
None);
// Later, when making requests:
// supported.get_extended_pubkey(RequestId(42), &path);
// When handling responses:
// SigningDeviceMsg::XPub(RequestId(42), fingerprint, path, xpub)
```
### BitBox02 Pairing Configuration
For BitBox02 devices, you can provide a noise configuration to persist pairing:
```rust
use async_hwi::bitbox::{NoiseConfig, NoiseConfigData, ConfigError};
use std::sync::Arc;
struct MyNoiseConfig {
// Your storage implementation
}
impl bitbox_api::Threading for MyNoiseConfig {}
impl NoiseConfig for MyNoiseConfig {
fn read_config(&self) -> Result<NoiseConfigData, ConfigError> {
// Read from your storage
todo!()
}
fn store_config(&self, data: &NoiseConfigData) -> Result<(), ConfigError> {
// Write to your storage
todo!()
}
}
// Set the configuration before starting the service
let noise_config: Arc<dyn NoiseConfig> = Arc::new(MyNoiseConfig { /* ... */ });
service.set_bitbox_noise_config(noise_config);
// Start the service
service.start(sender);
// Later, if needed:
// service.clear_bitbox_noise_config();
```
### Multiple Consumers
The service supports multiple concurrent consumers with reference counting:
```rust
// First consumer starts the service
service.start(sender1.clone());
// Second consumer increments ref count (service already running)
service.start(sender2.clone());
// First consumer done - decrements ref count (service keeps running)
service.stop();
// Second consumer done - decrements ref count to 0, service stops
service.stop();
```
## Device States
### Supported Devices
A `SupportedDevice` provides access to:
- `device()` - The underlying `HWI` trait object
- `version()` - Device firmware version
- `fingerprint()` - Master key fingerprint
- `kind()` - Device type (Ledger, BitBox02, etc.)
### Locked Devices
Devices in the `Locked` state require user interaction:
- **BitBox02**: Requires pairing confirmation on device (displays pairing code)
- **Jade**: Requires PIN entry and blind oracle authentication
The service automatically attempts to unlock devices. Monitor
`SigningDeviceMsg::Update` for state transitions.
### Unsupported Devices
Devices may be unsupported for various reasons:
```rust
pub enum UnsupportedReason {
/// Firmware version too old
Version { minimal_supported_version: &'static str },
/// Method not supported by device
Method(&'static str),
/// Device not part of wallet (fingerprint mismatch)
NotPartOfWallet(Fingerprint),
/// Device configured for different network
WrongNetwork,
/// Ledger: Bitcoin app not open
AppIsNotOpen,
}
```
## Taproot Miniscript Compatibility
Check if a device supports Taproot Miniscript:
```rust
use async_hwi::service::is_compatible_with_tapminiscript;
use async_hwi::DeviceKind;
let compatible = is_compatible_with_tapminiscript(
&DeviceKind::Ledger,
Some(&version)
);
```
Minimum versions for Taproot Miniscript support:
- Ledger: v2.2.0
- Coldcard: v6.3.3
- BitBox02: v9.21.0
- Specter: All versions