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
15const COSMOS_CLA: u8 = 0x55;
17
18const GET_VERSION_INS: u8 = 0x00;
20
21const GET_ADDR_SECP256K1_INS: u8 = 0x04;
23
24const 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(); get_addr_payload.extend(hrp.as_bytes()); 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 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}