From 07d873b0162f6f4e36c2f9f6b0a90f6b8117891f Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 30 Aug 2024 16:18:21 +0200 Subject: [PATCH 01/11] Use proper lifetime parameter --- ledger_device_sdk/src/nbgl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index d19c7a85..7989b636 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -161,9 +161,9 @@ impl ToMessage for StatusType { /// This function should be called from the main function of the application. /// The COMM_REF variable is used by the NBGL API to detect touch events and /// APDU reception. -pub fn init_comm(comm: &mut Comm) { +pub fn init_comm(comm: &'static mut Comm) { unsafe { - COMM_REF = Some(transmute(comm)); + COMM_REF = Some(comm); } } From cfcd9258a4c5ac00d032ab69cfbc167e26977cfe Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 30 Aug 2024 17:51:42 +0200 Subject: [PATCH 02/11] Remove unused fields in some structures --- ledger_device_sdk/src/nbgl.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index 7989b636..651062bd 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -617,8 +617,6 @@ impl InfoButton { /// using the NbglGenericReview struct. pub struct TagValueList { pairs: Vec, - items: Vec, - values: Vec, nb_max_lines_for_value: u8, small_case_for_value: bool, wrapping: bool, @@ -632,8 +630,6 @@ impl TagValueList { wrapping: bool, ) -> TagValueList { let mut c_field_strings: Vec = Vec::with_capacity(pairs.len()); - let mut c_field_names: Vec = Vec::with_capacity(pairs.len()); - let mut c_field_values: Vec = Vec::with_capacity(pairs.len()); for field in pairs { let name = CString::new(field.name).unwrap(); let value = CString::new(field.value).unwrap(); @@ -643,13 +639,9 @@ impl TagValueList { ..Default::default() }; c_field_strings.push(tag_value); - c_field_names.push(name); - c_field_values.push(value); } TagValueList { pairs: c_field_strings, - items: c_field_names, - values: c_field_values, nb_max_lines_for_value, small_case_for_value, wrapping, @@ -705,7 +697,6 @@ impl TagValueConfirm { /// when using the NbglGenericReview struct. pub struct InfosList { info_types_cstrings: Vec, - info_contents_cstrings: Vec, info_types_ptr: Vec<*const c_char>, info_contents_ptr: Vec<*const c_char>, } @@ -726,7 +717,6 @@ impl InfosList { info_contents_cstrings.iter().map(|s| s.as_ptr()).collect(); InfosList { info_types_cstrings: info_types_cstrings, - info_contents_cstrings: info_contents_cstrings, info_types_ptr: info_types_ptr, info_contents_ptr: info_contents_ptr, } From 5a3f79c4bc9a1c79d896b8f8203b8f6790ab8f92 Mon Sep 17 00:00:00 2001 From: GroM Date: Tue, 3 Sep 2024 17:06:11 +0200 Subject: [PATCH 03/11] Use async NBGL use case API in Rust SDK (HomeAndSettings) --- .github/workflows/ci.yml | 3 - ledger_device_sdk/.cargo/config.toml | 4 +- ledger_device_sdk/examples/nbgl_home.rs | 30 +++-- ledger_device_sdk/src/nbgl.rs | 160 +++++++++++------------- 4 files changed, 100 insertions(+), 97 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bcce93f..afba35fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,6 @@ jobs: matrix: target: ["nanos", "nanox", "nanosplus", "stax", "flex"] steps: - - name: Force speculos update - run: | - pip3 install --no-cache-dir speculos --upgrade - name: Clone uses: actions/checkout@v4 - name: Unit tests diff --git a/ledger_device_sdk/.cargo/config.toml b/ledger_device_sdk/.cargo/config.toml index bf0bd24b..58298b1e 100644 --- a/ledger_device_sdk/.cargo/config.toml +++ b/ledger_device_sdk/.cargo/config.toml @@ -8,10 +8,10 @@ runner = "speculos -m nanox --display=headless" runner = "speculos -m nanosp --display=headless" [target.stax] -runner = "speculos --model stax --display=headless" +runner = "speculos --model stax" [target.flex] -runner = "speculos --model flex --display=headless" +runner = "speculos --model flex" [unstable] build-std = ["core", "alloc"] diff --git a/ledger_device_sdk/examples/nbgl_home.rs b/ledger_device_sdk/examples/nbgl_home.rs index 02a96ed1..9a28708d 100644 --- a/ledger_device_sdk/examples/nbgl_home.rs +++ b/ledger_device_sdk/examples/nbgl_home.rs @@ -2,7 +2,7 @@ #![no_main] // Force boot section to be embedded in -use ledger_device_sdk as _; +use ledger_device_sdk::{self as _, testing}; use include_gif::include_gif; use ledger_device_sdk::io::*; @@ -45,23 +45,39 @@ extern "C" fn sample_main() { NVMData::new(AtomicStorage::new(&[0u8; 10])); let mut comm = Comm::new(); - // Initialize reference to Comm instance for NBGL - // API calls. - init_comm(&mut comm); // Load glyph from 64x64 4bpp gif file with include_gif macro. Creates an NBGL compatible glyph. const FERRIS: NbglGlyph = NbglGlyph::from_include(include_gif!("examples/crab_64x64.gif", NBGL)); let settings_strings = [["Switch title", "Switch subtitle"]]; + + testing::debug_print("Starting App\n"); + // Display the home screen. - NbglHomeAndSettings::new() + let mut home = NbglHomeAndSettings::new() .glyph(&FERRIS) .settings(unsafe { DATA.get_mut() }, &settings_strings) .infos( "Example App", env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_AUTHORS"), - ) - .show::(); + ); + + home.show(); + + testing::debug_print("App started\n"); + + loop { + // Wait for an APDU command + match comm.next_command() { + Instruction::GetVersion => { + comm.reply_ok(); + } + Instruction::GetAppName => { + comm.reply_ok(); + } + _ => {} + } + } } diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index 651062bd..a4dd39d2 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -161,9 +161,9 @@ impl ToMessage for StatusType { /// This function should be called from the main function of the application. /// The COMM_REF variable is used by the NBGL API to detect touch events and /// APDU reception. -pub fn init_comm(comm: &'static mut Comm) { +pub fn init_comm(comm: &mut Comm) { unsafe { - COMM_REF = Some(comm); + COMM_REF = Some(transmute(comm)); } } @@ -174,10 +174,7 @@ pub fn init_comm(comm: &'static mut Comm) { pub extern "C" fn io_recv_and_process_event() -> bool { unsafe { if let Some(comm) = COMM_REF.as_mut() { - let apdu_received = comm.next_event_ahead::(); - if apdu_received { - return true; - } + return comm.next_event_ahead::(); } } false @@ -185,6 +182,8 @@ pub extern "C" fn io_recv_and_process_event() -> bool { /// Callback triggered by the NBGL API when a setting switch is toggled. unsafe extern "C" fn settings_callback(token: c_int, _index: u8, _page: c_int) { + crate::testing::debug_print("settings_callback\n"); + let idx = token - FIRST_USER_TOKEN as i32; if idx < 0 || idx >= SETTINGS_SIZE as i32 { panic!("Invalid token."); @@ -215,32 +214,43 @@ const INFO_FIELDS: [*const c_char; 2] = [ "Developer\0".as_ptr() as *const c_char, ]; +unsafe extern "C" fn quit_callback() { + exit_app(0); +} + /// A wrapper around the synchronous NBGL ux_sync_homeAndSettings C API binding. /// Used to display the home screen of the application, with an optional glyph, /// information fields, and settings switches. -pub struct NbglHomeAndSettings<'a> { - glyph: Option<&'a NbglGlyph<'a>>, - // app_name, version, author +pub struct NbglHomeAndSettings { + app_name: CString, info_contents: Vec, + info_contents_ptr: Vec<*const c_char>, setting_contents: Vec<[CString; 2]>, nb_settings: u8, + content: nbgl_content_t, + generic_contents: nbgl_genericContents_t, + info_list: nbgl_contentInfoList_t, + icon: nbgl_icon_details_t, } -impl<'a> NbglHomeAndSettings<'a> { - pub fn new() -> NbglHomeAndSettings<'a> { +impl<'a> NbglHomeAndSettings { + pub fn new() -> NbglHomeAndSettings { NbglHomeAndSettings { - glyph: None, + app_name: CString::new("").unwrap(), info_contents: Vec::default(), + info_contents_ptr: Vec::default(), setting_contents: Vec::default(), nb_settings: 0, + content: nbgl_content_t::default(), + generic_contents: nbgl_genericContents_t::default(), + info_list: nbgl_contentInfoList_t::default(), + icon: nbgl_icon_details_t::default(), } } - pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglHomeAndSettings<'a> { - NbglHomeAndSettings { - glyph: Some(glyph), - ..self - } + pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglHomeAndSettings { + let icon = glyph.into(); + NbglHomeAndSettings { icon: icon, ..self } } pub fn infos( @@ -248,12 +258,13 @@ impl<'a> NbglHomeAndSettings<'a> { app_name: &'a str, version: &'a str, author: &'a str, - ) -> NbglHomeAndSettings<'a> { + ) -> NbglHomeAndSettings { let mut v: Vec = Vec::new(); - v.push(CString::new(app_name).unwrap()); v.push(CString::new(version).unwrap()); v.push(CString::new(author).unwrap()); + NbglHomeAndSettings { + app_name: CString::new(app_name).unwrap(), info_contents: v, ..self } @@ -263,7 +274,7 @@ impl<'a> NbglHomeAndSettings<'a> { self, nvm_data: &'a mut AtomicStorage<[u8; SETTINGS_SIZE]>, settings_strings: &[[&'a str; 2]], - ) -> NbglHomeAndSettings<'a> { + ) -> NbglHomeAndSettings { unsafe { NVM_REF = Some(transmute(nvm_data)); } @@ -284,78 +295,57 @@ impl<'a> NbglHomeAndSettings<'a> { } } - pub fn show>(&mut self) -> Event - where - Reply: From<>::Error>, - { + pub fn show(&mut self) { unsafe { - loop { - let info_contents: Vec<*const c_char> = self - .info_contents - .iter() - .map(|s| s.as_ptr()) - .collect::>(); - - let info_list: nbgl_contentInfoList_t = nbgl_contentInfoList_t { - infoTypes: INFO_FIELDS.as_ptr() as *const *const c_char, - infoContents: info_contents[1..].as_ptr() as *const *const c_char, - nbInfos: INFO_FIELDS.len() as u8, - }; + self.info_contents_ptr = self + .info_contents + .iter() + .map(|s| s.as_ptr()) + .collect::>(); - let icon: nbgl_icon_details_t = match self.glyph { - Some(g) => g.into(), - None => nbgl_icon_details_t::default(), - }; + self.info_list = nbgl_contentInfoList_t { + infoTypes: INFO_FIELDS.as_ptr() as *const *const c_char, + infoContents: self.info_contents_ptr[..].as_ptr() as *const *const c_char, + nbInfos: INFO_FIELDS.len() as u8, + }; - for (i, setting) in self.setting_contents.iter().enumerate() { - SWITCH_ARRAY[i].text = setting[0].as_ptr(); - SWITCH_ARRAY[i].subText = setting[1].as_ptr(); - SWITCH_ARRAY[i].initState = - NVM_REF.as_mut().unwrap().get_ref()[i] as nbgl_state_t; - SWITCH_ARRAY[i].token = (FIRST_USER_TOKEN + i as u32) as u8; - SWITCH_ARRAY[i].tuneId = TuneIndex::TapCasual as u8; - } + for (i, setting) in self.setting_contents.iter().enumerate() { + SWITCH_ARRAY[i].text = setting[0].as_ptr(); + SWITCH_ARRAY[i].subText = setting[1].as_ptr(); + SWITCH_ARRAY[i].initState = NVM_REF.as_mut().unwrap().get_ref()[i] as nbgl_state_t; + SWITCH_ARRAY[i].token = (FIRST_USER_TOKEN + i as u32) as u8; + SWITCH_ARRAY[i].tuneId = TuneIndex::TapCasual as u8; + } - let content: nbgl_content_t = nbgl_content_t { - content: nbgl_content_u { - switchesList: nbgl_pageSwitchesList_s { - switches: &SWITCH_ARRAY as *const nbgl_contentSwitch_t, - nbSwitches: self.nb_settings, - }, + self.content = nbgl_content_t { + content: nbgl_content_u { + switchesList: nbgl_pageSwitchesList_s { + switches: &SWITCH_ARRAY as *const nbgl_contentSwitch_t, + nbSwitches: self.nb_settings, }, - contentActionCallback: Some(settings_callback), - type_: SWITCHES_LIST, - }; + }, + contentActionCallback: Some(settings_callback), + type_: SWITCHES_LIST, + }; - let generic_contents: nbgl_genericContents_t = nbgl_genericContents_t { - callbackCallNeeded: false, - __bindgen_anon_1: nbgl_genericContents_t__bindgen_ty_1 { - contentsList: &content as *const nbgl_content_t, - }, - nbContents: if self.nb_settings > 0 { 1 } else { 0 }, - }; + self.generic_contents = nbgl_genericContents_t { + callbackCallNeeded: false, + __bindgen_anon_1: nbgl_genericContents_t__bindgen_ty_1 { + contentsList: &self.content as *const nbgl_content_t, + }, + nbContents: if self.nb_settings > 0 { 1 } else { 0 }, + }; - match ux_sync_homeAndSettings( - info_contents[0], - &icon as *const nbgl_icon_details_t, - core::ptr::null(), - INIT_HOME_PAGE as u8, - &generic_contents as *const nbgl_genericContents_t, - &info_list as *const nbgl_contentInfoList_t, - core::ptr::null(), - ) { - UX_SYNC_RET_APDU_RECEIVED => { - if let Some(comm) = COMM_REF.as_mut() { - if let Some(value) = comm.check_event() { - return value; - } - } - } - _ => { - panic!("Unexpected return value from ux_sync_homeAndSettings"); - } - } - } + nbgl_useCaseHomeAndSettings( + self.app_name.as_ptr() as *const c_char, + &self.icon as *const nbgl_icon_details_t, + core::ptr::null(), + INIT_HOME_PAGE as u8, + &self.generic_contents as *const nbgl_genericContents_t, + &self.info_list as *const nbgl_contentInfoList_t, + core::ptr::null(), + Some(quit_callback), + ); } } } From 29f1058bb457c7503dc8373b2a2862ef07036736 Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 6 Sep 2024 16:15:45 +0200 Subject: [PATCH 04/11] Fix switch toggle UI issue --- ledger_device_sdk/examples/nbgl_home.rs | 4 ++-- ledger_device_sdk/src/nbgl.rs | 27 ++++++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/ledger_device_sdk/examples/nbgl_home.rs b/ledger_device_sdk/examples/nbgl_home.rs index 9a28708d..9b436879 100644 --- a/ledger_device_sdk/examples/nbgl_home.rs +++ b/ledger_device_sdk/examples/nbgl_home.rs @@ -6,6 +6,7 @@ use ledger_device_sdk::{self as _, testing}; use include_gif::include_gif; use ledger_device_sdk::io::*; +use ledger_device_sdk::nbgl::SETTINGS_SIZE; use ledger_device_sdk::nbgl::{init_comm, NbglGlyph, NbglHomeAndSettings}; use ledger_device_sdk::nvm::*; use ledger_device_sdk::NVMData; @@ -39,10 +40,9 @@ extern "C" fn sample_main() { nbgl_refreshReset(); } - const SETTINGS_SIZE: usize = 10; #[link_section = ".nvm_data"] static mut DATA: NVMData> = - NVMData::new(AtomicStorage::new(&[0u8; 10])); + NVMData::new(AtomicStorage::new(&[0u8; SETTINGS_SIZE])); let mut comm = Comm::new(); diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index a4dd39d2..1af80854 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -13,7 +13,7 @@ use ledger_secure_sdk_sys::*; pub static mut G_ux_params: bolos_ux_params_t = unsafe { const_zero!(bolos_ux_params_t) }; static mut COMM_REF: Option<&mut Comm> = None; -const SETTINGS_SIZE: usize = 10; +pub const SETTINGS_SIZE: usize = 10; static mut NVM_REF: Option<&mut AtomicStorage<[u8; SETTINGS_SIZE]>> = None; static mut SWITCH_ARRAY: [nbgl_contentSwitch_t; SETTINGS_SIZE] = [unsafe { const_zero!(nbgl_contentSwitch_t) }; SETTINGS_SIZE]; @@ -182,19 +182,27 @@ pub extern "C" fn io_recv_and_process_event() -> bool { /// Callback triggered by the NBGL API when a setting switch is toggled. unsafe extern "C" fn settings_callback(token: c_int, _index: u8, _page: c_int) { - crate::testing::debug_print("settings_callback\n"); - let idx = token - FIRST_USER_TOKEN as i32; if idx < 0 || idx >= SETTINGS_SIZE as i32 { panic!("Invalid token."); } + let setting_idx: usize = idx as usize; + + match SWITCH_ARRAY[setting_idx].initState { + OFF_STATE => SWITCH_ARRAY[setting_idx].initState = ON_STATE, + ON_STATE => SWITCH_ARRAY[setting_idx].initState = OFF_STATE, + _ => panic!("Invalid state."), + } + if let Some(data) = NVM_REF.as_mut() { - let setting_idx: usize = idx as usize; let mut switch_values: [u8; SETTINGS_SIZE] = data.get_ref().clone(); - switch_values[setting_idx] = !switch_values[setting_idx]; + if switch_values[setting_idx] == OFF_STATE { + switch_values[setting_idx] = ON_STATE; + } else { + switch_values[setting_idx] = OFF_STATE; + } data.update(&switch_values); - SWITCH_ARRAY[setting_idx].initState = switch_values[setting_idx] as nbgl_state_t; } } @@ -312,7 +320,12 @@ impl<'a> NbglHomeAndSettings { for (i, setting) in self.setting_contents.iter().enumerate() { SWITCH_ARRAY[i].text = setting[0].as_ptr(); SWITCH_ARRAY[i].subText = setting[1].as_ptr(); - SWITCH_ARRAY[i].initState = NVM_REF.as_mut().unwrap().get_ref()[i] as nbgl_state_t; + let state = if let Some(data) = NVM_REF.as_mut() { + data.get_ref()[i] + } else { + OFF_STATE + }; + SWITCH_ARRAY[i].initState = state; SWITCH_ARRAY[i].token = (FIRST_USER_TOKEN + i as u32) as u8; SWITCH_ARRAY[i].tuneId = TuneIndex::TapCasual as u8; } From cab5eacc2e65531f56936db82f35c6dc89ae19c4 Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 6 Sep 2024 17:48:27 +0200 Subject: [PATCH 05/11] Use async NBGL use case API in Rust SDK (AddressReview) --- ledger_device_sdk/examples/nbgl_address.rs | 9 +- ledger_device_sdk/src/nbgl.rs | 104 ++++++++++++++++++--- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/ledger_device_sdk/examples/nbgl_address.rs b/ledger_device_sdk/examples/nbgl_address.rs index cc09c99c..ac839da1 100644 --- a/ledger_device_sdk/examples/nbgl_address.rs +++ b/ledger_device_sdk/examples/nbgl_address.rs @@ -23,21 +23,20 @@ extern "C" fn sample_main() { } let mut comm = Comm::new(); - // Initialize reference to Comm instance for NBGL - // API calls. - init_comm(&mut comm); let addr_hex = "0x1234567890ABCDEF1234567890ABCDEF12345678"; // Load glyph from 64x64 4bpp gif file with include_gif macro. Creates an NBGL compatible glyph. const FERRIS: NbglGlyph = NbglGlyph::from_include(include_gif!("examples/crab_64x64.gif", NBGL)); + // Display the address confirmation screen. - let success = NbglAddressReview::new() + let success = NbglAddressReview::new(&mut comm) .glyph(&FERRIS) .verify_str("Verify Address") .show(addr_hex); - NbglReviewStatus::new() + + NbglReviewStatus::new(&mut comm) .status_type(StatusType::Address) .show(success); } diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index 1af80854..ac7f6a77 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -18,6 +18,9 @@ static mut NVM_REF: Option<&mut AtomicStorage<[u8; SETTINGS_SIZE]>> = None; static mut SWITCH_ARRAY: [nbgl_contentSwitch_t; SETTINGS_SIZE] = [unsafe { const_zero!(nbgl_contentSwitch_t) }; SETTINGS_SIZE]; +static mut g_ret: u8 = 0; +static mut g_ended: bool = false; + pub struct Field<'a> { pub name: &'a str, pub value: &'a str, @@ -1060,18 +1063,48 @@ impl NbglStreamingReview { } } +unsafe extern "C" fn choice_callback(confirm: bool) { + crate::testing::debug_print("choice_callback\n"); + if (confirm) { + crate::testing::debug_print("APPROVED\n"); + g_ret = UX_SYNC_RET_APPROVED; + } else { + g_ret = UX_SYNC_RET_REJECTED; + } + g_ended = true; +} + +enum FakeInstruction { + Foo, +} + +impl TryFrom for FakeInstruction { + type Error = crate::io::StatusWords; + + fn try_from(value: crate::io::ApduHeader) -> Result { + match value.ins { + 255 => Ok(FakeInstruction::Foo), + _ => Err(crate::io::StatusWords::NothingReceived), + } + } +} + /// A wrapper around the synchronous NBGL ux_sync_addressReview C API binding. /// Used to display address confirmation screens. pub struct NbglAddressReview<'a> { glyph: Option<&'a NbglGlyph<'a>>, verify_str: CString, + address: CString, + comm: &'a mut Comm, } impl<'a> NbglAddressReview<'a> { - pub fn new() -> NbglAddressReview<'a> { + pub fn new(comm: &'a mut Comm) -> NbglAddressReview<'a> { NbglAddressReview { verify_str: CString::new("").unwrap(), glyph: None, + address: CString::new("").unwrap(), + comm: comm, } } @@ -1089,6 +1122,25 @@ impl<'a> NbglAddressReview<'a> { } } + fn ux_sync_init(&self) { + unsafe { + g_ret = UX_SYNC_RET_ERROR; + g_ended = false; + } + } + + fn ux_sync_wait(&mut self) -> u8 { + unsafe { + while (!g_ended) { + match self.comm.next_event() { + crate::io::Event::Command(FakeInstruction::Foo) => {} + _ => {} + } + } + return g_ret; + } + } + pub fn show(&mut self, address: &str) -> bool { unsafe { let icon: nbgl_icon_details_t = match self.glyph { @@ -1096,16 +1148,18 @@ impl<'a> NbglAddressReview<'a> { None => nbgl_icon_details_t::default(), }; - let address = CString::new(address).unwrap(); + self.address = CString::new(address).unwrap(); - // Show the address confirmation on the device. - let sync_ret = ux_sync_addressReview( - address.as_ptr(), + self.ux_sync_init(); + nbgl_useCaseAddressReview( + self.address.as_ptr(), core::ptr::null(), &icon as *const nbgl_icon_details_t, self.verify_str.as_ptr(), core::ptr::null(), + Some(choice_callback), ); + let sync_ret = self.ux_sync_wait(); // Return true if the user approved the address, false otherwise. match sync_ret { @@ -1182,24 +1236,50 @@ impl<'a> NbglChoice<'a> { /// A wrapper around the synchronous NBGL ux_sync_reviewStatus C API binding. /// Draws a transient (3s) status page of the chosen type. -pub struct NbglReviewStatus { +pub struct NbglReviewStatus<'a> { status_type: StatusType, + comm: &'a mut Comm, } -impl NbglReviewStatus { - pub fn new() -> NbglReviewStatus { +impl<'a> NbglReviewStatus<'a> { + pub fn new(comm: &'a mut Comm) -> NbglReviewStatus<'a> { NbglReviewStatus { status_type: StatusType::Transaction, + comm: comm, } } - pub fn status_type(self, status_type: StatusType) -> NbglReviewStatus { - NbglReviewStatus { status_type } + fn ux_sync_init(&self) { + unsafe { + g_ret = UX_SYNC_RET_ERROR; + g_ended = false; + } } - pub fn show(&self, success: bool) { + fn ux_sync_wait(&mut self) -> u8 { + unsafe { + while (!g_ended) { + match self.comm.next_event() { + crate::io::Event::Command(FakeInstruction::Foo) => {} + _ => {} + } + } + return g_ret; + } + } + + pub fn status_type(self, status_type: StatusType) -> NbglReviewStatus<'a> { + NbglReviewStatus { + status_type, + ..self + } + } + + pub fn show(&mut self, success: bool) { unsafe { - ux_sync_reviewStatus(self.status_type.to_message(success)); + self.ux_sync_init(); + nbgl_useCaseReviewStatus(self.status_type.to_message(success), Some(quit_callback)); + self.ux_sync_wait(); } } } From 2867f022450d4419c52158f184eeb4dee6a70bae Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 9 Sep 2024 16:05:11 +0200 Subject: [PATCH 06/11] Use async NBGL C API in Rust SDK (Home&Settings, AddressReview and ReviewStatus) --- ledger_device_sdk/examples/nbgl_address.rs | 10 +- ledger_device_sdk/examples/nbgl_home.rs | 34 +- ledger_device_sdk/src/nbgl.rs | 1230 ++--------------- .../src/nbgl/nbgl_address_review.rs | 68 + ledger_device_sdk/src/nbgl/nbgl_choice.rs | 58 + .../src/nbgl/nbgl_generic_review.rs | 425 ++++++ .../src/nbgl/nbgl_home_and_settings.rs | 193 +++ ledger_device_sdk/src/nbgl/nbgl_review.rs | 118 ++ .../src/nbgl/nbgl_review_status.rs | 29 + ledger_device_sdk/src/nbgl/nbgl_spinner.rs | 28 + ledger_device_sdk/src/nbgl/nbgl_status.rs | 27 + .../src/nbgl/nbgl_streaming_review.rs | 128 ++ 12 files changed, 1192 insertions(+), 1156 deletions(-) create mode 100644 ledger_device_sdk/src/nbgl/nbgl_address_review.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_choice.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_generic_review.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_home_and_settings.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_review.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_review_status.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_spinner.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_status.rs create mode 100644 ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs diff --git a/ledger_device_sdk/examples/nbgl_address.rs b/ledger_device_sdk/examples/nbgl_address.rs index ac839da1..197c6e81 100644 --- a/ledger_device_sdk/examples/nbgl_address.rs +++ b/ledger_device_sdk/examples/nbgl_address.rs @@ -23,20 +23,22 @@ extern "C" fn sample_main() { } let mut comm = Comm::new(); + // Initialize reference to Comm instance for NBGL + // API calls. + init_comm(&mut comm); let addr_hex = "0x1234567890ABCDEF1234567890ABCDEF12345678"; // Load glyph from 64x64 4bpp gif file with include_gif macro. Creates an NBGL compatible glyph. const FERRIS: NbglGlyph = NbglGlyph::from_include(include_gif!("examples/crab_64x64.gif", NBGL)); - // Display the address confirmation screen. - let success = NbglAddressReview::new(&mut comm) + let success = NbglAddressReview::new() .glyph(&FERRIS) .verify_str("Verify Address") .show(addr_hex); - - NbglReviewStatus::new(&mut comm) + NbglReviewStatus::new() .status_type(StatusType::Address) .show(success); + exit_app(0); } diff --git a/ledger_device_sdk/examples/nbgl_home.rs b/ledger_device_sdk/examples/nbgl_home.rs index 9b436879..02a96ed1 100644 --- a/ledger_device_sdk/examples/nbgl_home.rs +++ b/ledger_device_sdk/examples/nbgl_home.rs @@ -2,11 +2,10 @@ #![no_main] // Force boot section to be embedded in -use ledger_device_sdk::{self as _, testing}; +use ledger_device_sdk as _; use include_gif::include_gif; use ledger_device_sdk::io::*; -use ledger_device_sdk::nbgl::SETTINGS_SIZE; use ledger_device_sdk::nbgl::{init_comm, NbglGlyph, NbglHomeAndSettings}; use ledger_device_sdk::nvm::*; use ledger_device_sdk::NVMData; @@ -40,44 +39,29 @@ extern "C" fn sample_main() { nbgl_refreshReset(); } + const SETTINGS_SIZE: usize = 10; #[link_section = ".nvm_data"] static mut DATA: NVMData> = - NVMData::new(AtomicStorage::new(&[0u8; SETTINGS_SIZE])); + NVMData::new(AtomicStorage::new(&[0u8; 10])); let mut comm = Comm::new(); + // Initialize reference to Comm instance for NBGL + // API calls. + init_comm(&mut comm); // Load glyph from 64x64 4bpp gif file with include_gif macro. Creates an NBGL compatible glyph. const FERRIS: NbglGlyph = NbglGlyph::from_include(include_gif!("examples/crab_64x64.gif", NBGL)); let settings_strings = [["Switch title", "Switch subtitle"]]; - - testing::debug_print("Starting App\n"); - // Display the home screen. - let mut home = NbglHomeAndSettings::new() + NbglHomeAndSettings::new() .glyph(&FERRIS) .settings(unsafe { DATA.get_mut() }, &settings_strings) .infos( "Example App", env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_AUTHORS"), - ); - - home.show(); - - testing::debug_print("App started\n"); - - loop { - // Wait for an APDU command - match comm.next_command() { - Instruction::GetVersion => { - comm.reply_ok(); - } - Instruction::GetAppName => { - comm.reply_ok(); - } - _ => {} - } - } + ) + .show::(); } diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index ac7f6a77..9e9bbbe5 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -12,14 +12,115 @@ use ledger_secure_sdk_sys::*; #[no_mangle] pub static mut G_ux_params: bolos_ux_params_t = unsafe { const_zero!(bolos_ux_params_t) }; +pub mod nbgl_address_review; +pub mod nbgl_choice; +pub mod nbgl_generic_review; +pub mod nbgl_home_and_settings; +pub mod nbgl_review; +pub mod nbgl_review_status; +pub mod nbgl_spinner; +pub mod nbgl_status; +pub mod nbgl_streaming_review; + +pub use nbgl_address_review::*; +pub use nbgl_choice::*; +pub use nbgl_generic_review::*; +pub use nbgl_home_and_settings::*; +pub use nbgl_review::*; +pub use nbgl_review_status::*; +pub use nbgl_spinner::*; +pub use nbgl_status::*; +pub use nbgl_streaming_review::*; + static mut COMM_REF: Option<&mut Comm> = None; pub const SETTINGS_SIZE: usize = 10; static mut NVM_REF: Option<&mut AtomicStorage<[u8; SETTINGS_SIZE]>> = None; static mut SWITCH_ARRAY: [nbgl_contentSwitch_t; SETTINGS_SIZE] = [unsafe { const_zero!(nbgl_contentSwitch_t) }; SETTINGS_SIZE]; -static mut g_ret: u8 = 0; -static mut g_ended: bool = false; +#[derive(Copy, Clone)] +enum SyncNbgl { + UxSyncRetApproved = 0x00, + UxSyncRetRejected = 0x01, + UxSyncRetQuitted = 0x02, + UxSyncRetApduReceived = 0x03, + UxSyncRetError = 0xFF, +} + +impl From for SyncNbgl { + fn from(val: u8) -> SyncNbgl { + match val { + 0x00 => SyncNbgl::UxSyncRetApproved, + 0x01 => SyncNbgl::UxSyncRetRejected, + 0x02 => SyncNbgl::UxSyncRetQuitted, + 0x03 => SyncNbgl::UxSyncRetApduReceived, + _ => SyncNbgl::UxSyncRetError, + } + } +} + +impl From for u8 { + fn from(val: SyncNbgl) -> u8 { + match val { + SyncNbgl::UxSyncRetApproved => 0x00, + SyncNbgl::UxSyncRetRejected => 0x01, + SyncNbgl::UxSyncRetQuitted => 0x02, + SyncNbgl::UxSyncRetApduReceived => 0x03, + SyncNbgl::UxSyncRetError => 0xFF, + } + } +} + +static mut G_RET: u8 = 0; +static mut G_ENDED: bool = false; + +trait SyncNBGL: Sized { + fn ux_sync_init(&self) { + unsafe { + G_RET = SyncNbgl::UxSyncRetError.into(); + G_ENDED = false; + } + } + + fn ux_sync_wait(&mut self, exit_on_apdu: bool) -> SyncNbgl { + unsafe { + if let Some(comm) = COMM_REF.as_mut() { + while !G_ENDED { + let apdu_received = comm.next_event_ahead::(); + if exit_on_apdu && apdu_received { + return SyncNbgl::UxSyncRetApduReceived; + } + } + return G_RET.into(); + } else { + panic!("COMM_REF not initialized"); + } + } + } +} + +unsafe extern "C" fn action_callback(token: c_int, _index: u8, _page: c_int) { + if token == FIRST_USER_TOKEN as i32 { + ux_sync_setReturnCode(UX_SYNC_RET_APPROVED); + } else if token == (FIRST_USER_TOKEN + 1) as i32 { + ux_sync_setReturnCode(UX_SYNC_RET_REJECTED); + } + ux_sync_setEnded(true); +} + +unsafe extern "C" fn choice_callback(confirm: bool) { + G_RET = if confirm { + SyncNbgl::UxSyncRetApproved.into() + } else { + SyncNbgl::UxSyncRetRejected.into() + }; + G_ENDED = true; +} + +unsafe extern "C" fn quit_callback() { + G_RET = SyncNbgl::UxSyncRetQuitted.into(); + G_ENDED = true; +} pub struct Field<'a> { pub name: &'a str, @@ -183,189 +284,6 @@ pub extern "C" fn io_recv_and_process_event() -> bool { false } -/// Callback triggered by the NBGL API when a setting switch is toggled. -unsafe extern "C" fn settings_callback(token: c_int, _index: u8, _page: c_int) { - let idx = token - FIRST_USER_TOKEN as i32; - if idx < 0 || idx >= SETTINGS_SIZE as i32 { - panic!("Invalid token."); - } - - let setting_idx: usize = idx as usize; - - match SWITCH_ARRAY[setting_idx].initState { - OFF_STATE => SWITCH_ARRAY[setting_idx].initState = ON_STATE, - ON_STATE => SWITCH_ARRAY[setting_idx].initState = OFF_STATE, - _ => panic!("Invalid state."), - } - - if let Some(data) = NVM_REF.as_mut() { - let mut switch_values: [u8; SETTINGS_SIZE] = data.get_ref().clone(); - if switch_values[setting_idx] == OFF_STATE { - switch_values[setting_idx] = ON_STATE; - } else { - switch_values[setting_idx] = OFF_STATE; - } - data.update(&switch_values); - } -} - -unsafe extern "C" fn action_callback(token: c_int, _index: u8, _page: c_int) { - if token == FIRST_USER_TOKEN as i32 { - ux_sync_setReturnCode(UX_SYNC_RET_APPROVED); - } else if token == (FIRST_USER_TOKEN + 1) as i32 { - ux_sync_setReturnCode(UX_SYNC_RET_REJECTED); - } - ux_sync_setEnded(true); -} - -/// Informations fields name to display in the dedicated -/// page of the home screen. -const INFO_FIELDS: [*const c_char; 2] = [ - "Version\0".as_ptr() as *const c_char, - "Developer\0".as_ptr() as *const c_char, -]; - -unsafe extern "C" fn quit_callback() { - exit_app(0); -} - -/// A wrapper around the synchronous NBGL ux_sync_homeAndSettings C API binding. -/// Used to display the home screen of the application, with an optional glyph, -/// information fields, and settings switches. -pub struct NbglHomeAndSettings { - app_name: CString, - info_contents: Vec, - info_contents_ptr: Vec<*const c_char>, - setting_contents: Vec<[CString; 2]>, - nb_settings: u8, - content: nbgl_content_t, - generic_contents: nbgl_genericContents_t, - info_list: nbgl_contentInfoList_t, - icon: nbgl_icon_details_t, -} - -impl<'a> NbglHomeAndSettings { - pub fn new() -> NbglHomeAndSettings { - NbglHomeAndSettings { - app_name: CString::new("").unwrap(), - info_contents: Vec::default(), - info_contents_ptr: Vec::default(), - setting_contents: Vec::default(), - nb_settings: 0, - content: nbgl_content_t::default(), - generic_contents: nbgl_genericContents_t::default(), - info_list: nbgl_contentInfoList_t::default(), - icon: nbgl_icon_details_t::default(), - } - } - - pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglHomeAndSettings { - let icon = glyph.into(); - NbglHomeAndSettings { icon: icon, ..self } - } - - pub fn infos( - self, - app_name: &'a str, - version: &'a str, - author: &'a str, - ) -> NbglHomeAndSettings { - let mut v: Vec = Vec::new(); - v.push(CString::new(version).unwrap()); - v.push(CString::new(author).unwrap()); - - NbglHomeAndSettings { - app_name: CString::new(app_name).unwrap(), - info_contents: v, - ..self - } - } - - pub fn settings( - self, - nvm_data: &'a mut AtomicStorage<[u8; SETTINGS_SIZE]>, - settings_strings: &[[&'a str; 2]], - ) -> NbglHomeAndSettings { - unsafe { - NVM_REF = Some(transmute(nvm_data)); - } - - if settings_strings.len() > SETTINGS_SIZE { - panic!("Too many settings."); - } - - let v: Vec<[CString; 2]> = settings_strings - .iter() - .map(|s| [CString::new(s[0]).unwrap(), CString::new(s[1]).unwrap()]) - .collect(); - - NbglHomeAndSettings { - nb_settings: settings_strings.len() as u8, - setting_contents: v, - ..self - } - } - - pub fn show(&mut self) { - unsafe { - self.info_contents_ptr = self - .info_contents - .iter() - .map(|s| s.as_ptr()) - .collect::>(); - - self.info_list = nbgl_contentInfoList_t { - infoTypes: INFO_FIELDS.as_ptr() as *const *const c_char, - infoContents: self.info_contents_ptr[..].as_ptr() as *const *const c_char, - nbInfos: INFO_FIELDS.len() as u8, - }; - - for (i, setting) in self.setting_contents.iter().enumerate() { - SWITCH_ARRAY[i].text = setting[0].as_ptr(); - SWITCH_ARRAY[i].subText = setting[1].as_ptr(); - let state = if let Some(data) = NVM_REF.as_mut() { - data.get_ref()[i] - } else { - OFF_STATE - }; - SWITCH_ARRAY[i].initState = state; - SWITCH_ARRAY[i].token = (FIRST_USER_TOKEN + i as u32) as u8; - SWITCH_ARRAY[i].tuneId = TuneIndex::TapCasual as u8; - } - - self.content = nbgl_content_t { - content: nbgl_content_u { - switchesList: nbgl_pageSwitchesList_s { - switches: &SWITCH_ARRAY as *const nbgl_contentSwitch_t, - nbSwitches: self.nb_settings, - }, - }, - contentActionCallback: Some(settings_callback), - type_: SWITCHES_LIST, - }; - - self.generic_contents = nbgl_genericContents_t { - callbackCallNeeded: false, - __bindgen_anon_1: nbgl_genericContents_t__bindgen_ty_1 { - contentsList: &self.content as *const nbgl_content_t, - }, - nbContents: if self.nb_settings > 0 { 1 } else { 0 }, - }; - - nbgl_useCaseHomeAndSettings( - self.app_name.as_ptr() as *const c_char, - &self.icon as *const nbgl_icon_details_t, - core::ptr::null(), - INIT_HOME_PAGE as u8, - &self.generic_contents as *const nbgl_genericContents_t, - &self.info_list as *const nbgl_contentInfoList_t, - core::ptr::null(), - Some(quit_callback), - ); - } - } -} - /// Private helper function to display a warning screen when a transaction /// is reviewed in "blind" mode. The user can choose to go back to safety /// or review the risk. If the user chooses to review the risk, a second screen @@ -395,948 +313,6 @@ fn show_blind_warning() -> bool { } } -/// A wrapper around the synchronous NBGL ux_sync_review C API binding. -/// Used to display transaction review screens. -pub struct NbglReview<'a> { - title: CString, - subtitle: CString, - finish_title: CString, - glyph: Option<&'a NbglGlyph<'a>>, - tx_type: TransactionType, - blind: bool, -} - -impl<'a> NbglReview<'a> { - pub fn new() -> NbglReview<'a> { - NbglReview { - title: CString::new("").unwrap(), - subtitle: CString::new("").unwrap(), - finish_title: CString::new("").unwrap(), - glyph: None, - tx_type: TransactionType::Transaction, - blind: false, - } - } - - pub fn tx_type(self, tx_type: TransactionType) -> NbglReview<'a> { - NbglReview { tx_type, ..self } - } - - pub fn blind(self) -> NbglReview<'a> { - NbglReview { - blind: true, - ..self - } - } - - pub fn titles( - self, - title: &'a str, - subtitle: &'a str, - finish_title: &'a str, - ) -> NbglReview<'a> { - NbglReview { - title: CString::new(title).unwrap(), - subtitle: CString::new(subtitle).unwrap(), - finish_title: CString::new(finish_title).unwrap(), - ..self - } - } - - pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglReview<'a> { - NbglReview { - glyph: Some(glyph), - ..self - } - } - - pub fn show(&mut self, fields: &[Field]) -> bool { - unsafe { - let v: Vec = fields - .iter() - .map(|f| CField { - name: CString::new(f.name).unwrap(), - value: CString::new(f.value).unwrap(), - }) - .collect(); - - // Fill the tag_value_array with the fields converted to nbgl_contentTagValue_t - let mut tag_value_array: Vec = Vec::new(); - for field in v.iter() { - let val = nbgl_contentTagValue_t { - item: field.name.as_ptr() as *const i8, - value: field.value.as_ptr() as *const i8, - ..Default::default() - }; - tag_value_array.push(val); - } - - // Create the tag_value_list with the tag_value_array. - let tag_value_list = nbgl_contentTagValueList_t { - pairs: tag_value_array.as_ptr() as *const nbgl_contentTagValue_t, - nbPairs: fields.len() as u8, - ..Default::default() - }; - - let icon: nbgl_icon_details_t = match self.glyph { - Some(g) => g.into(), - None => nbgl_icon_details_t::default(), - }; - - if self.blind { - if !show_blind_warning() { - return false; - } - } - - // Show the review on the device. - let sync_ret = ux_sync_review( - self.tx_type.to_c_type(self.blind, false), - &tag_value_list as *const nbgl_contentTagValueList_t, - &icon as *const nbgl_icon_details_t, - self.title.as_ptr() as *const c_char, - self.subtitle.as_ptr() as *const c_char, - self.finish_title.as_ptr() as *const c_char, - ); - - // Return true if the user approved the transaction, false otherwise. - match sync_ret { - UX_SYNC_RET_APPROVED => { - return true; - } - _ => { - return false; - } - } - } - } -} - -#[derive(Copy, Clone)] -pub enum CenteredInfoStyle { - LargeCaseInfo = 0, - LargeCaseBoldInfo, - NormalInfo, - PluginInfo, -} - -impl From for nbgl_contentCenteredInfoStyle_t { - fn from(style: CenteredInfoStyle) -> nbgl_contentCenteredInfoStyle_t { - match style { - CenteredInfoStyle::LargeCaseInfo => LARGE_CASE_INFO, - CenteredInfoStyle::LargeCaseBoldInfo => LARGE_CASE_BOLD_INFO, - CenteredInfoStyle::NormalInfo => NORMAL_INFO, - CenteredInfoStyle::PluginInfo => PLUGIN_INFO, - } - } -} - -/// Structure exposed by the NBGL Rust API to the user to create a -/// centered info screen that will be displayed on the device -/// when using the NbglGenericReview struct. -pub struct CenteredInfo { - text1: CString, - text2: CString, - text3: CString, - icon: Option, - on_top: bool, - style: CenteredInfoStyle, - offset_y: i16, -} - -impl CenteredInfo { - pub fn new( - text1: &str, - text2: &str, - text3: &str, - icon: Option<&NbglGlyph>, - on_top: bool, - style: CenteredInfoStyle, - offset_y: i16, - ) -> CenteredInfo { - CenteredInfo { - text1: CString::new(text1).unwrap(), - text2: CString::new(text2).unwrap(), - text3: CString::new(text3).unwrap(), - icon: icon.map_or(None, |g| Some(g.into())), - on_top: on_top, - style: style, - offset_y: offset_y, - } - } -} - -/// Structure exposed by the NBGL Rust API to the user to create a -/// "long press" button to confirm some information that will be displayed -/// on the device when using the NbglGenericReview struct. -pub struct InfoLongPress { - text: CString, - icon: Option, - long_press_text: CString, - tune_id: TuneIndex, -} - -impl InfoLongPress { - pub fn new( - text: &str, - icon: Option<&NbglGlyph>, - long_press_text: &str, - tune_id: TuneIndex, - ) -> InfoLongPress { - InfoLongPress { - text: CString::new(text).unwrap(), - icon: icon.map_or(None, |g| Some(g.into())), - long_press_text: CString::new(long_press_text).unwrap(), - tune_id: tune_id, - } - } -} - -/// Structure exposed by the NBGL Rust API to the user to create a -/// button to confirm some information that will be displayed -/// on the device when using the NbglGenericReview struct. -pub struct InfoButton { - text: CString, - icon: Option, - button_text: CString, - tune_id: TuneIndex, -} - -impl InfoButton { - pub fn new( - text: &str, - icon: Option<&NbglGlyph>, - button_text: &str, - tune_id: TuneIndex, - ) -> InfoButton { - InfoButton { - text: CString::new(text).unwrap(), - icon: icon.map_or(None, |g| Some(g.into())), - button_text: CString::new(button_text).unwrap(), - tune_id: tune_id, - } - } -} - -/// Structure exposed by the NBGL Rust API to the user to create a -/// tag/value list screen that will be displayed on the device when -/// using the NbglGenericReview struct. -pub struct TagValueList { - pairs: Vec, - nb_max_lines_for_value: u8, - small_case_for_value: bool, - wrapping: bool, -} - -impl TagValueList { - pub fn new( - pairs: &[Field], - nb_max_lines_for_value: u8, - small_case_for_value: bool, - wrapping: bool, - ) -> TagValueList { - let mut c_field_strings: Vec = Vec::with_capacity(pairs.len()); - for field in pairs { - let name = CString::new(field.name).unwrap(); - let value = CString::new(field.value).unwrap(); - let tag_value = nbgl_contentTagValue_t { - item: name.as_ptr() as *const c_char, - value: value.as_ptr() as *const c_char, - ..Default::default() - }; - c_field_strings.push(tag_value); - } - TagValueList { - pairs: c_field_strings, - nb_max_lines_for_value, - small_case_for_value, - wrapping, - } - } -} - -impl From<&TagValueList> for nbgl_contentTagValueList_t { - fn from(list: &TagValueList) -> nbgl_contentTagValueList_t { - let list = nbgl_contentTagValueList_t { - pairs: list.pairs.as_ptr() as *const nbgl_contentTagValue_t, - nbPairs: list.pairs.len() as u8, - nbMaxLinesForValue: list.nb_max_lines_for_value, - token: FIRST_USER_TOKEN as u8, - smallCaseForValue: list.small_case_for_value, - wrapping: list.wrapping, - ..Default::default() - }; - list - } -} - -/// Structure exposed by the NBGL Rust API to the user to create a -/// list of tag-value pairs and confirmation button that will be displayed -/// on the device when using the NbglGenericReview struct. -pub struct TagValueConfirm { - tag_value_list: nbgl_contentTagValueList_t, - tune_id: TuneIndex, - confirmation_text: CString, - cancel_text: CString, -} - -impl TagValueConfirm { - pub fn new( - tag_value_list: &TagValueList, - tune_id: TuneIndex, - confirmation_text: &str, - cancel_text: &str, - ) -> TagValueConfirm { - let confirmation_text_cstring = CString::new(confirmation_text).unwrap(); - let cancel_text_cstring = CString::new(cancel_text).unwrap(); - TagValueConfirm { - tag_value_list: tag_value_list.into(), - tune_id: tune_id, - confirmation_text: confirmation_text_cstring, - cancel_text: cancel_text_cstring, - } - } -} - -/// Structure exposed by the NBGL Rust API to the user to create a -/// list of information fields that will be displayed on the device -/// when using the NbglGenericReview struct. -pub struct InfosList { - info_types_cstrings: Vec, - info_types_ptr: Vec<*const c_char>, - info_contents_ptr: Vec<*const c_char>, -} - -impl InfosList { - pub fn new(infos: &[Field]) -> InfosList { - let info_types_cstrings: Vec = infos - .iter() - .map(|field| CString::new(field.name).unwrap()) - .collect(); - let info_contents_cstrings: Vec = infos - .iter() - .map(|field| CString::new(field.value).unwrap()) - .collect(); - let info_types_ptr: Vec<*const c_char> = - info_types_cstrings.iter().map(|s| s.as_ptr()).collect(); - let info_contents_ptr: Vec<*const c_char> = - info_contents_cstrings.iter().map(|s| s.as_ptr()).collect(); - InfosList { - info_types_cstrings: info_types_cstrings, - info_types_ptr: info_types_ptr, - info_contents_ptr: info_contents_ptr, - } - } -} - -/// Represents the different types of content that can be displayed -/// on the device when using the NbglGenericReview add_content method. -pub enum NbglPageContent { - CenteredInfo(CenteredInfo), - InfoLongPress(InfoLongPress), - InfoButton(InfoButton), - TagValueList(TagValueList), - TagValueConfirm(TagValueConfirm), - InfosList(InfosList), -} - -impl From<&NbglPageContent> - for ( - nbgl_content_u, - nbgl_contentType_t, - nbgl_contentActionCallback_t, - ) -{ - fn from( - content: &NbglPageContent, - ) -> ( - nbgl_content_u, - nbgl_contentType_t, - nbgl_contentActionCallback_t, - ) { - match content { - NbglPageContent::CenteredInfo(data) => { - let centered_info = nbgl_contentCenteredInfo_t { - text1: data.text1.as_ptr() as *const c_char, - text2: data.text2.as_ptr() as *const c_char, - text3: data.text3.as_ptr() as *const c_char, - icon: data - .icon - .as_ref() - .map_or(core::ptr::null(), |icon| icon as *const nbgl_icon_details_t), - onTop: data.on_top, - style: data.style.into(), - offsetY: data.offset_y, - ..Default::default() - }; - ( - nbgl_content_u { - centeredInfo: centered_info, - }, - CENTERED_INFO, - None, - ) - } - NbglPageContent::TagValueList(data) => { - let tag_list = nbgl_contentTagValueList_t { - pairs: data.pairs.as_ptr() as *const nbgl_contentTagValue_t, - nbPairs: data.pairs.len() as u8, - nbMaxLinesForValue: data.nb_max_lines_for_value, - smallCaseForValue: data.small_case_for_value, - wrapping: data.wrapping, - ..Default::default() - }; - ( - nbgl_content_u { - tagValueList: tag_list, - }, - TAG_VALUE_LIST, - None, - ) - } - NbglPageContent::TagValueConfirm(data) => { - let confirm = nbgl_contentTagValueConfirm_t { - tagValueList: data.tag_value_list, - detailsButtonToken: (FIRST_USER_TOKEN + 2) as u8, - tuneId: data.tune_id as u8, - confirmationText: data.confirmation_text.as_ptr() as *const c_char, - cancelText: data.cancel_text.as_ptr() as *const c_char, - confirmationToken: FIRST_USER_TOKEN as u8, - cancelToken: (FIRST_USER_TOKEN + 1) as u8, - ..Default::default() - }; - ( - nbgl_content_u { - tagValueConfirm: confirm, - }, - TAG_VALUE_CONFIRM, - Some(action_callback), - ) - } - NbglPageContent::InfoLongPress(data) => { - let long_press = nbgl_contentInfoLongPress_t { - text: data.text.as_ptr() as *const c_char, - icon: data - .icon - .as_ref() - .map_or(core::ptr::null(), |icon| icon as *const nbgl_icon_details_t), - longPressText: data.long_press_text.as_ptr() as *const c_char, - longPressToken: FIRST_USER_TOKEN as u8, - tuneId: data.tune_id as u8, - ..Default::default() - }; - ( - nbgl_content_u { - infoLongPress: long_press, - }, - INFO_LONG_PRESS, - Some(action_callback), - ) - } - NbglPageContent::InfoButton(data) => { - let button = nbgl_contentInfoButton_t { - text: data.text.as_ptr() as *const c_char, - icon: data - .icon - .as_ref() - .map_or(core::ptr::null(), |icon| icon as *const nbgl_icon_details_t), - buttonText: data.button_text.as_ptr() as *const c_char, - buttonToken: FIRST_USER_TOKEN as u8, - tuneId: data.tune_id as u8, - ..Default::default() - }; - ( - nbgl_content_u { infoButton: button }, - INFO_BUTTON, - Some(action_callback), - ) - } - NbglPageContent::InfosList(data) => { - let infos_list = nbgl_contentInfoList_t { - infoTypes: data.info_types_ptr.as_ptr() as *const *const c_char, - infoContents: data.info_contents_ptr.as_ptr() as *const *const c_char, - nbInfos: data.info_types_cstrings.len() as u8, - ..Default::default() - }; - ( - nbgl_content_u { - infosList: infos_list, - }, - INFOS_LIST, - None, - ) - } - } - } -} - -/// A wrapper around the synchronous NBGL ux_sync_genericReview C API binding. -/// Used to display custom built review screens. User can add different kind of -/// contents (CenteredInfo, InfoLongPress, InfoButton, TagValueList, TagValueConfirm, InfosList) -/// to the review screen using the add_content method. -pub struct NbglGenericReview { - content_list: Vec, -} - -impl NbglGenericReview { - pub fn new() -> NbglGenericReview { - NbglGenericReview { - content_list: Vec::new(), - } - } - - pub fn add_content(mut self, content: NbglPageContent) -> NbglGenericReview { - self.content_list.push(content); - self - } - - fn to_c_content_list(&self) -> Vec { - self.content_list - .iter() - .map(|content| { - let (c_struct, content_type, action_callback) = content.into(); - nbgl_content_t { - content: c_struct, - contentActionCallback: action_callback, - type_: content_type, - } - }) - .collect() - } - - pub fn show(&mut self, reject_button_str: &str) -> bool { - unsafe { - let c_content_list: Vec = self.to_c_content_list(); - - let content_struct = nbgl_genericContents_t { - callbackCallNeeded: false, - __bindgen_anon_1: nbgl_genericContents_t__bindgen_ty_1 { - contentsList: c_content_list.as_ptr() as *const nbgl_content_t, - }, - nbContents: self.content_list.len() as u8, - }; - - let reject_button_cstring = CString::new(reject_button_str).unwrap(); - - let sync_ret = ux_sync_genericReview( - &content_struct as *const nbgl_genericContents_t, - reject_button_cstring.as_ptr() as *const c_char, - ); - - // Return true if the user approved the transaction, false otherwise. - match sync_ret { - UX_SYNC_RET_APPROVED => { - return true; - } - _ => { - return false; - } - } - } - } -} - -/// A wrapper around the synchronous NBGL ux_sync_reviewStreaming (start, continue and finish) -/// C API binding. Used to display streamed transaction review screens. -pub struct NbglStreamingReview { - icon: nbgl_icon_details_t, - tx_type: TransactionType, - blind: bool, -} - -impl NbglStreamingReview { - pub fn new() -> NbglStreamingReview { - NbglStreamingReview { - icon: nbgl_icon_details_t::default(), - tx_type: TransactionType::Transaction, - blind: false, - } - } - - pub fn tx_type(self, tx_type: TransactionType) -> NbglStreamingReview { - NbglStreamingReview { tx_type, ..self } - } - - pub fn blind(self) -> NbglStreamingReview { - NbglStreamingReview { - blind: true, - ..self - } - } - - pub fn glyph(self, glyph: &NbglGlyph) -> NbglStreamingReview { - NbglStreamingReview { - icon: glyph.into(), - ..self - } - } - - pub fn start(&mut self, title: &str, subtitle: &str) -> bool { - unsafe { - let title = CString::new(title).unwrap(); - let subtitle = CString::new(subtitle).unwrap(); - - if self.blind { - if !show_blind_warning() { - return false; - } - } - - let sync_ret = ux_sync_reviewStreamingStart( - self.tx_type.to_c_type(self.blind, false), - &self.icon as *const nbgl_icon_details_t, - title.as_ptr() as *const c_char, - subtitle.as_ptr() as *const c_char, - ); - - // Return true if the user approved the transaction, false otherwise. - match sync_ret { - UX_SYNC_RET_APPROVED => { - return true; - } - _ => { - return false; - } - } - } - } - - pub fn continue_review(&mut self, fields: &[Field]) -> bool { - unsafe { - let v: Vec = fields - .iter() - .map(|f| CField { - name: CString::new(f.name).unwrap(), - value: CString::new(f.value).unwrap(), - }) - .collect(); - - // Fill the tag_value_array with the fields converted to nbgl_contentTagValue_t - let mut tag_value_array: Vec = Vec::new(); - for field in v.iter() { - let val = nbgl_contentTagValue_t { - item: field.name.as_ptr() as *const i8, - value: field.value.as_ptr() as *const i8, - ..Default::default() - }; - tag_value_array.push(val); - } - - // Create the tag_value_list with the tag_value_array. - let tag_value_list = nbgl_contentTagValueList_t { - pairs: tag_value_array.as_ptr() as *const nbgl_contentTagValue_t, - nbPairs: fields.len() as u8, - ..Default::default() - }; - - let sync_ret = ux_sync_reviewStreamingContinue( - &tag_value_list as *const nbgl_contentTagValueList_t, - ); - - // Return true if the user approved the transaction, false otherwise. - match sync_ret { - UX_SYNC_RET_APPROVED => { - return true; - } - _ => { - return false; - } - } - } - } - - pub fn finish(&mut self, finish_title: &str) -> bool { - unsafe { - let finish_title = CString::new(finish_title).unwrap(); - let sync_ret = ux_sync_reviewStreamingFinish(finish_title.as_ptr() as *const c_char); - - // Return true if the user approved the transaction, false otherwise. - match sync_ret { - UX_SYNC_RET_APPROVED => { - return true; - } - _ => { - return false; - } - } - } - } -} - -unsafe extern "C" fn choice_callback(confirm: bool) { - crate::testing::debug_print("choice_callback\n"); - if (confirm) { - crate::testing::debug_print("APPROVED\n"); - g_ret = UX_SYNC_RET_APPROVED; - } else { - g_ret = UX_SYNC_RET_REJECTED; - } - g_ended = true; -} - -enum FakeInstruction { - Foo, -} - -impl TryFrom for FakeInstruction { - type Error = crate::io::StatusWords; - - fn try_from(value: crate::io::ApduHeader) -> Result { - match value.ins { - 255 => Ok(FakeInstruction::Foo), - _ => Err(crate::io::StatusWords::NothingReceived), - } - } -} - -/// A wrapper around the synchronous NBGL ux_sync_addressReview C API binding. -/// Used to display address confirmation screens. -pub struct NbglAddressReview<'a> { - glyph: Option<&'a NbglGlyph<'a>>, - verify_str: CString, - address: CString, - comm: &'a mut Comm, -} - -impl<'a> NbglAddressReview<'a> { - pub fn new(comm: &'a mut Comm) -> NbglAddressReview<'a> { - NbglAddressReview { - verify_str: CString::new("").unwrap(), - glyph: None, - address: CString::new("").unwrap(), - comm: comm, - } - } - - pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglAddressReview<'a> { - NbglAddressReview { - glyph: Some(glyph), - ..self - } - } - - pub fn verify_str(self, verify_str: &str) -> NbglAddressReview<'a> { - NbglAddressReview { - verify_str: CString::new(verify_str).unwrap(), - ..self - } - } - - fn ux_sync_init(&self) { - unsafe { - g_ret = UX_SYNC_RET_ERROR; - g_ended = false; - } - } - - fn ux_sync_wait(&mut self) -> u8 { - unsafe { - while (!g_ended) { - match self.comm.next_event() { - crate::io::Event::Command(FakeInstruction::Foo) => {} - _ => {} - } - } - return g_ret; - } - } - - pub fn show(&mut self, address: &str) -> bool { - unsafe { - let icon: nbgl_icon_details_t = match self.glyph { - Some(g) => g.into(), - None => nbgl_icon_details_t::default(), - }; - - self.address = CString::new(address).unwrap(); - - self.ux_sync_init(); - nbgl_useCaseAddressReview( - self.address.as_ptr(), - core::ptr::null(), - &icon as *const nbgl_icon_details_t, - self.verify_str.as_ptr(), - core::ptr::null(), - Some(choice_callback), - ); - let sync_ret = self.ux_sync_wait(); - - // Return true if the user approved the address, false otherwise. - match sync_ret { - UX_SYNC_RET_APPROVED => { - return true; - } - UX_SYNC_RET_REJECTED => { - return false; - } - _ => { - panic!("Unexpected return value from ux_sync_addressReview"); - } - } - } - } -} - -/// A wrapper around the synchronous NBGL ux_sync_status C API binding. -/// Draws a generic choice page, described in a centered info (with configurable icon), -/// thanks to a button and a footer at the bottom of the page. -pub struct NbglChoice<'a> { - glyph: Option<&'a NbglGlyph<'a>>, -} - -impl<'a> NbglChoice<'a> { - pub fn new() -> NbglChoice<'a> { - NbglChoice { glyph: None } - } - - pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglChoice<'a> { - NbglChoice { - glyph: Some(glyph), - ..self - } - } - - pub fn show( - self, - message: &str, - sub_message: &str, - confirm_text: &str, - cancel_text: &str, - ) -> bool { - unsafe { - let icon: nbgl_icon_details_t = match self.glyph { - Some(g) => g.into(), - None => nbgl_icon_details_t::default(), - }; - let message = CString::new(message).unwrap(); - let sub_message = CString::new(sub_message).unwrap(); - let confirm_text = CString::new(confirm_text).unwrap(); - let cancel_text = CString::new(cancel_text).unwrap(); - - let sync_ret = ux_sync_choice( - &icon as *const nbgl_icon_details_t, - message.as_ptr() as *const c_char, - sub_message.as_ptr() as *const c_char, - confirm_text.as_ptr() as *const c_char, - cancel_text.as_ptr() as *const c_char, - ); - - // Return true if the user approved the transaction, false otherwise. - match sync_ret { - UX_SYNC_RET_APPROVED => { - return true; - } - _ => { - return false; - } - } - } - } -} - -/// A wrapper around the synchronous NBGL ux_sync_reviewStatus C API binding. -/// Draws a transient (3s) status page of the chosen type. -pub struct NbglReviewStatus<'a> { - status_type: StatusType, - comm: &'a mut Comm, -} - -impl<'a> NbglReviewStatus<'a> { - pub fn new(comm: &'a mut Comm) -> NbglReviewStatus<'a> { - NbglReviewStatus { - status_type: StatusType::Transaction, - comm: comm, - } - } - - fn ux_sync_init(&self) { - unsafe { - g_ret = UX_SYNC_RET_ERROR; - g_ended = false; - } - } - - fn ux_sync_wait(&mut self) -> u8 { - unsafe { - while (!g_ended) { - match self.comm.next_event() { - crate::io::Event::Command(FakeInstruction::Foo) => {} - _ => {} - } - } - return g_ret; - } - } - - pub fn status_type(self, status_type: StatusType) -> NbglReviewStatus<'a> { - NbglReviewStatus { - status_type, - ..self - } - } - - pub fn show(&mut self, success: bool) { - unsafe { - self.ux_sync_init(); - nbgl_useCaseReviewStatus(self.status_type.to_message(success), Some(quit_callback)); - self.ux_sync_wait(); - } - } -} - -/// A wrapper around the synchronous NBGL ux_sync_status C API binding. -/// Draws a transient (3s) status page, either of success or failure, with the given message -pub struct NbglStatus { - text: CString, -} - -impl NbglStatus { - pub fn new() -> NbglStatus { - NbglStatus { - text: CString::new("").unwrap(), - } - } - - pub fn text(self, text: &str) -> NbglStatus { - NbglStatus { - text: CString::new(text).unwrap(), - } - } - - pub fn show(&self, success: bool) { - unsafe { - ux_sync_status(self.text.as_ptr() as *const c_char, success); - } - } -} - -/// A wrapper around the asynchronous NBGL nbgl_useCaseSpinner C API binding. -/// Draws a spinner page with the given parameters. The spinner will "turn" automatically every -/// 800 ms, provided the IO event loop is running to process TickerEvents. -pub struct NbglSpinner { - text: CString, -} - -impl NbglSpinner { - pub fn new() -> NbglSpinner { - NbglSpinner { - text: CString::new("").unwrap(), - } - } - - pub fn text(self, text: &str) -> NbglSpinner { - NbglSpinner { - text: CString::new(text).unwrap(), - } - } - - pub fn show(&self) { - unsafe { - nbgl_useCaseSpinner(self.text.as_ptr() as *const c_char); - } - } -} - #[derive(Copy, Clone)] pub enum TuneIndex { Reserved, diff --git a/ledger_device_sdk/src/nbgl/nbgl_address_review.rs b/ledger_device_sdk/src/nbgl/nbgl_address_review.rs new file mode 100644 index 00000000..dc4338d2 --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_address_review.rs @@ -0,0 +1,68 @@ +use super::*; + +/// A wrapper around the synchronous NBGL ux_sync_addressReview C API binding. +/// Used to display address confirmation screens. +pub struct NbglAddressReview<'a> { + glyph: Option<&'a NbglGlyph<'a>>, + verify_str: CString, +} + +impl SyncNBGL for NbglAddressReview<'_> {} + +impl<'a> NbglAddressReview<'a> { + pub fn new() -> NbglAddressReview<'a> { + NbglAddressReview { + verify_str: CString::new("").unwrap(), + glyph: None, + } + } + + pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglAddressReview<'a> { + NbglAddressReview { + glyph: Some(glyph), + ..self + } + } + + pub fn verify_str(self, verify_str: &str) -> NbglAddressReview<'a> { + NbglAddressReview { + verify_str: CString::new(verify_str).unwrap(), + ..self + } + } + + pub fn show(&mut self, address: &str) -> bool { + unsafe { + let icon: nbgl_icon_details_t = match self.glyph { + Some(g) => g.into(), + None => nbgl_icon_details_t::default(), + }; + + let address = CString::new(address).unwrap(); + + self.ux_sync_init(); + nbgl_useCaseAddressReview( + address.as_ptr(), + core::ptr::null(), + &icon as *const nbgl_icon_details_t, + self.verify_str.as_ptr(), + core::ptr::null(), + Some(choice_callback), + ); + let sync_ret = self.ux_sync_wait(false); + + // Return true if the user approved the address, false otherwise. + match sync_ret { + SyncNbgl::UxSyncRetApproved => { + return true; + } + SyncNbgl::UxSyncRetRejected => { + return false; + } + _ => { + panic!("Unexpected return value from ux_sync_addressReview"); + } + } + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_choice.rs b/ledger_device_sdk/src/nbgl/nbgl_choice.rs new file mode 100644 index 00000000..7b1804a5 --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_choice.rs @@ -0,0 +1,58 @@ +use super::*; + +/// A wrapper around the synchronous NBGL ux_sync_status C API binding. +/// Draws a generic choice page, described in a centered info (with configurable icon), +/// thanks to a button and a footer at the bottom of the page. +pub struct NbglChoice<'a> { + glyph: Option<&'a NbglGlyph<'a>>, +} + +impl<'a> NbglChoice<'a> { + pub fn new() -> NbglChoice<'a> { + NbglChoice { glyph: None } + } + + pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglChoice<'a> { + NbglChoice { + glyph: Some(glyph), + ..self + } + } + + pub fn show( + self, + message: &str, + sub_message: &str, + confirm_text: &str, + cancel_text: &str, + ) -> bool { + unsafe { + let icon: nbgl_icon_details_t = match self.glyph { + Some(g) => g.into(), + None => nbgl_icon_details_t::default(), + }; + let message = CString::new(message).unwrap(); + let sub_message = CString::new(sub_message).unwrap(); + let confirm_text = CString::new(confirm_text).unwrap(); + let cancel_text = CString::new(cancel_text).unwrap(); + + let sync_ret = ux_sync_choice( + &icon as *const nbgl_icon_details_t, + message.as_ptr() as *const c_char, + sub_message.as_ptr() as *const c_char, + confirm_text.as_ptr() as *const c_char, + cancel_text.as_ptr() as *const c_char, + ); + + // Return true if the user approved the transaction, false otherwise. + match sync_ret { + UX_SYNC_RET_APPROVED => { + return true; + } + _ => { + return false; + } + } + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs b/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs new file mode 100644 index 00000000..87525de4 --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs @@ -0,0 +1,425 @@ +use super::*; + +#[derive(Copy, Clone)] +pub enum CenteredInfoStyle { + LargeCaseInfo = 0, + LargeCaseBoldInfo, + NormalInfo, + PluginInfo, +} + +impl From for nbgl_contentCenteredInfoStyle_t { + fn from(style: CenteredInfoStyle) -> nbgl_contentCenteredInfoStyle_t { + match style { + CenteredInfoStyle::LargeCaseInfo => LARGE_CASE_INFO, + CenteredInfoStyle::LargeCaseBoldInfo => LARGE_CASE_BOLD_INFO, + CenteredInfoStyle::NormalInfo => NORMAL_INFO, + CenteredInfoStyle::PluginInfo => PLUGIN_INFO, + } + } +} + +/// Structure exposed by the NBGL Rust API to the user to create a +/// centered info screen that will be displayed on the device +/// when using the NbglGenericReview struct. +pub struct CenteredInfo { + text1: CString, + text2: CString, + text3: CString, + icon: Option, + on_top: bool, + style: CenteredInfoStyle, + offset_y: i16, +} + +impl CenteredInfo { + pub fn new( + text1: &str, + text2: &str, + text3: &str, + icon: Option<&NbglGlyph>, + on_top: bool, + style: CenteredInfoStyle, + offset_y: i16, + ) -> CenteredInfo { + CenteredInfo { + text1: CString::new(text1).unwrap(), + text2: CString::new(text2).unwrap(), + text3: CString::new(text3).unwrap(), + icon: icon.map_or(None, |g| Some(g.into())), + on_top: on_top, + style: style, + offset_y: offset_y, + } + } +} + +/// Structure exposed by the NBGL Rust API to the user to create a +/// "long press" button to confirm some information that will be displayed +/// on the device when using the NbglGenericReview struct. +pub struct InfoLongPress { + text: CString, + icon: Option, + long_press_text: CString, + tune_id: TuneIndex, +} + +impl InfoLongPress { + pub fn new( + text: &str, + icon: Option<&NbglGlyph>, + long_press_text: &str, + tune_id: TuneIndex, + ) -> InfoLongPress { + InfoLongPress { + text: CString::new(text).unwrap(), + icon: icon.map_or(None, |g| Some(g.into())), + long_press_text: CString::new(long_press_text).unwrap(), + tune_id: tune_id, + } + } +} + +/// Structure exposed by the NBGL Rust API to the user to create a +/// button to confirm some information that will be displayed +/// on the device when using the NbglGenericReview struct. +pub struct InfoButton { + text: CString, + icon: Option, + button_text: CString, + tune_id: TuneIndex, +} + +impl InfoButton { + pub fn new( + text: &str, + icon: Option<&NbglGlyph>, + button_text: &str, + tune_id: TuneIndex, + ) -> InfoButton { + InfoButton { + text: CString::new(text).unwrap(), + icon: icon.map_or(None, |g| Some(g.into())), + button_text: CString::new(button_text).unwrap(), + tune_id: tune_id, + } + } +} + +/// Structure exposed by the NBGL Rust API to the user to create a +/// tag/value list screen that will be displayed on the device when +/// using the NbglGenericReview struct. +pub struct TagValueList { + pairs: Vec, + nb_max_lines_for_value: u8, + small_case_for_value: bool, + wrapping: bool, +} + +impl TagValueList { + pub fn new( + pairs: &[Field], + nb_max_lines_for_value: u8, + small_case_for_value: bool, + wrapping: bool, + ) -> TagValueList { + let mut c_field_strings: Vec = Vec::with_capacity(pairs.len()); + for field in pairs { + let name = CString::new(field.name).unwrap(); + let value = CString::new(field.value).unwrap(); + let tag_value = nbgl_contentTagValue_t { + item: name.as_ptr() as *const c_char, + value: value.as_ptr() as *const c_char, + ..Default::default() + }; + c_field_strings.push(tag_value); + } + TagValueList { + pairs: c_field_strings, + nb_max_lines_for_value, + small_case_for_value, + wrapping, + } + } +} + +impl From<&TagValueList> for nbgl_contentTagValueList_t { + fn from(list: &TagValueList) -> nbgl_contentTagValueList_t { + let list = nbgl_contentTagValueList_t { + pairs: list.pairs.as_ptr() as *const nbgl_contentTagValue_t, + nbPairs: list.pairs.len() as u8, + nbMaxLinesForValue: list.nb_max_lines_for_value, + token: FIRST_USER_TOKEN as u8, + smallCaseForValue: list.small_case_for_value, + wrapping: list.wrapping, + ..Default::default() + }; + list + } +} + +/// Structure exposed by the NBGL Rust API to the user to create a +/// list of tag-value pairs and confirmation button that will be displayed +/// on the device when using the NbglGenericReview struct. +pub struct TagValueConfirm { + tag_value_list: nbgl_contentTagValueList_t, + tune_id: TuneIndex, + confirmation_text: CString, + cancel_text: CString, +} + +impl TagValueConfirm { + pub fn new( + tag_value_list: &TagValueList, + tune_id: TuneIndex, + confirmation_text: &str, + cancel_text: &str, + ) -> TagValueConfirm { + let confirmation_text_cstring = CString::new(confirmation_text).unwrap(); + let cancel_text_cstring = CString::new(cancel_text).unwrap(); + TagValueConfirm { + tag_value_list: tag_value_list.into(), + tune_id: tune_id, + confirmation_text: confirmation_text_cstring, + cancel_text: cancel_text_cstring, + } + } +} + +/// Structure exposed by the NBGL Rust API to the user to create a +/// list of information fields that will be displayed on the device +/// when using the NbglGenericReview struct. +pub struct InfosList { + info_types_cstrings: Vec, + info_types_ptr: Vec<*const c_char>, + info_contents_ptr: Vec<*const c_char>, +} + +impl InfosList { + pub fn new(infos: &[Field]) -> InfosList { + let info_types_cstrings: Vec = infos + .iter() + .map(|field| CString::new(field.name).unwrap()) + .collect(); + let info_contents_cstrings: Vec = infos + .iter() + .map(|field| CString::new(field.value).unwrap()) + .collect(); + let info_types_ptr: Vec<*const c_char> = + info_types_cstrings.iter().map(|s| s.as_ptr()).collect(); + let info_contents_ptr: Vec<*const c_char> = + info_contents_cstrings.iter().map(|s| s.as_ptr()).collect(); + InfosList { + info_types_cstrings: info_types_cstrings, + info_types_ptr: info_types_ptr, + info_contents_ptr: info_contents_ptr, + } + } +} + +/// Represents the different types of content that can be displayed +/// on the device when using the NbglGenericReview add_content method. +pub enum NbglPageContent { + CenteredInfo(CenteredInfo), + InfoLongPress(InfoLongPress), + InfoButton(InfoButton), + TagValueList(TagValueList), + TagValueConfirm(TagValueConfirm), + InfosList(InfosList), +} + +impl From<&NbglPageContent> + for ( + nbgl_content_u, + nbgl_contentType_t, + nbgl_contentActionCallback_t, + ) +{ + fn from( + content: &NbglPageContent, + ) -> ( + nbgl_content_u, + nbgl_contentType_t, + nbgl_contentActionCallback_t, + ) { + match content { + NbglPageContent::CenteredInfo(data) => { + let centered_info = nbgl_contentCenteredInfo_t { + text1: data.text1.as_ptr() as *const c_char, + text2: data.text2.as_ptr() as *const c_char, + text3: data.text3.as_ptr() as *const c_char, + icon: data + .icon + .as_ref() + .map_or(core::ptr::null(), |icon| icon as *const nbgl_icon_details_t), + onTop: data.on_top, + style: data.style.into(), + offsetY: data.offset_y, + ..Default::default() + }; + ( + nbgl_content_u { + centeredInfo: centered_info, + }, + CENTERED_INFO, + None, + ) + } + NbglPageContent::TagValueList(data) => { + let tag_list = nbgl_contentTagValueList_t { + pairs: data.pairs.as_ptr() as *const nbgl_contentTagValue_t, + nbPairs: data.pairs.len() as u8, + nbMaxLinesForValue: data.nb_max_lines_for_value, + smallCaseForValue: data.small_case_for_value, + wrapping: data.wrapping, + ..Default::default() + }; + ( + nbgl_content_u { + tagValueList: tag_list, + }, + TAG_VALUE_LIST, + None, + ) + } + NbglPageContent::TagValueConfirm(data) => { + let confirm = nbgl_contentTagValueConfirm_t { + tagValueList: data.tag_value_list, + detailsButtonToken: (FIRST_USER_TOKEN + 2) as u8, + tuneId: data.tune_id as u8, + confirmationText: data.confirmation_text.as_ptr() as *const c_char, + cancelText: data.cancel_text.as_ptr() as *const c_char, + confirmationToken: FIRST_USER_TOKEN as u8, + cancelToken: (FIRST_USER_TOKEN + 1) as u8, + ..Default::default() + }; + ( + nbgl_content_u { + tagValueConfirm: confirm, + }, + TAG_VALUE_CONFIRM, + Some(action_callback), + ) + } + NbglPageContent::InfoLongPress(data) => { + let long_press = nbgl_contentInfoLongPress_t { + text: data.text.as_ptr() as *const c_char, + icon: data + .icon + .as_ref() + .map_or(core::ptr::null(), |icon| icon as *const nbgl_icon_details_t), + longPressText: data.long_press_text.as_ptr() as *const c_char, + longPressToken: FIRST_USER_TOKEN as u8, + tuneId: data.tune_id as u8, + ..Default::default() + }; + ( + nbgl_content_u { + infoLongPress: long_press, + }, + INFO_LONG_PRESS, + Some(action_callback), + ) + } + NbglPageContent::InfoButton(data) => { + let button = nbgl_contentInfoButton_t { + text: data.text.as_ptr() as *const c_char, + icon: data + .icon + .as_ref() + .map_or(core::ptr::null(), |icon| icon as *const nbgl_icon_details_t), + buttonText: data.button_text.as_ptr() as *const c_char, + buttonToken: FIRST_USER_TOKEN as u8, + tuneId: data.tune_id as u8, + ..Default::default() + }; + ( + nbgl_content_u { infoButton: button }, + INFO_BUTTON, + Some(action_callback), + ) + } + NbglPageContent::InfosList(data) => { + let infos_list = nbgl_contentInfoList_t { + infoTypes: data.info_types_ptr.as_ptr() as *const *const c_char, + infoContents: data.info_contents_ptr.as_ptr() as *const *const c_char, + nbInfos: data.info_types_cstrings.len() as u8, + ..Default::default() + }; + ( + nbgl_content_u { + infosList: infos_list, + }, + INFOS_LIST, + None, + ) + } + } + } +} + +/// A wrapper around the synchronous NBGL ux_sync_genericReview C API binding. +/// Used to display custom built review screens. User can add different kind of +/// contents (CenteredInfo, InfoLongPress, InfoButton, TagValueList, TagValueConfirm, InfosList) +/// to the review screen using the add_content method. +pub struct NbglGenericReview { + content_list: Vec, +} + +impl NbglGenericReview { + pub fn new() -> NbglGenericReview { + NbglGenericReview { + content_list: Vec::new(), + } + } + + pub fn add_content(mut self, content: NbglPageContent) -> NbglGenericReview { + self.content_list.push(content); + self + } + + fn to_c_content_list(&self) -> Vec { + self.content_list + .iter() + .map(|content| { + let (c_struct, content_type, action_callback) = content.into(); + nbgl_content_t { + content: c_struct, + contentActionCallback: action_callback, + type_: content_type, + } + }) + .collect() + } + + pub fn show(&mut self, reject_button_str: &str) -> bool { + unsafe { + let c_content_list: Vec = self.to_c_content_list(); + + let content_struct = nbgl_genericContents_t { + callbackCallNeeded: false, + __bindgen_anon_1: nbgl_genericContents_t__bindgen_ty_1 { + contentsList: c_content_list.as_ptr() as *const nbgl_content_t, + }, + nbContents: self.content_list.len() as u8, + }; + + let reject_button_cstring = CString::new(reject_button_str).unwrap(); + + let sync_ret = ux_sync_genericReview( + &content_struct as *const nbgl_genericContents_t, + reject_button_cstring.as_ptr() as *const c_char, + ); + + // Return true if the user approved the transaction, false otherwise. + match sync_ret { + UX_SYNC_RET_APPROVED => { + return true; + } + _ => { + return false; + } + } + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_home_and_settings.rs b/ledger_device_sdk/src/nbgl/nbgl_home_and_settings.rs new file mode 100644 index 00000000..126eabba --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_home_and_settings.rs @@ -0,0 +1,193 @@ +use super::*; + +/// Callback triggered by the NBGL API when a setting switch is toggled. +unsafe extern "C" fn settings_callback(token: c_int, _index: u8, _page: c_int) { + let idx = token - FIRST_USER_TOKEN as i32; + if idx < 0 || idx >= SETTINGS_SIZE as i32 { + panic!("Invalid token."); + } + + let setting_idx: usize = idx as usize; + + match SWITCH_ARRAY[setting_idx].initState { + OFF_STATE => SWITCH_ARRAY[setting_idx].initState = ON_STATE, + ON_STATE => SWITCH_ARRAY[setting_idx].initState = OFF_STATE, + _ => panic!("Invalid state."), + } + + if let Some(data) = NVM_REF.as_mut() { + let mut switch_values: [u8; SETTINGS_SIZE] = data.get_ref().clone(); + if switch_values[setting_idx] == OFF_STATE { + switch_values[setting_idx] = ON_STATE; + } else { + switch_values[setting_idx] = OFF_STATE; + } + data.update(&switch_values); + } +} + +/// Informations fields name to display in the dedicated +/// page of the home screen. +const INFO_FIELDS: [*const c_char; 2] = [ + "Version\0".as_ptr() as *const c_char, + "Developer\0".as_ptr() as *const c_char, +]; + +/// Used to display the home screen of the application, with an optional glyph, +/// information fields, and settings switches. +pub struct NbglHomeAndSettings { + app_name: CString, + info_contents: Vec, + info_contents_ptr: Vec<*const c_char>, + setting_contents: Vec<[CString; 2]>, + nb_settings: u8, + content: nbgl_content_t, + generic_contents: nbgl_genericContents_t, + info_list: nbgl_contentInfoList_t, + icon: nbgl_icon_details_t, +} + +impl SyncNBGL for NbglHomeAndSettings {} + +impl<'a> NbglHomeAndSettings { + pub fn new() -> NbglHomeAndSettings { + NbglHomeAndSettings { + app_name: CString::new("").unwrap(), + info_contents: Vec::default(), + info_contents_ptr: Vec::default(), + setting_contents: Vec::default(), + nb_settings: 0, + content: nbgl_content_t::default(), + generic_contents: nbgl_genericContents_t::default(), + info_list: nbgl_contentInfoList_t::default(), + icon: nbgl_icon_details_t::default(), + } + } + + pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglHomeAndSettings { + let icon = glyph.into(); + NbglHomeAndSettings { icon: icon, ..self } + } + + pub fn infos( + self, + app_name: &'a str, + version: &'a str, + author: &'a str, + ) -> NbglHomeAndSettings { + let mut v: Vec = Vec::new(); + v.push(CString::new(version).unwrap()); + v.push(CString::new(author).unwrap()); + + NbglHomeAndSettings { + app_name: CString::new(app_name).unwrap(), + info_contents: v, + ..self + } + } + + pub fn settings( + self, + nvm_data: &'a mut AtomicStorage<[u8; SETTINGS_SIZE]>, + settings_strings: &[[&'a str; 2]], + ) -> NbglHomeAndSettings { + unsafe { + NVM_REF = Some(transmute(nvm_data)); + } + + if settings_strings.len() > SETTINGS_SIZE { + panic!("Too many settings."); + } + + let v: Vec<[CString; 2]> = settings_strings + .iter() + .map(|s| [CString::new(s[0]).unwrap(), CString::new(s[1]).unwrap()]) + .collect(); + + NbglHomeAndSettings { + nb_settings: settings_strings.len() as u8, + setting_contents: v, + ..self + } + } + + pub fn show>(&mut self) -> Event + where + Reply: From<>::Error>, + { + unsafe { + loop { + self.info_contents_ptr = self + .info_contents + .iter() + .map(|s| s.as_ptr()) + .collect::>(); + + self.info_list = nbgl_contentInfoList_t { + infoTypes: INFO_FIELDS.as_ptr() as *const *const c_char, + infoContents: self.info_contents_ptr[..].as_ptr() as *const *const c_char, + nbInfos: INFO_FIELDS.len() as u8, + }; + + for (i, setting) in self.setting_contents.iter().enumerate() { + SWITCH_ARRAY[i].text = setting[0].as_ptr(); + SWITCH_ARRAY[i].subText = setting[1].as_ptr(); + let state = if let Some(data) = NVM_REF.as_mut() { + data.get_ref()[i] + } else { + OFF_STATE + }; + SWITCH_ARRAY[i].initState = state; + SWITCH_ARRAY[i].token = (FIRST_USER_TOKEN + i as u32) as u8; + SWITCH_ARRAY[i].tuneId = TuneIndex::TapCasual as u8; + } + + self.content = nbgl_content_t { + content: nbgl_content_u { + switchesList: nbgl_pageSwitchesList_s { + switches: &SWITCH_ARRAY as *const nbgl_contentSwitch_t, + nbSwitches: self.nb_settings, + }, + }, + contentActionCallback: Some(settings_callback), + type_: SWITCHES_LIST, + }; + + self.generic_contents = nbgl_genericContents_t { + callbackCallNeeded: false, + __bindgen_anon_1: nbgl_genericContents_t__bindgen_ty_1 { + contentsList: &self.content as *const nbgl_content_t, + }, + nbContents: if self.nb_settings > 0 { 1 } else { 0 }, + }; + + self.ux_sync_init(); + nbgl_useCaseHomeAndSettings( + self.app_name.as_ptr() as *const c_char, + &self.icon as *const nbgl_icon_details_t, + core::ptr::null(), + INIT_HOME_PAGE as u8, + &self.generic_contents as *const nbgl_genericContents_t, + &self.info_list as *const nbgl_contentInfoList_t, + core::ptr::null(), + Some(quit_callback), + ); + match self.ux_sync_wait(true) { + SyncNbgl::UxSyncRetApduReceived => { + if let Some(comm) = COMM_REF.as_mut() { + if let Some(value) = comm.check_event() { + return value; + } + } + } + SyncNbgl::UxSyncRetQuitted => { + exit_app(0); + } + _ => { + panic!("Unexpected return value from ux_sync_homeAndSettings"); + } + } + } + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_review.rs b/ledger_device_sdk/src/nbgl/nbgl_review.rs new file mode 100644 index 00000000..c7b7815e --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_review.rs @@ -0,0 +1,118 @@ +use super::*; + +/// A wrapper around the synchronous NBGL ux_sync_review C API binding. +/// Used to display transaction review screens. +pub struct NbglReview<'a> { + title: CString, + subtitle: CString, + finish_title: CString, + glyph: Option<&'a NbglGlyph<'a>>, + tx_type: TransactionType, + blind: bool, +} + +impl<'a> NbglReview<'a> { + pub fn new() -> NbglReview<'a> { + NbglReview { + title: CString::new("").unwrap(), + subtitle: CString::new("").unwrap(), + finish_title: CString::new("").unwrap(), + glyph: None, + tx_type: TransactionType::Transaction, + blind: false, + } + } + + pub fn tx_type(self, tx_type: TransactionType) -> NbglReview<'a> { + NbglReview { tx_type, ..self } + } + + pub fn blind(self) -> NbglReview<'a> { + NbglReview { + blind: true, + ..self + } + } + + pub fn titles( + self, + title: &'a str, + subtitle: &'a str, + finish_title: &'a str, + ) -> NbglReview<'a> { + NbglReview { + title: CString::new(title).unwrap(), + subtitle: CString::new(subtitle).unwrap(), + finish_title: CString::new(finish_title).unwrap(), + ..self + } + } + + pub fn glyph(self, glyph: &'a NbglGlyph) -> NbglReview<'a> { + NbglReview { + glyph: Some(glyph), + ..self + } + } + + pub fn show(&mut self, fields: &[Field]) -> bool { + unsafe { + let v: Vec = fields + .iter() + .map(|f| CField { + name: CString::new(f.name).unwrap(), + value: CString::new(f.value).unwrap(), + }) + .collect(); + + // Fill the tag_value_array with the fields converted to nbgl_contentTagValue_t + let mut tag_value_array: Vec = Vec::new(); + for field in v.iter() { + let val = nbgl_contentTagValue_t { + item: field.name.as_ptr() as *const i8, + value: field.value.as_ptr() as *const i8, + ..Default::default() + }; + tag_value_array.push(val); + } + + // Create the tag_value_list with the tag_value_array. + let tag_value_list = nbgl_contentTagValueList_t { + pairs: tag_value_array.as_ptr() as *const nbgl_contentTagValue_t, + nbPairs: fields.len() as u8, + ..Default::default() + }; + + let icon: nbgl_icon_details_t = match self.glyph { + Some(g) => g.into(), + None => nbgl_icon_details_t::default(), + }; + + if self.blind { + if !show_blind_warning() { + return false; + } + } + + // Show the review on the device. + let sync_ret = ux_sync_review( + self.tx_type.to_c_type(self.blind, false), + &tag_value_list as *const nbgl_contentTagValueList_t, + &icon as *const nbgl_icon_details_t, + self.title.as_ptr() as *const c_char, + self.subtitle.as_ptr() as *const c_char, + self.finish_title.as_ptr() as *const c_char, + ); + + // Return true if the user approved the transaction, false otherwise. + match sync_ret { + UX_SYNC_RET_APPROVED => { + return true; + } + _ => { + return false; + } + } + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_review_status.rs b/ledger_device_sdk/src/nbgl/nbgl_review_status.rs new file mode 100644 index 00000000..86b4891c --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_review_status.rs @@ -0,0 +1,29 @@ +use super::*; + +/// A wrapper around the synchronous NBGL ux_sync_reviewStatus C API binding. +/// Draws a transient (3s) status page of the chosen type. +pub struct NbglReviewStatus { + status_type: StatusType, +} + +impl SyncNBGL for NbglReviewStatus {} + +impl<'a> NbglReviewStatus { + pub fn new() -> NbglReviewStatus { + NbglReviewStatus { + status_type: StatusType::Transaction, + } + } + + pub fn status_type(self, status_type: StatusType) -> NbglReviewStatus { + NbglReviewStatus { status_type } + } + + pub fn show(&mut self, success: bool) { + unsafe { + self.ux_sync_init(); + nbgl_useCaseReviewStatus(self.status_type.to_message(success), Some(quit_callback)); + self.ux_sync_wait(false); + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_spinner.rs b/ledger_device_sdk/src/nbgl/nbgl_spinner.rs new file mode 100644 index 00000000..83c99368 --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_spinner.rs @@ -0,0 +1,28 @@ +use super::*; + +/// A wrapper around the asynchronous NBGL nbgl_useCaseSpinner C API binding. +/// Draws a spinner page with the given parameters. The spinner will "turn" automatically every +/// 800 ms, provided the IO event loop is running to process TickerEvents. +pub struct NbglSpinner { + text: CString, +} + +impl NbglSpinner { + pub fn new() -> NbglSpinner { + NbglSpinner { + text: CString::new("").unwrap(), + } + } + + pub fn text(self, text: &str) -> NbglSpinner { + NbglSpinner { + text: CString::new(text).unwrap(), + } + } + + pub fn show(&self) { + unsafe { + nbgl_useCaseSpinner(self.text.as_ptr() as *const c_char); + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_status.rs b/ledger_device_sdk/src/nbgl/nbgl_status.rs new file mode 100644 index 00000000..b5a53fca --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_status.rs @@ -0,0 +1,27 @@ +use super::*; + +/// A wrapper around the synchronous NBGL ux_sync_status C API binding. +/// Draws a transient (3s) status page, either of success or failure, with the given message +pub struct NbglStatus { + text: CString, +} + +impl NbglStatus { + pub fn new() -> NbglStatus { + NbglStatus { + text: CString::new("").unwrap(), + } + } + + pub fn text(self, text: &str) -> NbglStatus { + NbglStatus { + text: CString::new(text).unwrap(), + } + } + + pub fn show(&self, success: bool) { + unsafe { + ux_sync_status(self.text.as_ptr() as *const c_char, success); + } + } +} diff --git a/ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs b/ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs new file mode 100644 index 00000000..6307db8e --- /dev/null +++ b/ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs @@ -0,0 +1,128 @@ +use super::*; + +/// A wrapper around the synchronous NBGL ux_sync_reviewStreaming (start, continue and finish) +/// C API binding. Used to display streamed transaction review screens. +pub struct NbglStreamingReview { + icon: nbgl_icon_details_t, + tx_type: TransactionType, + blind: bool, +} + +impl NbglStreamingReview { + pub fn new() -> NbglStreamingReview { + NbglStreamingReview { + icon: nbgl_icon_details_t::default(), + tx_type: TransactionType::Transaction, + blind: false, + } + } + + pub fn tx_type(self, tx_type: TransactionType) -> NbglStreamingReview { + NbglStreamingReview { tx_type, ..self } + } + + pub fn blind(self) -> NbglStreamingReview { + NbglStreamingReview { + blind: true, + ..self + } + } + + pub fn glyph(self, glyph: &NbglGlyph) -> NbglStreamingReview { + NbglStreamingReview { + icon: glyph.into(), + ..self + } + } + + pub fn start(&mut self, title: &str, subtitle: &str) -> bool { + unsafe { + let title = CString::new(title).unwrap(); + let subtitle = CString::new(subtitle).unwrap(); + + if self.blind { + if !show_blind_warning() { + return false; + } + } + + let sync_ret = ux_sync_reviewStreamingStart( + self.tx_type.to_c_type(self.blind, false), + &self.icon as *const nbgl_icon_details_t, + title.as_ptr() as *const c_char, + subtitle.as_ptr() as *const c_char, + ); + + // Return true if the user approved the transaction, false otherwise. + match sync_ret { + UX_SYNC_RET_APPROVED => { + return true; + } + _ => { + return false; + } + } + } + } + + pub fn continue_review(&mut self, fields: &[Field]) -> bool { + unsafe { + let v: Vec = fields + .iter() + .map(|f| CField { + name: CString::new(f.name).unwrap(), + value: CString::new(f.value).unwrap(), + }) + .collect(); + + // Fill the tag_value_array with the fields converted to nbgl_contentTagValue_t + let mut tag_value_array: Vec = Vec::new(); + for field in v.iter() { + let val = nbgl_contentTagValue_t { + item: field.name.as_ptr() as *const i8, + value: field.value.as_ptr() as *const i8, + ..Default::default() + }; + tag_value_array.push(val); + } + + // Create the tag_value_list with the tag_value_array. + let tag_value_list = nbgl_contentTagValueList_t { + pairs: tag_value_array.as_ptr() as *const nbgl_contentTagValue_t, + nbPairs: fields.len() as u8, + ..Default::default() + }; + + let sync_ret = ux_sync_reviewStreamingContinue( + &tag_value_list as *const nbgl_contentTagValueList_t, + ); + + // Return true if the user approved the transaction, false otherwise. + match sync_ret { + UX_SYNC_RET_APPROVED => { + return true; + } + _ => { + return false; + } + } + } + } + + pub fn finish(&mut self, finish_title: &str) -> bool { + unsafe { + let finish_title = CString::new(finish_title).unwrap(); + let sync_ret = ux_sync_reviewStreamingFinish(finish_title.as_ptr() as *const c_char); + + // Return true if the user approved the transaction, false otherwise. + match sync_ret { + UX_SYNC_RET_APPROVED => { + return true; + } + _ => { + return false; + } + } + } + } +} From 3446a42be6155513fbc94fa9fac2b78d6146539d Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 9 Sep 2024 17:01:18 +0200 Subject: [PATCH 07/11] Use async NBGL C API in Rust SDK --- ledger_device_sdk/src/nbgl.rs | 14 ++++------ .../src/nbgl/nbgl_address_review.rs | 2 +- ledger_device_sdk/src/nbgl/nbgl_choice.rs | 13 ++++++--- .../src/nbgl/nbgl_generic_review.rs | 20 +++++++++++-- ledger_device_sdk/src/nbgl/nbgl_review.rs | 11 ++++++-- ledger_device_sdk/src/nbgl/nbgl_status.rs | 14 ++++++++-- .../src/nbgl/nbgl_streaming_review.rs | 28 ++++++++++++++----- 7 files changed, 72 insertions(+), 30 deletions(-) diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index 9e9bbbe5..ca450a18 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -99,15 +99,6 @@ trait SyncNBGL: Sized { } } -unsafe extern "C" fn action_callback(token: c_int, _index: u8, _page: c_int) { - if token == FIRST_USER_TOKEN as i32 { - ux_sync_setReturnCode(UX_SYNC_RET_APPROVED); - } else if token == (FIRST_USER_TOKEN + 1) as i32 { - ux_sync_setReturnCode(UX_SYNC_RET_REJECTED); - } - ux_sync_setEnded(true); -} - unsafe extern "C" fn choice_callback(confirm: bool) { G_RET = if confirm { SyncNbgl::UxSyncRetApproved.into() @@ -122,6 +113,11 @@ unsafe extern "C" fn quit_callback() { G_ENDED = true; } +unsafe extern "C" fn rejected_callback() { + G_RET = SyncNbgl::UxSyncRetRejected.into(); + G_ENDED = true; +} + pub struct Field<'a> { pub name: &'a str, pub value: &'a str, diff --git a/ledger_device_sdk/src/nbgl/nbgl_address_review.rs b/ledger_device_sdk/src/nbgl/nbgl_address_review.rs index dc4338d2..f43df154 100644 --- a/ledger_device_sdk/src/nbgl/nbgl_address_review.rs +++ b/ledger_device_sdk/src/nbgl/nbgl_address_review.rs @@ -1,6 +1,6 @@ use super::*; -/// A wrapper around the synchronous NBGL ux_sync_addressReview C API binding. +/// A wrapper around the asynchronous NBGL nbgl_useCaseAddressReview C API binding. /// Used to display address confirmation screens. pub struct NbglAddressReview<'a> { glyph: Option<&'a NbglGlyph<'a>>, diff --git a/ledger_device_sdk/src/nbgl/nbgl_choice.rs b/ledger_device_sdk/src/nbgl/nbgl_choice.rs index 7b1804a5..f84d6bd4 100644 --- a/ledger_device_sdk/src/nbgl/nbgl_choice.rs +++ b/ledger_device_sdk/src/nbgl/nbgl_choice.rs @@ -1,12 +1,14 @@ use super::*; -/// A wrapper around the synchronous NBGL ux_sync_status C API binding. +/// A wrapper around the asynchronous NBGL nbgl_useCaseChoice C API binding. /// Draws a generic choice page, described in a centered info (with configurable icon), /// thanks to a button and a footer at the bottom of the page. pub struct NbglChoice<'a> { glyph: Option<&'a NbglGlyph<'a>>, } +impl SyncNBGL for NbglChoice<'_> {} + impl<'a> NbglChoice<'a> { pub fn new() -> NbglChoice<'a> { NbglChoice { glyph: None } @@ -20,7 +22,7 @@ impl<'a> NbglChoice<'a> { } pub fn show( - self, + &mut self, message: &str, sub_message: &str, confirm_text: &str, @@ -36,17 +38,20 @@ impl<'a> NbglChoice<'a> { let confirm_text = CString::new(confirm_text).unwrap(); let cancel_text = CString::new(cancel_text).unwrap(); - let sync_ret = ux_sync_choice( + self.ux_sync_init(); + nbgl_useCaseChoice( &icon as *const nbgl_icon_details_t, message.as_ptr() as *const c_char, sub_message.as_ptr() as *const c_char, confirm_text.as_ptr() as *const c_char, cancel_text.as_ptr() as *const c_char, + Some(choice_callback), ); + let sync_ret = self.ux_sync_wait(false); // Return true if the user approved the transaction, false otherwise. match sync_ret { - UX_SYNC_RET_APPROVED => { + SyncNbgl::UxSyncRetApproved => { return true; } _ => { diff --git a/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs b/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs index 87525de4..c3e38c73 100644 --- a/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs +++ b/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs @@ -217,6 +217,15 @@ impl InfosList { } } +unsafe extern "C" fn action_callback(token: c_int, _index: u8, _page: c_int) { + if token == FIRST_USER_TOKEN as i32 { + G_RET = SyncNbgl::UxSyncRetApproved.into(); + } else if token == (FIRST_USER_TOKEN + 1) as i32 { + G_RET = SyncNbgl::UxSyncRetRejected.into(); + } + G_ENDED = true; +} + /// Represents the different types of content that can be displayed /// on the device when using the NbglGenericReview add_content method. pub enum NbglPageContent { @@ -358,7 +367,7 @@ impl From<&NbglPageContent> } } -/// A wrapper around the synchronous NBGL ux_sync_genericReview C API binding. +/// A wrapper around the asynchronous NBGL nbgl_useCaseGenericReview C API binding. /// Used to display custom built review screens. User can add different kind of /// contents (CenteredInfo, InfoLongPress, InfoButton, TagValueList, TagValueConfirm, InfosList) /// to the review screen using the add_content method. @@ -366,6 +375,8 @@ pub struct NbglGenericReview { content_list: Vec, } +impl SyncNBGL for NbglGenericReview {} + impl NbglGenericReview { pub fn new() -> NbglGenericReview { NbglGenericReview { @@ -406,14 +417,17 @@ impl NbglGenericReview { let reject_button_cstring = CString::new(reject_button_str).unwrap(); - let sync_ret = ux_sync_genericReview( + self.ux_sync_init(); + nbgl_useCaseGenericReview( &content_struct as *const nbgl_genericContents_t, reject_button_cstring.as_ptr() as *const c_char, + Some(rejected_callback), ); + let sync_ret = self.ux_sync_wait(false); // Return true if the user approved the transaction, false otherwise. match sync_ret { - UX_SYNC_RET_APPROVED => { + SyncNbgl::UxSyncRetApproved => { return true; } _ => { diff --git a/ledger_device_sdk/src/nbgl/nbgl_review.rs b/ledger_device_sdk/src/nbgl/nbgl_review.rs index c7b7815e..008b5414 100644 --- a/ledger_device_sdk/src/nbgl/nbgl_review.rs +++ b/ledger_device_sdk/src/nbgl/nbgl_review.rs @@ -1,6 +1,6 @@ use super::*; -/// A wrapper around the synchronous NBGL ux_sync_review C API binding. +/// A wrapper around the asynchronous NBGL nbgl_useCaseReview C API binding. /// Used to display transaction review screens. pub struct NbglReview<'a> { title: CString, @@ -11,6 +11,8 @@ pub struct NbglReview<'a> { blind: bool, } +impl SyncNBGL for NbglReview<'_> {} + impl<'a> NbglReview<'a> { pub fn new() -> NbglReview<'a> { NbglReview { @@ -95,18 +97,21 @@ impl<'a> NbglReview<'a> { } // Show the review on the device. - let sync_ret = ux_sync_review( + self.ux_sync_init(); + nbgl_useCaseReview( self.tx_type.to_c_type(self.blind, false), &tag_value_list as *const nbgl_contentTagValueList_t, &icon as *const nbgl_icon_details_t, self.title.as_ptr() as *const c_char, self.subtitle.as_ptr() as *const c_char, self.finish_title.as_ptr() as *const c_char, + Some(choice_callback), ); + let sync_ret = self.ux_sync_wait(false); // Return true if the user approved the transaction, false otherwise. match sync_ret { - UX_SYNC_RET_APPROVED => { + SyncNbgl::UxSyncRetApproved => { return true; } _ => { diff --git a/ledger_device_sdk/src/nbgl/nbgl_status.rs b/ledger_device_sdk/src/nbgl/nbgl_status.rs index b5a53fca..4b2cdbba 100644 --- a/ledger_device_sdk/src/nbgl/nbgl_status.rs +++ b/ledger_device_sdk/src/nbgl/nbgl_status.rs @@ -1,11 +1,13 @@ use super::*; -/// A wrapper around the synchronous NBGL ux_sync_status C API binding. +/// A wrapper around the asynchronous NBGL ux_sync_status C API binding. /// Draws a transient (3s) status page, either of success or failure, with the given message pub struct NbglStatus { text: CString, } +impl SyncNBGL for NbglStatus {} + impl NbglStatus { pub fn new() -> NbglStatus { NbglStatus { @@ -19,9 +21,15 @@ impl NbglStatus { } } - pub fn show(&self, success: bool) { + pub fn show(&mut self, success: bool) { unsafe { - ux_sync_status(self.text.as_ptr() as *const c_char, success); + self.ux_sync_init(); + nbgl_useCaseStatus( + self.text.as_ptr() as *const c_char, + success, + Some(quit_callback), + ); + self.ux_sync_wait(false); } } } diff --git a/ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs b/ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs index 6307db8e..8204ca76 100644 --- a/ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs +++ b/ledger_device_sdk/src/nbgl/nbgl_streaming_review.rs @@ -1,6 +1,6 @@ use super::*; -/// A wrapper around the synchronous NBGL ux_sync_reviewStreaming (start, continue and finish) +/// A wrapper around the asynchronous NBGL nbgl_useCaseReviewStreamingStart/Continue/Finish) /// C API binding. Used to display streamed transaction review screens. pub struct NbglStreamingReview { icon: nbgl_icon_details_t, @@ -8,6 +8,8 @@ pub struct NbglStreamingReview { blind: bool, } +impl SyncNBGL for NbglStreamingReview {} + impl NbglStreamingReview { pub fn new() -> NbglStreamingReview { NbglStreamingReview { @@ -46,16 +48,19 @@ impl NbglStreamingReview { } } - let sync_ret = ux_sync_reviewStreamingStart( + self.ux_sync_init(); + nbgl_useCaseReviewStreamingStart( self.tx_type.to_c_type(self.blind, false), &self.icon as *const nbgl_icon_details_t, title.as_ptr() as *const c_char, subtitle.as_ptr() as *const c_char, + Some(choice_callback), ); + let sync_ret = self.ux_sync_wait(false); // Return true if the user approved the transaction, false otherwise. match sync_ret { - UX_SYNC_RET_APPROVED => { + SyncNbgl::UxSyncRetApproved => { return true; } _ => { @@ -93,13 +98,16 @@ impl NbglStreamingReview { ..Default::default() }; - let sync_ret = ux_sync_reviewStreamingContinue( + self.ux_sync_init(); + nbgl_useCaseReviewStreamingContinue( &tag_value_list as *const nbgl_contentTagValueList_t, + Some(choice_callback), ); + let sync_ret = self.ux_sync_wait(false); // Return true if the user approved the transaction, false otherwise. match sync_ret { - UX_SYNC_RET_APPROVED => { + SyncNbgl::UxSyncRetApproved => { return true; } _ => { @@ -112,11 +120,17 @@ impl NbglStreamingReview { pub fn finish(&mut self, finish_title: &str) -> bool { unsafe { let finish_title = CString::new(finish_title).unwrap(); - let sync_ret = ux_sync_reviewStreamingFinish(finish_title.as_ptr() as *const c_char); + + self.ux_sync_init(); + nbgl_useCaseReviewStreamingFinish( + finish_title.as_ptr() as *const c_char, + Some(choice_callback), + ); + let sync_ret = self.ux_sync_wait(false); // Return true if the user approved the transaction, false otherwise. match sync_ret { - UX_SYNC_RET_APPROVED => { + SyncNbgl::UxSyncRetApproved => { return true; } _ => { From 393c8bcc219d9381ec1f412a33fe4c7c4e440e8d Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 9 Sep 2024 17:57:01 +0200 Subject: [PATCH 08/11] Fix NbglGenericReview --- ledger_device_sdk/src/nbgl/nbgl_generic_review.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs b/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs index c3e38c73..dff48a4a 100644 --- a/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs +++ b/ledger_device_sdk/src/nbgl/nbgl_generic_review.rs @@ -111,6 +111,8 @@ impl InfoButton { /// using the NbglGenericReview struct. pub struct TagValueList { pairs: Vec, + items: Vec, + values: Vec, nb_max_lines_for_value: u8, small_case_for_value: bool, wrapping: bool, @@ -124,6 +126,8 @@ impl TagValueList { wrapping: bool, ) -> TagValueList { let mut c_field_strings: Vec = Vec::with_capacity(pairs.len()); + let mut c_field_names: Vec = Vec::with_capacity(pairs.len()); + let mut c_field_values: Vec = Vec::with_capacity(pairs.len()); for field in pairs { let name = CString::new(field.name).unwrap(); let value = CString::new(field.value).unwrap(); @@ -133,9 +137,13 @@ impl TagValueList { ..Default::default() }; c_field_strings.push(tag_value); + c_field_names.push(name); + c_field_values.push(value); } TagValueList { pairs: c_field_strings, + items: c_field_names, + values: c_field_values, nb_max_lines_for_value, small_case_for_value, wrapping, @@ -191,6 +199,7 @@ impl TagValueConfirm { /// when using the NbglGenericReview struct. pub struct InfosList { info_types_cstrings: Vec, + info_contents_cstrings: Vec, info_types_ptr: Vec<*const c_char>, info_contents_ptr: Vec<*const c_char>, } @@ -211,6 +220,7 @@ impl InfosList { info_contents_cstrings.iter().map(|s| s.as_ptr()).collect(); InfosList { info_types_cstrings: info_types_cstrings, + info_contents_cstrings: info_contents_cstrings, info_types_ptr: info_types_ptr, info_contents_ptr: info_contents_ptr, } From fe9ca18d8df052ec97b8cd3092a0a419c3f5255b Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 9 Sep 2024 18:08:24 +0200 Subject: [PATCH 09/11] Remove unused C called function --- ledger_device_sdk/src/nbgl.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/ledger_device_sdk/src/nbgl.rs b/ledger_device_sdk/src/nbgl.rs index ca450a18..4b030d2f 100644 --- a/ledger_device_sdk/src/nbgl.rs +++ b/ledger_device_sdk/src/nbgl.rs @@ -10,7 +10,7 @@ use include_gif::include_gif; use ledger_secure_sdk_sys::*; #[no_mangle] -pub static mut G_ux_params: bolos_ux_params_t = unsafe { const_zero!(bolos_ux_params_t) }; +static mut G_ux_params: bolos_ux_params_t = unsafe { const_zero!(bolos_ux_params_t) }; pub mod nbgl_address_review; pub mod nbgl_choice; @@ -267,19 +267,6 @@ pub fn init_comm(comm: &mut Comm) { } } -/// IO function used in the synchronous NBGL C library to process -/// events (touch, buttons, etc.) or detect if an APDU was received. -/// It returns true if an APDU was received, false otherwise. -#[no_mangle] -pub extern "C" fn io_recv_and_process_event() -> bool { - unsafe { - if let Some(comm) = COMM_REF.as_mut() { - return comm.next_event_ahead::(); - } - } - false -} - /// Private helper function to display a warning screen when a transaction /// is reviewed in "blind" mode. The user can choose to go back to safety /// or review the risk. If the user chooses to review the risk, a second screen From 8dd024e39f3ebe05f4d36b10a720859d8b177bf4 Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 9 Sep 2024 18:14:54 +0200 Subject: [PATCH 10/11] revert config.toml --- ledger_device_sdk/.cargo/config.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ledger_device_sdk/.cargo/config.toml b/ledger_device_sdk/.cargo/config.toml index 58298b1e..bf0bd24b 100644 --- a/ledger_device_sdk/.cargo/config.toml +++ b/ledger_device_sdk/.cargo/config.toml @@ -8,10 +8,10 @@ runner = "speculos -m nanox --display=headless" runner = "speculos -m nanosp --display=headless" [target.stax] -runner = "speculos --model stax" +runner = "speculos --model stax --display=headless" [target.flex] -runner = "speculos --model flex" +runner = "speculos --model flex --display=headless" [unstable] build-std = ["core", "alloc"] From ed85a9b4bf911158a33e1d9ccf371b563c8d1444 Mon Sep 17 00:00:00 2001 From: GroM Date: Wed, 11 Sep 2024 15:52:08 +0200 Subject: [PATCH 11/11] Bump SDK version --- Cargo.lock | 2 +- ledger_device_sdk/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05f60397..19ee5e27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,7 +474,7 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "ledger_device_sdk" -version = "1.15.3" +version = "1.15.4" dependencies = [ "const-zero", "include_gif", diff --git a/ledger_device_sdk/Cargo.toml b/ledger_device_sdk/Cargo.toml index 670b57d3..cc4f3f59 100644 --- a/ledger_device_sdk/Cargo.toml +++ b/ledger_device_sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ledger_device_sdk" -version = "1.15.3" +version = "1.15.4" authors = ["yhql", "yogh333", "agrojean-ledger", "kingofpayne"] edition = "2021" license.workspace = true