Skip to main content

ledger_cosmos_secp256k1/
lib.rs

1#![allow(clippy::too_many_arguments)]
2use byteorder::{LittleEndian, WriteBytesExt};
3use cosmrs::{tendermint::PublicKey, tx::Msg};
4use errors::LedgerCosmosError;
5
6use k256::ecdsa::Signature;
7use ledger_transport::{APDUCommand, APDUErrorCode, Exchange};
8use ledger_zondax_generic::{App, AppExt};
9use log::info;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13pub mod errors;
14
15/// CLA for the cosmos app.
16const COSMOS_CLA: u8 = 0x55;
17
18/// Instruction for getting the cosmos app version.
19const GET_VERSION_INS: u8 = 0x00;
20
21/// Instruction for getting the secp256k1 public key.
22const GET_ADDR_SECP256K1_INS: u8 = 0x04;
23
24/// Instruction for signing a secp256k1 transaction.
25const SIGN_SECP256K1_INS: u8 = 0x02;
26
27pub struct CosmosApp<T>
28where
29    T: Exchange + Send + Sync,
30    T::Error: std::error::Error,
31{
32    transport: T,
33}
34
35impl<T> App for CosmosApp<T>
36where
37    T: Exchange + Send + Sync,
38    T::Error: std::error::Error,
39{
40    const CLA: u8 = COSMOS_CLA;
41}
42
43#[derive(Serialize, Deserialize, Clone, Debug)]
44pub struct AnyJson {
45    #[serde(rename = "type")]
46    pub type_url: String,
47    pub value: Value,
48}
49
50pub trait IntoAnyJson: Msg + Serialize {
51    fn into_any_json(self) -> AnyJson;
52}
53
54impl<T> IntoAnyJson for T
55where
56    T: Msg + Serialize,
57{
58    fn into_any_json(self) -> AnyJson {
59        let type_url = self.to_any().unwrap().type_url;
60        let value = serde_json::to_value(self).unwrap();
61        AnyJson { type_url, value }
62    }
63}
64
65#[derive(Debug)]
66pub struct CosmosAppVersion {
67    pub test_mode: u8,
68    pub major: u8,
69    pub minor: u8,
70    pub patch: u8,
71    pub locked: u8,
72}
73
74pub struct Secp256k1Response {
75    pub public_key: PublicKey,
76    pub addr: String,
77}
78
79#[derive(Serialize, Deserialize, Debug)]
80struct Fee {
81    pub amount: Vec<Coin>,
82    pub gas: String,
83}
84
85impl From<cosmrs::tx::Fee> for Fee {
86    fn from(value: cosmrs::tx::Fee) -> Self {
87        Self {
88            amount: value.amount.into_iter().map(|c| c.into()).collect(),
89            gas: value.gas_limit.to_string(),
90        }
91    }
92}
93
94#[derive(Serialize, Deserialize, Debug)]
95struct Coin {
96    pub amount: String,
97    pub denom: String,
98}
99
100impl From<cosmrs::Coin> for Coin {
101    fn from(value: cosmrs::Coin) -> Self {
102        Self {
103            amount: value.amount.to_string(),
104            denom: value.denom.to_string(),
105        }
106    }
107}
108
109#[derive(Serialize, Debug)]
110struct LedgerPayload {
111    pub account_number: String,
112    pub chain_id: String,
113    pub fee: Fee,
114    pub memo: String,
115    pub msgs: Vec<Value>,
116    pub sequence: String,
117}
118
119impl<T> CosmosApp<T>
120where
121    T: Exchange + Send + Sync,
122    T::Error: std::error::Error,
123{
124    pub fn new(transport: T) -> Self {
125        CosmosApp { transport }
126    }
127
128    pub async fn get_cosmos_app_version(
129        &self,
130    ) -> Result<CosmosAppVersion, LedgerCosmosError<T::Error>> {
131        let command = APDUCommand {
132            cla: COSMOS_CLA,
133            ins: GET_VERSION_INS,
134            p1: 0x00,
135            p2: 0x00,
136            data: vec![],
137        };
138        let answer = self.transport.exchange(&command).await?;
139        let error_code = answer.error_code();
140        match error_code {
141            Ok(code) => match code {
142                APDUErrorCode::NoError => {
143                    let data = answer.apdu_data();
144                    Ok(CosmosAppVersion {
145                        test_mode: data[0],
146                        major: data[1],
147                        minor: data[2],
148                        patch: data[3],
149                        locked: data[4],
150                    })
151                }
152                _ => Err(LedgerCosmosError::Apdu(code.description())),
153            },
154            Err(code) => Err(LedgerCosmosError::UnknownApduCode(code)),
155        }
156    }
157
158    pub async fn get_addr_secp256k1(
159        &self,
160        path: [u32; 5],
161        hrp: &str,
162        display_on_ledger: bool,
163    ) -> Result<Secp256k1Response, LedgerCosmosError<T::Error>> {
164        let mut get_addr_payload: Vec<u8> = Vec::new();
165        get_addr_payload.write_u8(hrp.len() as u8).unwrap(); // hrp len
166        get_addr_payload.extend(hrp.as_bytes()); // hrp
167        get_addr_payload
168            .write_u32::<LittleEndian>(path[0] + 0x80000000)
169            .unwrap();
170        get_addr_payload
171            .write_u32::<LittleEndian>(path[1] + 0x80000000)
172            .unwrap();
173        get_addr_payload
174            .write_u32::<LittleEndian>(path[2] + 0x80000000)
175            .unwrap();
176        get_addr_payload.write_u32::<LittleEndian>(path[3]).unwrap();
177        get_addr_payload.write_u32::<LittleEndian>(path[4]).unwrap();
178        let command = APDUCommand {
179            cla: COSMOS_CLA,
180            ins: GET_ADDR_SECP256K1_INS,
181            p1: display_on_ledger as u8,
182            p2: 0x00,
183            data: get_addr_payload,
184        };
185        let answer = self.transport.exchange(&command).await?;
186        let error_code = answer.error_code();
187        match error_code {
188            Ok(code) => match code {
189                APDUErrorCode::NoError => {
190                    let data = answer.apdu_data();
191                    Ok(Secp256k1Response {
192                        public_key: decompress_pk(&data[0..33]).unwrap(),
193                        addr: String::from_utf8(data[33..].to_vec())
194                            .map_err(|_| LedgerCosmosError::InvalidAddress)?,
195                    })
196                }
197                _ => Err(LedgerCosmosError::Apdu(code.description())),
198            },
199            Err(code) => Err(LedgerCosmosError::UnknownApduCode(code)),
200        }
201    }
202
203    pub async fn sign_secp256k1(
204        &self,
205        path: [u32; 5],
206        message: Vec<u8>,
207    ) -> Result<Signature, LedgerCosmosError<T::Error>> {
208        let mut init_payload: Vec<u8> = Vec::new();
209        init_payload
210            .write_u32::<LittleEndian>(path[0] + 0x80000000)
211            .unwrap();
212        init_payload
213            .write_u32::<LittleEndian>(path[1] + 0x80000000)
214            .unwrap();
215        init_payload
216            .write_u32::<LittleEndian>(path[2] + 0x80000000)
217            .unwrap();
218        init_payload.write_u32::<LittleEndian>(path[3]).unwrap();
219        init_payload.write_u32::<LittleEndian>(path[4]).unwrap();
220        let init_command = APDUCommand {
221            cla: COSMOS_CLA,
222            ins: SIGN_SECP256K1_INS,
223            p1: 0x00,
224            p2: 0x00,
225            data: init_payload,
226        };
227        info!("init command: {:#?}", init_command);
228        let res = Self::send_chunks::<Vec<u8>>(&self.transport, init_command, &message).await;
229        let sign_answer = res.unwrap();
230        let sign_error_code = sign_answer.error_code();
231        match sign_error_code {
232            Ok(code) => match code {
233                APDUErrorCode::NoError => {
234                    info!("code: {:?}", code);
235                    let data = sign_answer.apdu_data().to_vec();
236                    info!("data: {:?}", data);
237                    let signature = Signature::from_der(data.as_slice()).unwrap();
238                    dbg!(signature);
239
240                    Ok(signature)
241                }
242                _ => Err(LedgerCosmosError::Apdu(code.description())),
243            },
244            Err(code) => Err(LedgerCosmosError::UnknownApduCode(code)),
245        }
246    }
247
248    pub async fn sign(
249        &self,
250        derivation_path: [u32; 5],
251        fee: cosmrs::tx::Fee,
252        chain_id: String,
253        memo: String,
254        account_number: u64,
255        sequence: u64,
256        msgs: Vec<Value>,
257    ) -> Result<Signature, LedgerCosmosError<T::Error>> {
258        let payload = LedgerPayload {
259            account_number: account_number.to_string(),
260            chain_id,
261            fee: fee.into(),
262            memo,
263            msgs,
264            sequence: sequence.to_string(),
265        };
266
267        let payload_val = serde_json::to_value(&payload).unwrap();
268        let payload_val_sorted = crate::sort_object_keys(payload_val.clone());
269        let bytes = serde_json::to_vec(&payload_val_sorted).unwrap();
270        info!(
271            "payload: {}",
272            serde_json::to_string_pretty(&payload_val_sorted).unwrap()
273        );
274        let res = self.sign_secp256k1(derivation_path, bytes).await?;
275        info!("res: {:?}", res);
276        Ok(res)
277    }
278}
279
280fn decompress_pk(compressed_pk: &[u8]) -> Result<PublicKey, LedgerCosmosError<()>> {
281    Ok(PublicKey::from_raw_secp256k1(compressed_pk).expect("invalid secp256k1 key"))
282}
283
284fn sort_object_keys(value: Value) -> Value {
285    match value {
286        Value::Object(map) => {
287            let mut vec: Vec<(String, Value)> = map
288                .into_iter()
289                .map(|(k, v)| (k, sort_object_keys(v)))
290                .collect();
291            vec.sort_by(|a, b| a.0.cmp(&b.0));
292            Value::Object(vec.into_iter().collect())
293        }
294        Value::Array(mut vec) => {
295            vec.iter_mut()
296                .for_each(|x| *x = sort_object_keys(x.to_owned()));
297            Value::Array(vec)
298        }
299        _ => value,
300    }
301}
302
303#[cfg(test)]
304mod tests {
305
306    use std::str::FromStr;
307
308    use btleplug::platform;
309
310    use cosmrs::{tx::Fee, Coin, Denom};
311    use ledger_bluetooth::TransportNativeBle;
312    use ledger_transport_hid::{hidapi::HidApi, TransportNativeHID};
313
314    use log::info;
315    use serde_json::json;
316    use serial_test::serial;
317
318    use crate::CosmosApp;
319
320    #[tokio::test]
321    #[serial]
322    async fn test_get_addr_secp256k1() {
323        let manager = platform::Manager::new().await.unwrap();
324        let ledger = TransportNativeBle::new(&manager)
325            .await
326            .unwrap()
327            .pop()
328            .unwrap();
329        let app = CosmosApp::new(ledger);
330        let path = [44, 118, 0, 0, 0];
331        let hrp = "cosmos";
332        let display_on_ledger = true;
333        let res = app
334            .get_addr_secp256k1(path, hrp, display_on_ledger)
335            .await
336            .unwrap();
337        info!("public key: {:?}", res.public_key);
338        info!("addr: {:?}", res.addr);
339    }
340
341    #[tokio::test]
342    #[serial]
343    async fn test_sign() {
344        let api = HidApi::new().unwrap();
345        let device = TransportNativeHID::list_ledgers(&api).next().unwrap();
346        let ledger = TransportNativeHID::open_device(&api, device).unwrap();
347
348        let app = CosmosApp::new(ledger);
349        let derivation_path = [44, 118, 0, 0, 0];
350
351        let fee = Fee {
352            amount: vec![Coin {
353                denom: Denom::from_str("uatom").unwrap(),
354                amount: 45,
355            }],
356            gas_limit: 4000,
357            payer: None,
358            granter: None,
359        };
360
361        let account_number = 123;
362        let chain_id = "oasis-1".to_string();
363        let memo = "hello".to_string();
364        let sequence = 500;
365
366        let msg = json!({
367            "hello": "world"
368        });
369
370        // let msg = MsgExecuteContract {
371        //     sender: AccountId::from_str("noria19n42dwl6mgwcep5ytqt7qpthy067ssq72gjsrk").unwrap(),
372        //     contract: AccountId::from_str("noria19n42dwl6mgwcep5ytqt7qpthy067ssq72gjsrk").unwrap(),
373        //     msg: b"hello".to_vec(),
374        //     funds: vec![],
375        // };
376
377        // let any_json = msg.into_any_json();
378        // let value = serde_json::to_value(any_json).unwrap();
379        let value = msg;
380        info!("value: {}", serde_json::to_string(&value).unwrap());
381
382        let res = app
383            .sign(
384                derivation_path,
385                fee,
386                chain_id,
387                memo,
388                account_number,
389                sequence,
390                vec![value],
391            )
392            .await
393            .unwrap();
394        info!("res: {:?}", res);
395    }
396}