diff --git a/.github/workflows/swap_ci_workflow.yml b/.github/workflows/swap_ci_workflow.yml new file mode 100644 index 0000000..3384162 --- /dev/null +++ b/.github/workflows/swap_ci_workflow.yml @@ -0,0 +1,18 @@ +name: Swap functional tests + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + job_functional_tests: + uses: LedgerHQ/app-exchange/.github/workflows/reusable_swap_functional_tests.yml@swap-near + with: + branch_for_near: ${{ github.ref }} + test_filter: '"NEAR or near or Near"' + branch_for_exchange: 'swap-near' \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 49bc6b3..d8c2d12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -352,9 +352,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "ledger_device_sdk" -version = "1.17.5" +version = "1.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626eae22265c909eaa40d67db1817d7a79e980990da2ce6fbd5259f4cd685147" +checksum = "b70a36808a9688592bc11f852fbd466c371ebe81c1e53c3e6f87404b02c3f202" dependencies = [ "const-zero", "include_gif", @@ -367,9 +367,9 @@ dependencies = [ [[package]] name = "ledger_secure_sdk_sys" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae67d1f69230aa9798ddf94b8fc0b6a21f3ea7206db813b9c12aa2f783fd396" +checksum = "b046c4ef3859a4e693bd6a49b2e9ee3295d7bf5c3ae7b6bb5cd8b358b1a6cb61" dependencies = [ "bindgen", "cc", @@ -446,7 +446,7 @@ dependencies = [ [[package]] name = "near" -version = "2.2.1" +version = "2.3.4" dependencies = [ "borsh", "bs58", diff --git a/Cargo.toml b/Cargo.toml index 80e7f81..cc9551a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "near" -version = "2.2.1" +version = "2.3.4" authors = ["dj8yf0μl", "polyprogrammist"] edition = "2021" [dependencies] -ledger_device_sdk = "1.17.5" +ledger_device_sdk = "1.19.4" include_gif = "1.2.0" hex = { version = "0.4.3", default-features = false, features = ["serde"] } bs58 = { version = "0.5.0", default-features = false } @@ -22,13 +22,10 @@ lto = true [package.metadata.ledger] curve = ["ed25519"] -flags = "0" +flags = "0x800" path = ["44'/397'"] name = "NEAR" -[package.metadata.ledger.nanos] -icon = "icons/app_near_16px.gif" - [package.metadata.ledger.nanox] icon = "icons/app_near_14px.gif" @@ -43,4 +40,8 @@ icon = "icons/app_near_40px.gif" [features] default = [] +debug = ["ledger_device_sdk/debug"] speculos = ["ledger_device_sdk/speculos"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("stax", "flex", "nanos", "nanox", "nanosplus"))'] } \ No newline at end of file diff --git a/src/app_ui/fields_writer.rs b/src/app_ui/fields_writer.rs index b209320..5f0ef12 100644 --- a/src/app_ui/fields_writer.rs +++ b/src/app_ui/fields_writer.rs @@ -15,9 +15,6 @@ pub struct FieldsWriter<'a, const N: usize> { #[cfg(feature = "speculos")] use ledger_device_sdk::testing; -#[derive(Debug)] -pub struct FieldsOverflow; - impl<'a, const N: usize> FieldsWriter<'a, N> { pub fn new() -> Self { let max_fields = [(); N].map(|_| Field { diff --git a/src/handlers/sign_tx.rs b/src/handlers/sign_tx.rs index e4670f4..3590412 100644 --- a/src/handlers/sign_tx.rs +++ b/src/handlers/sign_tx.rs @@ -77,3 +77,77 @@ pub fn handler(mut stream: SingleTxStream<'_>) -> Result { finalize_sign::end(stream, &path) } + +use ledger_device_sdk::libcall::swap::CreateTxParams; + +/// Sign handler for the swap transaction +/// This handler is called when the user wants to sign a swap transaction +/// The swap transaction is a transfer transaction with a specific amount and destination address +/// The handler checks the transaction parameters and signs the transaction +pub fn swap_handler( + mut stream: SingleTxStream<'_>, + tx_params: &CreateTxParams, +) -> Result { + ledger_device_sdk::testing::debug_print("sign_tx.rs: swap_handler()\n"); + + let path = ::deserialize_reader(&mut stream) + .map_err(|_| AppSW::Bip32PathParsingFail)?; + + ledger_device_sdk::testing::debug_print("sign_tx.rs: path computed\n"); + + // Get the public key from the transaction + let mut stream = HashingStream::new(stream)?; + let mut tx_prefix = parsing::types::transaction::prefix::Prefix::new(); + tx_prefix + .deserialize_reader_in_place(&mut stream) + .map_err(|_err| AppSW::TxParsingFail)?; + let tx_public_key = match PublicKeyBe::try_from(tx_prefix.public_key) { + Ok(tx_public_key) => tx_public_key, + Err(_) => return Err(AppSW::PublicKeyMismatch), + }; + + // Derive the public key from the path and compare it with the transaction public key + let dpath_public_key = { + let pk = ledger_device_sdk::ecc::Ed25519::derive_from_path_slip10(&path.0) + .public_key() + .map_err(|_| AppSW::KeyDeriveFail)?; + PublicKeyBe::from_little_endian(pk) + }; + + if tx_public_key != dpath_public_key { + return Err(AppSW::PublicKeyMismatch); + } + + // Check nb of actions (shall be == 1 == Transfer in swap context) + if tx_prefix.number_of_actions != 1 { + return Err(AppSW::TxSignFail); + } + let action = crate::parsing::types::Action::deserialize_reader(&mut stream) + .map_err(|_err| AppSW::TxParsingFail)?; + if action != crate::parsing::types::Action::Transfer { + return Err(AppSW::TxSignFail); + } + + // Check the tx parameters match with the ones previously validated in Exchange app (tx_params) + let transfer = crate::parsing::types::Transfer::deserialize_reader(&mut stream) + .map_err(|_err| AppSW::TxParsingFail)?; + + let amount_match = near_token::NearToken::from_yoctonear(u128::from_be_bytes(tx_params.amount)) + == transfer.deposit; + if !amount_match { + ledger_device_sdk::testing::debug_print("sign_tx.rs: amounts do not not match\n"); + return Err(AppSW::TxSignFail); + } + + let dest_address_match = tx_prefix.receiver_id.as_str() + == core::str::from_utf8(tx_params.dest_address[..tx_params.dest_address_len].as_ref()) + .unwrap(); + if !dest_address_match { + ledger_device_sdk::testing::debug_print( + "sign_tx.rs: receiver_id does not match with dest_address\n", + ); + return Err(AppSW::TxSignFail); + } + + finalize_sign::end(stream, &path) +} diff --git a/src/main.rs b/src/main.rs index 1742706..062002e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,20 +238,28 @@ impl TryFrom for Instruction { #[cfg(any(target_os = "stax", target_os = "flex"))] use ledger_device_sdk::nbgl::init_comm; +mod swap; + #[no_mangle] -extern "C" fn sample_main() { - let mut comm = Comm::new(); +extern "C" fn sample_main(arg0: u32) { + if arg0 != 0 { + swap::swap_main(arg0); + } else { + ledger_device_sdk::testing::debug_print("call app-near as a standalone\n"); + + let mut comm = Comm::new(); - #[cfg(any(target_os = "stax", target_os = "flex"))] - init_comm(&mut comm); + #[cfg(any(target_os = "stax", target_os = "flex"))] + init_comm(&mut comm); - loop { - // Wait for either a specific button push to exit the app - // or an APDU command - if let Event::Command(ins) = ui_menu_main(&mut comm) { - match handle_apdu(&mut comm, ins) { - Ok(()) => comm.reply_ok(), - Err(sw) => comm.reply(sw), + loop { + // Wait for either a specific button push to exit the app + // or an APDU command + if let Event::Command(ins) = ui_menu_main(&mut comm) { + match handle_apdu(&mut comm, ins) { + Ok(()) => comm.reply_ok(), + Err(sw) => comm.reply(sw), + } } } } diff --git a/src/parsing/transaction_stream_reader/mod.rs b/src/parsing/transaction_stream_reader/mod.rs index 94f3a27..64b7d0e 100644 --- a/src/parsing/transaction_stream_reader/mod.rs +++ b/src/parsing/transaction_stream_reader/mod.rs @@ -76,7 +76,7 @@ impl io::Read for HashingStream { } } -impl<'a> SingleTxStream<'a> { +impl SingleTxStream<'_> { pub fn peek_u8(&mut self) -> io::Result> { let data = self .comm @@ -132,7 +132,7 @@ impl<'a> SingleTxStream<'a> { } } -impl<'a> io::Read for SingleTxStream<'a> { +impl io::Read for SingleTxStream<'_> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let data = self .comm diff --git a/src/parsing/types/common/action/mod.rs b/src/parsing/types/common/action/mod.rs index 3e49b85..3c24bda 100644 --- a/src/parsing/types/common/action/mod.rs +++ b/src/parsing/types/common/action/mod.rs @@ -16,6 +16,7 @@ pub mod function_call; pub mod stake; pub mod transfer; +#[derive(PartialEq)] pub enum Action { CreateAccount, DeployContract, diff --git a/src/swap.rs b/src/swap.rs new file mode 100644 index 0000000..4bc79dc --- /dev/null +++ b/src/swap.rs @@ -0,0 +1,142 @@ +use crate::utils::crypto::{PathBip32, PublicKeyBe}; +use fmt_buffer::Buffer; +use near_gas::{GasBuffer, NearGas}; +use near_token::{NearToken, TokenBuffer}; + +use ledger_device_sdk::{ + ecc, + io::Comm, + libcall::{ + self, + swap::{self, CheckAddressParams, CreateTxParams, PrintableAmountParams}, + }, + testing::debug_print, +}; + +use crate::parsing::transaction_stream_reader::SingleTxStream; + +pub fn swap_main(arg0: u32) { + debug_print("call app for swap \n"); + + let cmd = libcall::get_command(arg0); + + match cmd { + libcall::LibCallCommand::SwapCheckAddress => { + let mut params = swap::get_check_address_params(arg0); + let res = match check_address(¶ms) { + Ok(_) => 1, + Err(err) => { + debug_print(err); + 0 + } + }; + swap::swap_return(swap::SwapResult::CheckAddressResult(&mut params, res)); + } + libcall::LibCallCommand::SwapGetPrintableAmount => { + let mut params = swap::get_printable_amount_params(arg0); + let mut s = get_printable_amount(¶ms); + swap::swap_return(swap::SwapResult::PrintableAmountResult( + &mut params, + s.as_str(), + )); + } + libcall::LibCallCommand::SwapSignTransaction => { + let mut params = swap::sign_tx_params(arg0); + + { + let mut comm = Comm::new().set_expected_cla(super::CLA); + + debug_print("Wait for APDU\n"); + + loop { + // Wait for an APDU command + let ins: super::Instruction = comm.next_command(); + + debug_print("APDU received\n"); + + swap_handle_apdu(&mut comm, ins, &mut params); + } + } + } + } +} + +fn swap_handle_apdu(comm: &mut Comm, ins: super::Instruction, tx_params: &mut CreateTxParams) { + match ins { + super::Instruction::SignTx { + is_last_chunk, + sign_mode, + } => { + debug_print("handle_swap_apdu => Sign Tx\n"); + let stream = SingleTxStream::new(comm, is_last_chunk, sign_mode); + match sign_mode { + super::SignMode::Transaction => { + let signature = crate::handlers::sign_tx::swap_handler(stream, tx_params); + match signature { + Ok(sig) => { + comm.append(&sig.0); + comm.swap_reply_ok(); + swap::swap_return(swap::SwapResult::CreateTxResult(tx_params, 1)); + } + Err(sw) => { + comm.swap_reply(sw); + swap::swap_return(swap::SwapResult::CreateTxResult(tx_params, 0)); + } + } + } + _ => { + comm.swap_reply(crate::AppSW::TxSignFail); + swap::swap_return(swap::SwapResult::CreateTxResult(tx_params, 0)); + } + } + } + super::Instruction::GetPubkey { display } => match display { + true => comm.swap_reply(crate::AppSW::InsNotSupported), + false => match crate::handlers::get_public_key::handler(comm, display) { + Ok(()) => comm.swap_reply_ok(), + Err(sw) => comm.swap_reply(sw), + }, + }, + _ => comm.swap_reply(crate::AppSW::InsNotSupported), + } +} + +fn check_address(params: &CheckAddressParams) -> Result<(), &'static str> { + let path = PathBip32::parse(¶ms.dpath[..params.dpath_len * 4]) + .map_err(|_| "Derivation path failure")?; + + let pk = ecc::Ed25519::derive_from_path_slip10(&path.0) + .public_key() + .map_err(|_| "Public key derivation failure")?; + + let pk = PublicKeyBe::from_little_endian(pk); + let mut buf = [0u8; 64]; + let address = pk.display_str_hex(&mut buf); + + let ref_address = core::str::from_utf8(¶ms.ref_address[..params.ref_address_len]) + .map_err(|_| "Invalid UTF-8 in reference address")?; + + if address == ref_address { + Ok(()) + } else { + Err("Address mismatch") + } +} + +fn get_printable_amount(params: &PrintableAmountParams) -> Buffer<30> { + match params.is_fee { + true => { + let gas = NearGas::from_gas(450_000_000_000); + let mut near_gas_buffer = GasBuffer::new(); + gas.display_as_buffer(&mut near_gas_buffer); + near_gas_buffer + } + false => { + let amount = u128::from_be_bytes(params.amount); + let near_token = NearToken::from_yoctonear(amount); + let mut near_token_buffer = TokenBuffer::new(); + near_token.display_as_buffer(&mut near_token_buffer); + near_token_buffer + } + } +} diff --git a/tests/test_version_cmd.py b/tests/test_version_cmd.py index 63383e4..f70cddd 100644 --- a/tests/test_version_cmd.py +++ b/tests/test_version_cmd.py @@ -1,4 +1,5 @@ from application_client.client import Nearbackend +import toml # In this test we check that the get_version replies the right application version @@ -8,5 +9,9 @@ def test_get_version_cmd(backend): # Send the get_version instruction to the app version = client.get_version().data assert len(version) == 3 - # Assert that we have received the correct app version compared as Makefile data - assert (version[0], version[1], version[2]) == (2, 2, 1) + # Read version from Cargo.toml + with open('Cargo.toml', 'r') as f: + config = toml.load(f) + v = config['package']['version'] + major, minor, patch = v.split('.') + assert (version[0], version[1], version[2]) == (int(major), int(minor), int(patch)) \ No newline at end of file