ledger_lib/provider/
mod.rs

1//! [LedgerProvider] provides a tokio-based thread-safe interface for
2//! interacting with ledger devices.
3
4use std::time::Duration;
5
6use tokio::sync::{
7    mpsc::{unbounded_channel, UnboundedSender},
8    OnceCell,
9};
10
11mod context;
12use context::ProviderContext;
13
14use crate::{error::Error, info::LedgerInfo, transport::Transport, Exchange, Filters};
15
16/// Ledger provider manages device discovery and connection
17pub struct LedgerProvider {
18    req_tx: ReqChannel,
19}
20
21/// Ledger device handle for interacting with [LedgerProvider] backed devices
22#[derive(Debug)]
23pub struct LedgerHandle {
24    pub info: LedgerInfo,
25
26    /// Device index in provider map
27    index: usize,
28
29    /// Channel for issuing requests to the provider task
30    req_tx: ReqChannel,
31}
32
33/// Request object for communication to the provider task
34#[derive(Clone, Debug, PartialEq)]
35pub enum LedgerReq {
36    /// List available devices
37    List(Filters),
38
39    /// Connect to a specific device
40    Connect(LedgerInfo),
41
42    /// APDU request issued to a device handle
43    Req(usize, Vec<u8>, Duration),
44
45    /// Close the device handle
46    Close(usize),
47}
48
49/// Request object for communication from the provider task
50#[derive(Debug)]
51pub enum LedgerResp {
52    /// List of available ledger devices
53    Devices(Vec<LedgerInfo>),
54
55    /// Device handle following connection
56    Handle(usize),
57
58    /// APDU response from a device handle
59    Resp(Vec<u8>),
60
61    /// Error / operation failure
62    Error(Error),
63}
64
65/// Helper type alias for [LedgerProvider] requests
66pub type ReqChannel = UnboundedSender<(LedgerReq, UnboundedSender<LedgerResp>)>;
67
68/// Global provider context, handle for pinned thread used for device communication
69static PROVIDER_CTX: OnceCell<ProviderContext> = OnceCell::const_new();
70
71impl LedgerProvider {
72    /// Create or connect to the ledger provider instance
73    pub async fn init() -> Self {
74        // Fetch or create the provider context
75        let ctx = PROVIDER_CTX
76            .get_or_init(|| async { ProviderContext::new().await })
77            .await;
78
79        // Return handle to request channel
80        Self {
81            req_tx: ctx.req_tx(),
82        }
83    }
84}
85
86/// [Transport] implementation for high-level [LedgerProvider]
87impl Transport for LedgerProvider {
88    type Device = LedgerHandle;
89    type Info = LedgerInfo;
90    type Filters = Filters;
91
92    /// List available devices using the specified filter
93    async fn list(&mut self, filters: Filters) -> Result<Vec<LedgerInfo>, Error> {
94        let (tx, mut rx) = unbounded_channel::<LedgerResp>();
95
96        // Send control request
97        self.req_tx
98            .send((LedgerReq::List(filters), tx))
99            .map_err(|_| Error::RequestChannelClosed)?;
100
101        // Await response
102        match rx.recv().await {
103            Some(LedgerResp::Devices(i)) => Ok(i),
104            Some(LedgerResp::Error(e)) => Err(e),
105            Some(LedgerResp::Resp(_) | LedgerResp::Handle(_)) => {
106                Err(Error::UnexpectedResponseWhileListingDevices)
107            }
108            None => Err(Error::RequestResponseChannelClosed),
109        }
110    }
111
112    /// Connect to an available device
113    async fn connect(&mut self, info: LedgerInfo) -> Result<LedgerHandle, Error> {
114        let (tx, mut rx) = unbounded_channel::<LedgerResp>();
115
116        // Send control request
117        self.req_tx
118            .send((LedgerReq::Connect(info.clone()), tx))
119            .map_err(|_| Error::RequestChannelClosed)?;
120
121        // Await response
122        match rx.recv().await {
123            Some(LedgerResp::Handle(index)) => Ok(LedgerHandle {
124                info,
125                index,
126                req_tx: self.req_tx.clone(),
127            }),
128            Some(LedgerResp::Error(e)) => Err(e),
129            Some(LedgerResp::Devices(_) | LedgerResp::Resp(_)) => {
130                Err(Error::UnexpectedResponseWhileConnecting)
131            }
132            None => Err(Error::RequestResponseChannelClosed),
133        }
134    }
135}
136
137/// [Exchange] implementation for [LedgerProvider] backed [LedgerHandle]
138impl Exchange for LedgerHandle {
139    async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
140        let (tx, mut rx) = unbounded_channel::<LedgerResp>();
141
142        // Send APDU request
143        self.req_tx
144            .send((LedgerReq::Req(self.index, command.to_vec(), timeout), tx))
145            .map_err(|_| Error::RequestChannelClosed)?;
146
147        // Await APDU response
148        match rx.recv().await {
149            Some(LedgerResp::Resp(data)) => Ok(data),
150            Some(LedgerResp::Error(e)) => Err(e),
151            Some(LedgerResp::Devices(_) | LedgerResp::Handle(_)) => {
152                Err(Error::UnexpectedResponseWhileExchangingData)
153            }
154            None => Err(Error::RequestResponseChannelClosed),
155        }
156    }
157}
158
159/// [Drop] impl sends close message to provider when [LedgerHandle] is dropped
160impl Drop for LedgerHandle {
161    fn drop(&mut self) {
162        let (tx, _rx) = unbounded_channel::<LedgerResp>();
163        let _ = self.req_tx.send((LedgerReq::Close(self.index), tx));
164    }
165}