stellar_ledger/emulator_test_support/
http_transport.rs

1// This is based on the `ledger-transport-zemu` crate's TransportZemuHttp: https://github.com/Zondax/ledger-rs/tree/master/ledger-transport-zemu
2// Instead of using TransportZemuHttp mod from the crate, we are including a custom copy here for a couple of reasons:
3// - we get more control over the mod for our testing purposes
4// - the ledger-transport-zemu TransportZemuHttp includes a Grpc implementation that we don't need right now, and was causing some errors with dependency mismatches when trying to use the whole TransportZemuHttp mod.
5
6use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE};
7use reqwest::{Client as HttpClient, Response};
8use serde::{Deserialize, Serialize};
9use std::ops::Deref;
10use std::time::Duration;
11
12use ledger_transport::{async_trait, APDUAnswer, APDUCommand, Exchange};
13
14use thiserror::Error;
15
16#[derive(Error, Debug)]
17pub enum LedgerZemuError {
18    /// zemu reponse error
19    #[error("Zemu response error")]
20    ResponseError,
21    /// Inner error
22    #[error("Ledger inner error")]
23    InnerError,
24}
25
26pub struct Emulator {
27    url: String,
28}
29
30#[derive(Serialize, Debug, Clone)]
31#[serde(rename_all = "camelCase")]
32struct ZemuRequest {
33    apdu_hex: String,
34}
35
36#[derive(Deserialize, Debug, Clone)]
37struct ZemuResponse {
38    data: String,
39    error: Option<String>,
40}
41
42impl Emulator {
43    #[allow(dead_code)] //this is being used in tests only
44    #[must_use]
45    pub fn new(host: &str, port: u16) -> Self {
46        Self {
47            url: format!("http://{host}:{port}"),
48        }
49    }
50}
51
52#[async_trait]
53impl Exchange for Emulator {
54    type Error = LedgerZemuError;
55    type AnswerType = Vec<u8>;
56
57    async fn exchange<I>(
58        &self,
59        command: &APDUCommand<I>,
60    ) -> Result<APDUAnswer<Self::AnswerType>, Self::Error>
61    where
62        I: Deref<Target = [u8]> + Send + Sync,
63    {
64        let raw_command = hex::encode(command.serialize());
65        let request = ZemuRequest {
66            apdu_hex: raw_command,
67        };
68
69        let mut headers = HeaderMap::new();
70        headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
71        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
72
73        let resp: Response = HttpClient::new()
74            .post(&self.url)
75            .headers(headers)
76            .timeout(Duration::from_secs(60))
77            .json(&request)
78            .send()
79            .await
80            .map_err(|e| {
81                tracing::error!("create http client error: {:?}", e);
82                LedgerZemuError::InnerError
83            })?;
84        tracing::debug!("http response: {:?}", resp);
85
86        if resp.status().is_success() {
87            let result: ZemuResponse = resp.json().await.map_err(|e| {
88                tracing::error!("error response: {:?}", e);
89                LedgerZemuError::ResponseError
90            })?;
91            if result.error.is_none() {
92                APDUAnswer::from_answer(hex::decode(result.data).expect("decode error"))
93                    .map_err(|_| LedgerZemuError::ResponseError)
94            } else {
95                Err(LedgerZemuError::ResponseError)
96            }
97        } else {
98            tracing::error!("error response: {:?}", resp.status());
99            Err(LedgerZemuError::ResponseError)
100        }
101    }
102}