#![cfg_attr(feature = "unstable_async_trait", feature(async_fn_in_trait))]
#![cfg_attr(feature = "unstable_async_trait", feature(negative_impls))]
use std::time::Duration;
use tracing::debug;
use ledger_proto::{
apdus::{ExitAppReq, RunAppReq},
GenericApdu, StatusCode,
};
pub mod info;
pub use info::LedgerInfo;
mod error;
pub use error::Error;
pub mod transport;
pub use transport::Transport;
mod provider;
pub use provider::{LedgerHandle, LedgerProvider};
mod device;
pub use device::Device;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3);
#[derive(Copy, Clone, Debug, PartialEq, strum::Display)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[non_exhaustive]
pub enum Filters {
Any,
Hid,
Tcp,
Ble,
}
impl Default for Filters {
fn default() -> Self {
Self::Any
}
}
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
pub trait Exchange {
async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error>;
}
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
impl<T: Exchange + Send> Exchange for &mut T {
async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
<T as Exchange>::exchange(self, command, timeout).await
}
}
pub async fn launch_app<T>(
mut t: T,
info: <T as Transport>::Info,
app_name: &str,
opts: &LaunchAppOpts,
timeout: Duration,
) -> Result<<T as Transport>::Device, Error>
where
T: Transport<Info = LedgerInfo, Filters = Filters> + Send,
<T as Transport>::Device: Send,
{
let mut buff = [0u8; 256];
debug!("Connecting to {info:?}");
let mut d = t.connect(info.clone()).await?;
let i = d.app_info(timeout).await?;
if i.name == app_name {
debug!("Already running app {app_name}");
return Ok(d);
}
if i.name != "BOLOS" {
debug!("Exiting running app {}", i.name);
match d
.request::<GenericApdu>(ExitAppReq::new(), &mut buff, timeout)
.await
{
Ok(_) | Err(Error::Status(StatusCode::Ok)) => (),
Err(e) => return Err(e),
}
debug!("Exit complete, reconnecting to {info:?}");
drop(d);
tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
d = reconnect(&mut t, info.clone(), opts).await?;
}
for i in 0..10 {
debug!("Issuing run request ({i}/10)");
let resp = d
.request::<GenericApdu>(RunAppReq::new(app_name), &mut buff, timeout)
.await;
match resp {
Ok(_) | Err(Error::Status(StatusCode::Ok)) => {
debug!("Run request complete, reconnecting to {info:?}");
drop(d);
tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
d = reconnect(&mut t, info.clone(), opts).await?;
return Ok(d);
}
Err(Error::EmptyResponse) => tokio::time::sleep(Duration::from_secs(1)).await,
Err(e) => return Err(e),
}
}
Err(Error::Timeout)
}
pub struct LaunchAppOpts {
pub reconnect_delay_s: usize,
pub reconnect_timeout_s: usize,
}
impl Default for LaunchAppOpts {
fn default() -> Self {
Self {
reconnect_delay_s: 3,
reconnect_timeout_s: 10,
}
}
}
async fn reconnect<T: Transport<Info = LedgerInfo, Filters = Filters>>(
mut t: T,
info: LedgerInfo,
opts: &LaunchAppOpts,
) -> Result<<T as Transport>::Device, Error> {
let mut new_info = None;
let filters = Filters::from(info.kind());
debug!("Starting reconnect");
for i in 0..opts.reconnect_timeout_s {
debug!("Listing devices ({i}/{})", opts.reconnect_timeout_s);
let devices = t.list(filters).await?;
match devices
.iter()
.find(|i| i.model == info.model && i.kind() == info.kind())
{
Some(i) => {
new_info = Some(i.clone());
break;
}
None => tokio::time::sleep(Duration::from_secs(1)).await,
};
}
let new_info = match new_info {
Some(v) => v,
None => return Err(Error::Closed),
};
debug!("Device found, reconnecting!");
let d = t.connect(new_info).await?;
Ok(d)
}