penumbra_sdk_custody_ledger_usb/
lib.rs

1//! Ledger USB custody for Penumbra.
2//!
3//! This crate implements Penumbra custody support for Ledger hardware wallets via USB.
4
5/// Abstraction layer over the ledger libraries for device interaction.
6mod device;
7
8use std::{ops::DerefMut, sync::Arc};
9
10use device::Device;
11use penumbra_sdk_custody::AuthorizeRequest;
12use penumbra_sdk_keys::{keys::AddressIndex, Address, FullViewingKey};
13use penumbra_sdk_proto::custody::v1::{self as pb, AuthorizeResponse};
14use penumbra_sdk_transaction::{AuthorizationData, TransactionPlan};
15use serde::{Deserialize, Serialize};
16use tokio::sync::{Mutex, MutexGuard};
17use tonic::{async_trait, Request, Response, Status};
18
19/// Options needed to create a new config for custodying with a ledger device.
20#[derive(Default)]
21pub struct InitOptions {}
22
23/// Contains configuration for custody with a ledger device.
24#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
25pub struct Config {}
26
27impl Config {
28    /// Initialize custody with a device.
29    pub async fn initialize(_opts: InitOptions) -> anyhow::Result<Self> {
30        // In the future, we might do things like actually create some kind
31        // of device checksum or something like that.
32        Ok(Self {})
33    }
34}
35
36/// Implements the APIs to allow using
37pub struct Service {
38    device: Arc<Mutex<Option<Device>>>,
39}
40
41impl Service {
42    pub fn new(_config: Config) -> Self {
43        Self {
44            device: Arc::new(Default::default()),
45        }
46    }
47
48    /// Acquire a new device, potentially initializing it if necessary.
49    ///
50    /// The resulting handle can be dereferenced to get a device.
51    /// This handle will have exclusive access to the device.
52    async fn acquire_device(&self) -> anyhow::Result<impl DerefMut<Target = Device> + '_> {
53        let mut guard = self.device.lock().await;
54        if guard.is_none() {
55            *guard = Some(Device::connect_to_first().await?);
56        }
57        let out = MutexGuard::map(guard, |x| x.as_mut().expect("device should be initialized"));
58        Ok(out)
59    }
60
61    /// A convenience method for getting the FVK.
62    ///
63    /// This also exists by virtue of implementing [`pb::custody_service_server::CustodyService`],
64    /// but calling that method is less ergonomic, and ultimately defers to this anyways.
65    pub async fn impl_export_full_viewing_key(&self) -> anyhow::Result<FullViewingKey> {
66        self.acquire_device().await?.get_fvk().await
67    }
68
69    /// A convenience method for confirming an address.
70    ///
71    /// This will ask the user to confirm the address on their device.
72    pub async fn impl_confirm_address(&self, index: AddressIndex) -> anyhow::Result<Address> {
73        self.acquire_device().await?.confirm_addr(index).await
74    }
75
76    /// A convenience method for authorizing a transaction
77    pub async fn impl_authorize(&self, plan: TransactionPlan) -> anyhow::Result<AuthorizationData> {
78        self.acquire_device().await?.authorize(plan).await
79    }
80}
81
82#[async_trait]
83impl pb::custody_service_server::CustodyService for Service {
84    async fn authorize(
85        &self,
86        request: Request<pb::AuthorizeRequest>,
87    ) -> Result<Response<AuthorizeResponse>, Status> {
88        let request: AuthorizeRequest = request
89            .into_inner()
90            .try_into()
91            .map_err(|e: anyhow::Error| Status::invalid_argument(e.to_string()))?;
92
93        let authorization_data = self
94            .impl_authorize(request.plan)
95            .await
96            .map_err(|e| Status::unauthenticated(format!("{e:#}")))?;
97
98        let authorization_response = AuthorizeResponse {
99            data: Some(authorization_data.into()),
100        };
101
102        Ok(Response::new(authorization_response))
103    }
104
105    async fn authorize_validator_definition(
106        &self,
107        _request: Request<pb::AuthorizeValidatorDefinitionRequest>,
108    ) -> Result<Response<pb::AuthorizeValidatorDefinitionResponse>, Status> {
109        unimplemented!("ledger does not support validator operations")
110    }
111
112    async fn authorize_validator_vote(
113        &self,
114        _request: Request<pb::AuthorizeValidatorVoteRequest>,
115    ) -> Result<Response<pb::AuthorizeValidatorVoteResponse>, Status> {
116        unimplemented!("ledger does not support validator operations")
117    }
118
119    async fn export_full_viewing_key(
120        &self,
121        _request: Request<pb::ExportFullViewingKeyRequest>,
122    ) -> Result<Response<pb::ExportFullViewingKeyResponse>, Status> {
123        let fvk = self
124            .impl_export_full_viewing_key()
125            .await
126            .map_err(|e| Status::internal(format!("{}", e)))?;
127        Ok(Response::new(pb::ExportFullViewingKeyResponse {
128            full_viewing_key: Some(fvk.into()),
129        }))
130    }
131
132    async fn confirm_address(
133        &self,
134        request: Request<pb::ConfirmAddressRequest>,
135    ) -> Result<Response<pb::ConfirmAddressResponse>, Status> {
136        let address_index = request
137            .into_inner()
138            .address_index
139            .ok_or_else(|| {
140                Status::invalid_argument("missing address index in confirm address request")
141            })?
142            .try_into()
143            .map_err(|e| {
144                Status::invalid_argument(format!(
145                    "invalid address index in confirm address request: {e:#}"
146                ))
147            })?;
148        let address = self
149            .impl_confirm_address(address_index)
150            .await
151            .map_err(|e| Status::internal(format!("{}", e)))?;
152
153        Ok(Response::new(pb::ConfirmAddressResponse {
154            address: Some(address.into()),
155        }))
156    }
157}