diff --git a/.cargo/config.toml b/.cargo/config.toml index cd2f521..010ec9d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,9 +1,3 @@ -[target.apex_p] -runner = "speculos -m apex_p" - -[build] -target = "apex_p" - [profile.release] opt-level = 'z' lto = true diff --git a/.github/workflows/extra_checks.yml b/.github/workflows/extra_checks.yml new file mode 100644 index 0000000..b90d19f --- /dev/null +++ b/.github/workflows/extra_checks.yml @@ -0,0 +1,28 @@ +name: Run extra checks + +# This workflow will run `run_extra_checks.sh` from the project root. + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + run_extra_checks: + runs-on: ubuntu-24.04 + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + + # Note: no need to install docker manually, because it's already installed, see + # https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md + + - name: Run the checks + run: | + docker run --rm -v "$(realpath .):/app" -w /app \ + ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \ + bash run_extra_checks.sh diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..fb70e1e --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,31 @@ +name: Run unit tests + +# This workflow will run `run_unit_tests.sh` from the project root for each device model. + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + run_extra_checks: + runs-on: ubuntu-24.04 + strategy: + matrix: + model: [nanox, nanosp, stax, flex, apex_p] + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + + # Note: no need to install docker manually, because it's already installed, see + # https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md + + - name: Run the checks + run: | + docker run --rm -v "$(realpath .):/app" -w /app \ + ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \ + bash run_unit_tests.sh ${{ matrix.model }} diff --git a/Cargo.lock b/Cargo.lock index ece13d9..4f1a245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -63,23 +63,23 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "av1-grain" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] [[package]] name = "avif-serialize" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" dependencies = [ "arrayvec", ] @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "bit_field" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" @@ -136,9 +136,9 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byte-slice-cast" @@ -175,7 +175,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -368,9 +368,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -383,23 +383,9 @@ dependencies = [ [[package]] name = "fax" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -431,25 +417,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", ] [[package]] @@ -470,12 +456,13 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -532,9 +519,9 @@ dependencies = [ [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" [[package]] name = "impl-trait-for-tuples" @@ -600,15 +587,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "lebe" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "ledger_device_sdk" @@ -647,9 +634,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -702,16 +689,6 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" -[[package]] -name = "messages" -version = "0.1.0" -dependencies = [ - "derive_more", - "mintlayer-core-primitives", - "num_enum", - "parity-scale-codec", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -731,15 +708,24 @@ dependencies = [ [[package]] name = "mintlayer-app" version = "0.1.0" +dependencies = [ + "image", + "ledger_device_sdk", + "mintlayer-app-core", +] + +[[package]] +name = "mintlayer-app-core" +version = "0.1.0" dependencies = [ "bech32", "chrono", "hex", - "image", "ledger_device_sdk", "ledger_secure_sdk_sys", - "messages", "mintlayer-core-primitives", + "mintlayer-messages", + "testmacro", ] [[package]] @@ -753,6 +739,16 @@ dependencies = [ "strum", ] +[[package]] +name = "mintlayer-messages" +version = "0.1.0" +dependencies = [ + "derive_more", + "mintlayer-core-primitives", + "num_enum", + "parity-scale-codec", +] + [[package]] name = "moxcms" version = "0.7.7" @@ -779,6 +775,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -864,9 +869,9 @@ checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parity-scale-codec" @@ -900,9 +905,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "png" @@ -956,18 +961,18 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", "syn 2.0.104", @@ -1014,9 +1019,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -1039,7 +1044,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -1094,9 +1099,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1104,9 +1109,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1143,9 +1148,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" [[package]] name = "rustc-hash" @@ -1320,6 +1325,16 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "testmacro" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e81321e8c082256753e5af547c7352ff5a9f1958c4c7e7de1eb8289ce65884f2" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1419,9 +1434,9 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "wasi" @@ -1429,29 +1444,20 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -1460,25 +1466,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.104", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1486,22 +1478,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.104", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -1594,24 +1586,24 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 74d068f..6343154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,65 @@ [package] name = "mintlayer-app" -version = "0.1.0" +version.workspace = true edition = "2021" -[dependencies] -messages = { path = "./messages" } +[workspace] +members = ["crates/app-core", "crates/messages"] +resolver = "2" + +[workspace.package] +version = "0.1.0" + +[workspace.dependencies] +bech32 = { version = "0.11", default-features = false } +chrono = { version = "0.4", default-features = false } +derive_more = { version = "2.1.1", default-features = false } +hex = { version = "0.4.3", default-features = false } +image = "0.25.8" ledger_device_sdk = "1.35.1" ledger_secure_sdk_sys = "1.16.1" -hex = { version = "0.4.3", default-features = false, features = ["alloc"] } -bech32 = { version = "0.11", default-features = false, features = ["alloc"] } -chrono = { version = "0.4", default-features = false, features = ["alloc"] } +num_enum = { version = "0.7.5", default-features = false } +# Note: the testmacro crate is published by Ledger and its source code comes from the `testmacro` +# dir inside the sdk repo, i.e. https://github.com/LedgerHQ/ledger-device-rust-sdk/tree/cad196841dbd72c037cfa01bec81a4a3ae57a04e/testmacro +# (though the published version is a bit older). +testmacro = "0.1.0" + +mintlayer-app-core = { path = "crates/app-core" } +mintlayer-messages = { path = "crates/messages" } -[dependencies.mintlayer-core-primitives] +[workspace.dependencies.parity-scale-codec] +git = "https://github.com/paritytech/parity-scale-codec.git" +# Use the specific commit "5021525697edc0661591ebc71392c48d950a10b0", +# which includes a fix for NanoX devices that do not support certain +# atomic operations. +# +# Fix reference: https://github.com/paritytech/parity-scale-codec/pull/751 +# This fix should be included in releases after version 3.7.5. +rev = "5021525697edc0661591ebc71392c48d950a10b0" +default-features = false + +[workspace.dependencies.mintlayer-core-primitives] git = "https://github.com/mintlayer/mintlayer-core-primitives" # The commit "Merge pull request #4 from mintlayer/fix_typo". rev = "8644bfe06d932d687075939d2d175183ba1c369d" package = "mintlayer-core-primitives" +[dependencies] +ledger_device_sdk.workspace = true + +mintlayer-app-core.workspace = true + [build-dependencies] -image = "0.25.8" +image.workspace = true -[features] -default = ["ledger_device_sdk/nano_nbgl"] -debug = ["ledger_device_sdk/debug"] +[[bin]] +# Note: we only have this section to disable tests, but Cargo inists that "name" should also +# be specified. +name = "mintlayer-app" +# The app bin crate is a thin wrapper around `mintlayer-app-core` and it's not supposed to have +# unit tests of its own. So we disable tests completely to prevent Cargo from producing an uncompilable +# test binary. +test = false [package.metadata.ledger] curve = ["secp256k1"] @@ -44,8 +81,3 @@ icon = "icons/mintlayer_40x40.gif" [package.metadata.ledger.apex_p] icon = "icons/mintlayer_32x32.png" - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(target_os, values("apex_p", "stax", "flex", "nanos", "nanox", "nanosplus"))', -] } diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..8feef8a --- /dev/null +++ b/clippy.toml @@ -0,0 +1,3 @@ +# Clippy will complain if objects smaller that this are boxed. +# The default is 200 and we sometimes box slightly smaller objects. +too-large-for-stack = 150 diff --git a/crates/app-core/Cargo.toml b/crates/app-core/Cargo.toml new file mode 100644 index 0000000..024e4ca --- /dev/null +++ b/crates/app-core/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "mintlayer-app-core" +version.workspace = true +edition = "2021" + +[dependencies] +bech32 = { workspace = true, default-features = false, features = ["alloc"] } +chrono = { workspace = true, default-features = false, features = ["alloc"] } +hex = { workspace = true, default-features = false, features = ["alloc"] } +ledger_device_sdk.workspace = true +ledger_secure_sdk_sys.workspace = true + +mintlayer-core-primitives.workspace = true +mintlayer-messages.workspace = true + +[dev-dependencies] +ledger_device_sdk = { workspace = true, features = ["unit_test"] } +testmacro.workspace = true + +[features] +default = ["ledger_device_sdk/nano_nbgl"] +debug = ["ledger_device_sdk/debug"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(target_os, values("apex_p", "stax", "flex", "nanos", "nanox", "nanosplus"))', +] } diff --git a/src/app_ui/address.rs b/crates/app-core/src/app_ui/address.rs similarity index 95% rename from src/app_ui/address.rs rename to crates/app-core/src/app_ui/address.rs index 2d40bcd..daaa282 100644 --- a/src/app_ui/address.rs +++ b/crates/app-core/src/app_ui/address.rs @@ -20,7 +20,7 @@ use crate::{ app_ui::utils::{compress_public_key, load_glyph, to_address}, StatusWord, }; -use messages::mlcp::{CoinType, Destination, PublicKey}; +use mintlayer_messages::mlcp::{CoinType, Destination, PublicKey}; use ledger_device_sdk::{ ecc::ECPublicKey, diff --git a/src/app_ui/menu.rs b/crates/app-core/src/app_ui/menu.rs similarity index 100% rename from src/app_ui/menu.rs rename to crates/app-core/src/app_ui/menu.rs diff --git a/src/app_ui/sign.rs b/crates/app-core/src/app_ui/sign.rs similarity index 99% rename from src/app_ui/sign.rs rename to crates/app-core/src/app_ui/sign.rs index 767e82d..4222b50 100644 --- a/src/app_ui/sign.rs +++ b/crates/app-core/src/app_ui/sign.rs @@ -26,7 +26,7 @@ use crate::{ handlers::sign_tx::{CoinOrTokenId, InputCommand, TxParsingOutputsContext, TxType}, StatusWord, }; -use messages::{ +use mintlayer_messages::{ encode, mlcp::{ AccountCommand, AccountSpending, Amount, CoinType, Destination, IsTokenFreezable, diff --git a/src/app_ui/utils.rs b/crates/app-core/src/app_ui/utils.rs similarity index 91% rename from src/app_ui/utils.rs rename to crates/app-core/src/app_ui/utils.rs index bd3997e..92a5200 100644 --- a/src/app_ui/utils.rs +++ b/crates/app-core/src/app_ui/utils.rs @@ -25,7 +25,7 @@ use ledger_device_sdk::{ }; use crate::StatusWord; -use messages::{ +use mintlayer_messages::{ encode, mlcp::{CoinType, Destination, PublicKeyHash, Secp256k1PublicKey}, }; @@ -48,13 +48,13 @@ pub fn to_address(destination: &Destination, coin: CoinType) -> Result NbglGlyph<'static> { #[cfg(target_os = "apex_p")] const MINTLAYER: NbglGlyph = - NbglGlyph::from_include(include_gif!("glyphs/mintlayer_48x48.png", NBGL)); + NbglGlyph::from_include(include_gif!("../../glyphs/mintlayer_48x48.png", NBGL)); #[cfg(any(target_os = "stax", target_os = "flex"))] const MINTLAYER: NbglGlyph = - NbglGlyph::from_include(include_gif!("glyphs/mintlayer_64x64.gif", NBGL)); + NbglGlyph::from_include(include_gif!("../../glyphs/mintlayer_64x64.gif", NBGL)); #[cfg(any(target_os = "nanosplus", target_os = "nanox"))] const MINTLAYER: NbglGlyph = - NbglGlyph::from_include(include_gif!("icons/mintlayer_14x14.gif", NBGL)); + NbglGlyph::from_include(include_gif!("../../icons/mintlayer_14x14.gif", NBGL)); MINTLAYER } diff --git a/src/errors.rs b/crates/app-core/src/errors.rs similarity index 98% rename from src/errors.rs rename to crates/app-core/src/errors.rs index 053018c..364bb55 100644 --- a/src/errors.rs +++ b/crates/app-core/src/errors.rs @@ -16,7 +16,7 @@ *****************************************************************************/ use ledger_device_sdk::ecc::CxError; -use messages::StatusWord; +use mintlayer_messages::StatusWord; pub fn cx_err_to_status(e: CxError) -> StatusWord { match e { diff --git a/src/handlers/get_public_key.rs b/crates/app-core/src/handlers/get_public_key.rs similarity index 94% rename from src/handlers/get_public_key.rs rename to crates/app-core/src/handlers/get_public_key.rs index 49b17f8..86b7c44 100644 --- a/src/handlers/get_public_key.rs +++ b/crates/app-core/src/handlers/get_public_key.rs @@ -17,7 +17,9 @@ use crate::app_ui::address::ui_display_pk; use crate::StatusWord; -use messages::{mlcp::CoinType, ChainCode, GetPublicKeyResponse, PublicKey, PublicKeyReq}; +use mintlayer_messages::{ + mlcp::CoinType, ChainCode, GetPublicKeyResponse, PublicKey, PublicKeyReq, +}; use ledger_device_sdk::ecc::{Secp256k1, SeedDerive}; diff --git a/src/handlers/sign_message.rs b/crates/app-core/src/handlers/sign_message.rs similarity index 99% rename from src/handlers/sign_message.rs rename to crates/app-core/src/handlers/sign_message.rs index 60dbdf1..5a60d55 100644 --- a/src/handlers/sign_message.rs +++ b/crates/app-core/src/handlers/sign_message.rs @@ -19,7 +19,7 @@ use crate::{ app_ui::sign::ui_display_message, errors::cx_err_to_status, handlers::utils::mintlayer_hash, DataContext, StatusWord, }; -use messages::{ +use mintlayer_messages::{ mlcp::CoinType, AddrType, Bip32Path, MsgSignatureResponse, SignMessageReq, SignatureResponse, }; diff --git a/src/handlers/sign_tx/mod.rs b/crates/app-core/src/handlers/sign_tx/mod.rs similarity index 99% rename from src/handlers/sign_tx/mod.rs rename to crates/app-core/src/handlers/sign_tx/mod.rs index a610f67..cbde975 100644 --- a/src/handlers/sign_tx/mod.rs +++ b/crates/app-core/src/handlers/sign_tx/mod.rs @@ -25,7 +25,7 @@ use crate::{ handlers::{sign_message::schnorr_sign, utils::mintlayer_hash}, DataContext, StatusWord, }; -use messages::{ +use mintlayer_messages::{ encode_as_compact, encode_to, mlcp::{CoinType as PCoinType, SighashInputCommitment, H256}, CoinType, Encode, InputAddressPath, Response, SignTxReq, SignatureResponse, TxInputReq, diff --git a/src/handlers/sign_tx/summary_collector.rs b/crates/app-core/src/handlers/sign_tx/summary_collector.rs similarity index 91% rename from src/handlers/sign_tx/summary_collector.rs rename to crates/app-core/src/handlers/sign_tx/summary_collector.rs index d39edef..8306b4f 100644 --- a/src/handlers/sign_tx/summary_collector.rs +++ b/crates/app-core/src/handlers/sign_tx/summary_collector.rs @@ -18,7 +18,7 @@ use alloc::collections::BTreeMap; use crate::StatusWord; -use messages::{ +use mintlayer_messages::{ mlcp::{ AccountCommand, AccountSpending, Amount, OrderAccountCommand, OutputValue, TxOutput, H256, }, @@ -355,3 +355,46 @@ fn into_coin_or_token_id_and_amount( } } } + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use crate::testing::prelude::*; + + use mintlayer_messages::mlcp; + + use super::*; + + // TODO: this is a sample test, need to expand it and add more tests + #[test_item] + fn sample_test() { + let mut collector = TxSummaryCollector::new(); + + collector + .process_input(&TxInputWithAdditionalInfo::Utxo( + mlcp::UtxoOutPoint::new( + mlcp::OutPointSourceId::Transaction(mlcp::Id::new(mlcp::H256::zero())), + 0, + ), + AdditionalUtxoInfo::Utxo(mlcp::TxOutput::Transfer( + mlcp::OutputValue::Coin(mlcp::Amount::from_atoms(123)), + mlcp::Destination::AnyoneCanSpend, + )), + )) + .unwrap(); + + collector + .process_output(&mlcp::TxOutput::Transfer( + mlcp::OutputValue::Coin(mlcp::Amount::from_atoms(120)), + mlcp::Destination::AnyoneCanSpend, + )) + .unwrap(); + + let fees = collector + .fees_iter() + .collect::, _>>() + .unwrap(); + assert!(fees == [(&CoinOrTokenId::Coin, 3)]); + } +} diff --git a/src/handlers/utils.rs b/crates/app-core/src/handlers/utils.rs similarity index 97% rename from src/handlers/utils.rs rename to crates/app-core/src/handlers/utils.rs index 1367672..d43404b 100644 --- a/src/handlers/utils.rs +++ b/crates/app-core/src/handlers/utils.rs @@ -17,7 +17,7 @@ use crate::StatusWord; -use messages::mlcp::H256; +use mintlayer_messages::mlcp::H256; use ledger_device_sdk::hash::{blake2::Blake2b_512, HashInit}; diff --git a/crates/app-core/src/lib.rs b/crates/app-core/src/lib.rs new file mode 100644 index 0000000..19ffc16 --- /dev/null +++ b/crates/app-core/src/lib.rs @@ -0,0 +1,351 @@ +/***************************************************************************** + * + * Mintlayer Ledger App. + * (c) 2023 Ledger SAS. + * (c) 2025 RBB S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +#![no_std] +// The following is needed to be able to generate a test executable that can be run on speculos. +// 1. Disable the generation of `fn main`. +#![cfg_attr(test, no_main)] +// 2. "custom_test_frameworks" must be enabled to be able to specify the custom runner and use +// the `#[test_case]` attribute (used internally by `testmacro::test_item`). +#![feature(custom_test_frameworks)] +// 3. Specify the custom test runner. All test cases collected by `#[test_case]` will be passed +// to this function. In particular, `sdk_test_runner` will loop over the array of test cases and: +// a) fix references stored inside the test case via pic_rs/pic; +// b) invoke the closure associated with the test case. +#![test_runner(ledger_device_sdk::testing::sdk_test_runner)] +// 4. This will put `fn test_main` at the test crate's root, which will call the runner that we've +// specified above; we'll call it from our `sample_main`. +#![reexport_test_harness_main = "test_main"] + +mod app_ui { + pub mod address; + pub mod menu; + pub mod sign; + pub mod utils; +} +mod handlers { + pub mod get_public_key; + pub mod sign_message; + pub mod sign_tx; + pub mod utils; +} + +mod errors; +#[cfg(test)] +mod testing; + +// Required for using String, Vec, format!... +extern crate alloc; +use alloc::vec::Vec; + +use ledger_device_sdk::{ + io::{ApduHeader, Comm, Reply}, + nbgl::{init_comm, NbglHomeAndSettings, NbglReviewStatus, NbglStreamingReview, StatusType}, +}; + +use app_ui::menu::ui_menu_main; +use errors::sdk_err_to_status; +use handlers::{ + get_public_key::handle_get_public_key, + sign_message::{handle_sign_message, setup_sign_message, SignMessageContext}, + sign_tx::{handle_sign_tx, setup_sign_tx, TxParsingContext}, +}; +use mintlayer_messages::{ + decode_all, encode, Ins, PubKeyP1, Response, SignP1, StatusWord, APDU_CLASS, MAX_ADPU_DATA_LEN, + P2_DONE, P2_MORE, +}; + +pub const MAX_BUFFER_LEN: usize = 4 * MAX_ADPU_DATA_LEN; + +/// Represents a fully assembled Low-Level Instruction. +/// Contains the aggregated data from one or more APDUs (if P2 indicated more data). +pub struct RawInstruction { + pub ins: u8, + pub p1: u8, + pub data: Vec, +} + +pub enum ReceiveInstructionResult { + ExpectingNextChunk, + Instruction(RawInstruction), +} + +/// State machine to handle APDU packet chaining (P2_MORE / P2_DONE). +pub struct ApduTransport { + buffer: Vec, + current_ins: Option, + current_p1: Option, +} + +impl Default for ApduTransport { + fn default() -> Self { + Self { + buffer: Vec::with_capacity(u8::MAX as usize), // Pre-alloc for at least one standard APDU + current_ins: None, + current_p1: None, + } + } +} + +impl ApduTransport { + /// Reads the next APDU from `comm`. + /// + /// - If `P2 == P2_MORE`, it accumulates the data and returns `Ok(None)`. + /// It also sends a `StatusWord::Ok` to the host to request the next chunk. + /// - If `P2 == P2_DONE`, it finishes accumulation and returns `Ok(Some(RawInstruction))`. + pub fn receive(&mut self, comm: &mut Comm) -> Result { + let header: ApduHeader = comm.next_command(); + let data = comm.get_data().map_err(sdk_err_to_status)?; + + // Validation: If we are in the middle of a stream, INS and P1 must match + if let (Some(curr_ins), Some(curr_p1)) = (self.current_ins, self.current_p1) { + if header.ins != curr_ins { + self.reset(); + return Err(StatusWord::WrongInstruction); + } + if header.p1 != curr_p1 { + self.reset(); + return Err(StatusWord::WrongP1P2); + } + } else { + // New command sequence starting + self.current_ins = Some(header.ins); + self.current_p1 = Some(header.p1); + } + + if self.buffer.len() + data.len() > MAX_BUFFER_LEN { + self.reset(); + return Err(StatusWord::MaxBufferLenExceeded); + } + + self.buffer.extend_from_slice(data); + + match header.p2 { + P2_MORE => Ok(ReceiveInstructionResult::ExpectingNextChunk), + P2_DONE => { + // Construct the full instruction + let raw = RawInstruction { + ins: header.ins, + p1: header.p1, + data: core::mem::take(&mut self.buffer), + }; + self.reset(); + Ok(ReceiveInstructionResult::Instruction(raw)) + } + _ => { + self.reset(); + Err(StatusWord::WrongP1P2) + } + } + } + + fn reset(&mut self) { + self.buffer.clear(); + self.current_ins = None; + self.current_p1 = None; + } +} + +pub enum Command { + GetPubkey { p1: PubKeyP1, data: Vec }, + SignTx { p1: SignP1, data: Vec }, + SignMessage { p1: SignP1, data: Vec }, + Ping, +} + +impl TryFrom for Command { + type Error = StatusWord; + + fn try_from(raw: RawInstruction) -> Result { + match raw.ins { + Ins::PUB_KEY => { + let p1: PubKeyP1 = raw.p1.try_into()?; + Ok(Command::GetPubkey { p1, data: raw.data }) + } + Ins::SIGN_TX => { + let p1: SignP1 = raw.p1.try_into()?; + Ok(Command::SignTx { p1, data: raw.data }) + } + Ins::SIGN_MSG => { + let p1: SignP1 = raw.p1.try_into()?; + Ok(Command::SignMessage { p1, data: raw.data }) + } + Ins::PING => Ok(Command::Ping), + _ => Err(StatusWord::InsNotSupported), + } + } +} + +fn show_status_and_home_if_needed(cmd: &Command, ctx: &mut AppContext, status: &StatusWord) { + let (show_status, status_type) = match (cmd, status) { + (Command::GetPubkey { p1, .. }, StatusWord::Deny | StatusWord::Ok) if p1.display() => { + (true, StatusType::Address) + } + (Command::SignTx { .. }, StatusWord::Deny | StatusWord::Ok) if ctx.finished() => { + (true, StatusType::Transaction) + } + (Command::SignMessage { .. }, StatusWord::Deny | StatusWord::Ok) if ctx.finished() => { + (true, StatusType::Message) + } + (Command::Ping, StatusWord::Ok) => { + ctx.home.show_and_return(); + return; + } + (_, _) => (false, StatusType::Transaction), // Default fallback + }; + + if show_status { + let success = *status == StatusWord::Ok; + NbglReviewStatus::new() + .status_type(status_type) + .show(success); + + // call home.show_and_return() to show home and setting screen + ctx.home.show_and_return(); + } +} + +pub enum DataContext { + TxContext(TxParsingContext, NbglStreamingReview), + SignMessageContext(SignMessageContext), +} + +struct AppContext { + pub data_context: Option, + pub home: NbglHomeAndSettings, +} + +impl AppContext { + fn new() -> Self { + Self { + data_context: None, + home: Default::default(), + } + } + + fn finished(&self) -> bool { + self.data_context.as_ref().is_some_and(|ctx| match ctx { + DataContext::SignMessageContext(ctx) => ctx.finished(), + DataContext::TxContext(ctx, _) => ctx.finished(), + }) + } +} + +pub fn mintlayer_main() { + let mut comm = Comm::new().set_expected_cla(APDU_CLASS); + + let mut tx_ctx = AppContext::new(); + + // Initialize reference to Comm instance for NBGL API calls. + init_comm(&mut comm); + tx_ctx.home = ui_menu_main(); + tx_ctx.home.show_and_return(); + + let mut transport = ApduTransport::default(); + + loop { + let raw_instruction = match transport.receive(&mut comm) { + Ok(ReceiveInstructionResult::Instruction(raw)) => raw, + Ok(ReceiveInstructionResult::ExpectingNextChunk) => { + // Signal host that we received the chunk and are waiting for more + comm.append(&encode(Response::ExpectingNextChunk)); + comm.reply(Reply(StatusWord::Ok as u16)); + continue; // Waiting for more chunks, loop around + } + Err(sw) => { + comm.reply(Reply(sw as u16)); + continue; + } + }; + + let command = match Command::try_from(raw_instruction) { + Ok(cmd) => cmd, + Err(sw) => { + comm.reply(Reply(sw as u16)); + continue; + } + }; + + let status = match handle_command(&command, &mut tx_ctx) { + Ok(response) => { + comm.append(&encode(response)); + comm.reply_ok(); + StatusWord::Ok + } + Err(sw) => { + comm.reply(Reply(sw as u16)); + sw + } + }; + + show_status_and_home_if_needed(&command, &mut tx_ctx, &status); + } +} + +fn handle_command(cmd: &Command, ctx: &mut AppContext) -> Result { + match cmd { + Command::GetPubkey { p1, data } => { + let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; + handle_get_public_key(req, p1.display()).map(Response::PublicKey) + } + Command::SignTx { p1, data } => match p1 { + SignP1::Start => { + let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; + ctx.data_context = Some(setup_sign_tx(req)?); + Ok(Response::TxSetup) + } + SignP1::Next => { + let (mut tx_ctx, mut review) = match ctx.data_context.take() { + Some(DataContext::TxContext(c, r)) => (c, r), + _ => return Err(StatusWord::WrongContext), + }; + + let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; + + tx_ctx.show_spinner(); + + match handle_sign_tx(req, tx_ctx, &mut review) { + Ok((response, new_ctx)) => { + ctx.data_context = Some(DataContext::TxContext(new_ctx, review)); + Ok(response) + } + Err(sw) => { + ctx.data_context = None; + Err(sw) + } + } + } + }, + Command::SignMessage { p1, data } => match p1 { + SignP1::Start => { + let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; + ctx.data_context = Some(setup_sign_message(req)); + Ok(Response::MessageSetup) + } + SignP1::Next => { + let msg_ctx = match ctx.data_context.as_mut() { + Some(DataContext::SignMessageContext(ctx)) => ctx, + _ => return Err(StatusWord::WrongContext), + }; + handle_sign_message(data, msg_ctx).map(Response::MessageSignature) + } + }, + Command::Ping => Ok(Response::Pong), + } +} diff --git a/crates/app-core/src/testing.rs b/crates/app-core/src/testing.rs new file mode 100644 index 0000000..8b75de4 --- /dev/null +++ b/crates/app-core/src/testing.rs @@ -0,0 +1,47 @@ +/***************************************************************************** + * + * Mintlayer Ledger App. + * (c) 2023 Ledger SAS. + * (c) 2026 RBB S.r.l. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +use alloc::{borrow::ToOwned as _, format}; + +pub mod prelude { + // testmacro::test_item expects `TestType` to be imported. + pub use ledger_device_sdk::testing::TestType; + + pub use testmacro::test_item; +} + +#[no_mangle] +extern "C" fn sample_main() { + crate::test_main(); + ledger_device_sdk::exit_app(0); +} + +#[panic_handler] +fn handle_panic(info: &core::panic::PanicInfo) -> ! { + ledger_device_sdk::error!( + "Panic occurred at {}: {}", + info.location().map_or_else( + || "???".to_owned(), + |loc| format!("{}:{}", loc.file(), loc.line()) + ), + info.message(), + ); + + ledger_device_sdk::exit_app(1); +} diff --git a/crates/messages/Cargo.toml b/crates/messages/Cargo.toml new file mode 100644 index 0000000..03edaeb --- /dev/null +++ b/crates/messages/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mintlayer-messages" +version.workspace = true +edition = "2024" + +[dependencies] +derive_more = { workspace = true, default-features = false, features = ["display"]} +num_enum = { workspace = true, default-features = false } +# Note: normally we would enable the "chain-error" feature of parity-scale-codec to make decode errors +# more informative. But in the Ledger app we never examine or even print those errors, so enabling this +# feature would only increase the size of the binary and make the app use more stack during decoding. +parity-scale-codec = { workspace = true, default-features = false, features = ["derive"] } + +mintlayer-core-primitives.workspace = true + +[lib] +# Note: this crate doesn't have unit tests at the moment. If we ever need them, a custom runner +# has to be set up the same way it's done in `mintlayer-app-core`. For now we just disable tests +# completely to prevent Cargo from producing an uncompilable test binary. +test = false diff --git a/messages/src/lib.rs b/crates/messages/src/lib.rs similarity index 99% rename from messages/src/lib.rs rename to crates/messages/src/lib.rs index eac377f..7bf1498 100644 --- a/messages/src/lib.rs +++ b/crates/messages/src/lib.rs @@ -340,7 +340,7 @@ impl<'a> Apdu<'a> { param1_byte: u8, command_data: &'a [u8], ) -> Option { - (command_data.len() <= MAX_ADPU_DATA_LEN).then(|| Self { + (command_data.len() <= MAX_ADPU_DATA_LEN).then_some(Self { instruction_byte, param1_byte, command_data, diff --git a/messages/Cargo.toml b/messages/Cargo.toml deleted file mode 100644 index dcceb54..0000000 --- a/messages/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "messages" -version = "0.1.0" -edition = "2024" - -[dependencies] -# Use the specific commit "5021525697edc0661591ebc71392c48d950a10b0", -# which includes a fix for NanoX devices that do not support certain -# atomic operations. -# -# Fix reference: https://github.com/paritytech/parity-scale-codec/pull/751 -# This fix should be included in releases after version 3.7.5. -# Note: normally we would enable the "chain-error" feature of parity-scale-codec to make decode errors -# more informative. But in the Ledger app we never examine or even print those errors, so enabling this -# feature would only increase the size of the binary and make the app use more stack during decoding. -parity-scale-codec = { git = "https://github.com/paritytech/parity-scale-codec.git", rev = "5021525697edc0661591ebc71392c48d950a10b0", default-features = false, features = [ - "derive", -] } - -num_enum = { version = "0.7.5", default-features = false } -derive_more = { version = "2.1.1", default-features = false, features = [ - "display", -] } - -[dependencies.mintlayer-core-primitives] -git = "https://github.com/mintlayer/mintlayer-core-primitives" -# The commit "Merge pull request #4 from mintlayer/fix_typo". -rev = "8644bfe06d932d687075939d2d175183ba1c369d" -package = "mintlayer-core-primitives" diff --git a/run_extra_checks.sh b/run_extra_checks.sh new file mode 100755 index 0000000..2e4eeea --- /dev/null +++ b/run_extra_checks.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e +set -o nounset + +# Run some extra checks (for now its mostly clippy). + +# Notes about clippy: +# 1. Ledger's guideline enforcer also runs it. But at the moment of writing this it doesn't check +# tests, see https://github.com/LedgerHQ/ledger-app-workflows/blob/master/scripts/check_all.sh. +# Besides, we want to enable some additional checks, similar to what we do in Mintlayer Core, +# so we do a separate clippy run here. +# 2. The guideline enforcer runs it for all existing device models, but in this additional run this +# is redundant, so we use one arbitrarily chosen model. +# 3. Unlike in Mintlayer Core, we can't disable certain annoying and mostly useless checks (such as +# let-and-return), because the guideline enforcer will run them anyway. + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) + +cd "$SCRIPT_DIR" + +echo "Running cargo fmt" +cargo fmt --check -- --config newline_style=Unix + +CLIPPY_TARGET_ARG=--target=apex_p + +echo "Running clippy (any code)" +cargo clippy "$CLIPPY_TARGET_ARG" --all-features --workspace --bins --lib --tests -- \ + -D warnings \ + -D clippy::implicit_saturating_sub \ + -D clippy::implicit_clone \ + -D clippy::map_unwrap_or \ + -D clippy::unnested_or_patterns \ + -D clippy::mut_mut \ + -D clippy::todo + +echo "Running clippy (production code)" +# TODO: consider also enabling `unwrap_used` and `items_after_statements`. +cargo clippy "$CLIPPY_TARGET_ARG" --all-features --workspace --bins --lib -- \ + -A clippy::all \ + -D clippy::float_arithmetic \ + -D clippy::dbg_macro \ + -D clippy::fallible_impl_from \ + -D clippy::string_slice + +echo "All checks passed" diff --git a/run_unit_tests.sh b/run_unit_tests.sh new file mode 100755 index 0000000..5f3d45c --- /dev/null +++ b/run_unit_tests.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e +set -o nounset + +# Run unit tests; the first argument must be the device model: nanox, nanosp, stax, flex or apex_p. + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) + +cd "$SCRIPT_DIR" + +MODEL=$1 + +if [[ "$MODEL" == "nanosp" ]]; then + TARGET="nanosplus" +else + TARGET="$MODEL" +fi + +echo "*** Running unit tests on $MODEL ***" + +PACKAGES=(mintlayer-app-core) + +for package in "${PACKAGES[@]}"; do + echo "*** Building unit tests for $package ***" + + output=$(cargo test -p "$package" --release --no-run --message-format=json --target="$TARGET") + jq_selector='select(.reason == "compiler-artifact") | select(.profile.test == true) | select(.executable != null) | .executable' + test_exe_path=$(jq -r "$jq_selector" <<< "$output") + + echo "*** Running unit tests for $package ***" + speculos --display headless --model "$MODEL" "$test_exe_path" +done diff --git a/src/main.rs b/src/main.rs index 962c253..1b527f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,320 +19,11 @@ #![no_std] #![no_main] -mod app_ui { - pub mod address; - pub mod menu; - pub mod sign; - pub mod utils; -} -mod handlers { - pub mod get_public_key; - pub mod sign_message; - pub mod sign_tx; - pub mod utils; -} - -mod errors; - -// Required for using String, Vec, format!... -extern crate alloc; -use alloc::vec::Vec; - -use ledger_device_sdk::{ - io::{ApduHeader, Comm, Reply}, - nbgl::{init_comm, NbglHomeAndSettings, NbglReviewStatus, NbglStreamingReview, StatusType}, -}; - -use app_ui::menu::ui_menu_main; -use errors::sdk_err_to_status; -use handlers::{ - get_public_key::handle_get_public_key, - sign_message::{handle_sign_message, setup_sign_message, SignMessageContext}, - sign_tx::{handle_sign_tx, setup_sign_tx, TxParsingContext}, -}; -use messages::{ - decode_all, encode, Ins, PubKeyP1, Response, SignP1, StatusWord, APDU_CLASS, MAX_ADPU_DATA_LEN, - P2_DONE, P2_MORE, -}; +use mintlayer_app_core::mintlayer_main; ledger_device_sdk::set_panic!(ledger_device_sdk::exiting_panic); -pub const MAX_BUFFER_LEN: usize = 4 * MAX_ADPU_DATA_LEN; - -/// Represents a fully assembled Low-Level Instruction. -/// Contains the aggregated data from one or more APDUs (if P2 indicated more data). -pub struct RawInstruction { - pub ins: u8, - pub p1: u8, - pub data: Vec, -} - -pub enum ReceiveInstructionResult { - ExpectingNextChunk, - Instruction(RawInstruction), -} - -/// State machine to handle APDU packet chaining (P2_MORE / P2_DONE). -pub struct ApduTransport { - buffer: Vec, - current_ins: Option, - current_p1: Option, -} - -impl Default for ApduTransport { - fn default() -> Self { - Self { - buffer: Vec::with_capacity(u8::MAX as usize), // Pre-alloc for at least one standard APDU - current_ins: None, - current_p1: None, - } - } -} - -impl ApduTransport { - /// Reads the next APDU from `comm`. - /// - /// - If `P2 == P2_MORE`, it accumulates the data and returns `Ok(None)`. - /// It also sends a `StatusWord::Ok` to the host to request the next chunk. - /// - If `P2 == P2_DONE`, it finishes accumulation and returns `Ok(Some(RawInstruction))`. - pub fn receive(&mut self, comm: &mut Comm) -> Result { - let header: ApduHeader = comm.next_command(); - let data = comm.get_data().map_err(sdk_err_to_status)?; - - // Validation: If we are in the middle of a stream, INS and P1 must match - if let (Some(curr_ins), Some(curr_p1)) = (self.current_ins, self.current_p1) { - if header.ins != curr_ins { - self.reset(); - return Err(StatusWord::WrongInstruction); - } - if header.p1 != curr_p1 { - self.reset(); - return Err(StatusWord::WrongP1P2); - } - } else { - // New command sequence starting - self.current_ins = Some(header.ins); - self.current_p1 = Some(header.p1); - } - - if self.buffer.len() + data.len() > MAX_BUFFER_LEN { - self.reset(); - return Err(StatusWord::MaxBufferLenExceeded); - } - - self.buffer.extend_from_slice(data); - - match header.p2 { - P2_MORE => Ok(ReceiveInstructionResult::ExpectingNextChunk), - P2_DONE => { - // Construct the full instruction - let raw = RawInstruction { - ins: header.ins, - p1: header.p1, - data: core::mem::take(&mut self.buffer), - }; - self.reset(); - Ok(ReceiveInstructionResult::Instruction(raw)) - } - _ => { - self.reset(); - Err(StatusWord::WrongP1P2) - } - } - } - - fn reset(&mut self) { - self.buffer.clear(); - self.current_ins = None; - self.current_p1 = None; - } -} - -pub enum Command { - GetPubkey { p1: PubKeyP1, data: Vec }, - SignTx { p1: SignP1, data: Vec }, - SignMessage { p1: SignP1, data: Vec }, - Ping, -} - -impl TryFrom for Command { - type Error = StatusWord; - - fn try_from(raw: RawInstruction) -> Result { - match raw.ins { - Ins::PUB_KEY => { - let p1: PubKeyP1 = raw.p1.try_into()?; - Ok(Command::GetPubkey { p1, data: raw.data }) - } - Ins::SIGN_TX => { - let p1: SignP1 = raw.p1.try_into()?; - Ok(Command::SignTx { p1, data: raw.data }) - } - Ins::SIGN_MSG => { - let p1: SignP1 = raw.p1.try_into()?; - Ok(Command::SignMessage { p1, data: raw.data }) - } - Ins::PING => Ok(Command::Ping), - _ => Err(StatusWord::InsNotSupported), - } - } -} - -fn show_status_and_home_if_needed(cmd: &Command, ctx: &mut AppContext, status: &StatusWord) { - let (show_status, status_type) = match (cmd, status) { - (Command::GetPubkey { p1, .. }, StatusWord::Deny | StatusWord::Ok) if p1.display() => { - (true, StatusType::Address) - } - (Command::SignTx { .. }, StatusWord::Deny | StatusWord::Ok) if ctx.finished() => { - (true, StatusType::Transaction) - } - (Command::SignMessage { .. }, StatusWord::Deny | StatusWord::Ok) if ctx.finished() => { - (true, StatusType::Message) - } - (Command::Ping, StatusWord::Ok) => { - ctx.home.show_and_return(); - return; - } - (_, _) => (false, StatusType::Transaction), // Default fallback - }; - - if show_status { - let success = *status == StatusWord::Ok; - NbglReviewStatus::new() - .status_type(status_type) - .show(success); - - // call home.show_and_return() to show home and setting screen - ctx.home.show_and_return(); - } -} - -pub enum DataContext { - TxContext(TxParsingContext, NbglStreamingReview), - SignMessageContext(SignMessageContext), -} - -struct AppContext { - pub data_context: Option, - pub home: NbglHomeAndSettings, -} - -impl AppContext { - fn new() -> Self { - Self { - data_context: None, - home: Default::default(), - } - } - - fn finished(&self) -> bool { - self.data_context.as_ref().is_some_and(|ctx| match ctx { - DataContext::SignMessageContext(ctx) => ctx.finished(), - DataContext::TxContext(ctx, _) => ctx.finished(), - }) - } -} - #[no_mangle] extern "C" fn sample_main() { - let mut comm = Comm::new().set_expected_cla(APDU_CLASS); - - let mut tx_ctx = AppContext::new(); - - // Initialize reference to Comm instance for NBGL API calls. - init_comm(&mut comm); - tx_ctx.home = ui_menu_main(); - tx_ctx.home.show_and_return(); - - let mut transport = ApduTransport::default(); - - loop { - let raw_instruction = match transport.receive(&mut comm) { - Ok(ReceiveInstructionResult::Instruction(raw)) => raw, - Ok(ReceiveInstructionResult::ExpectingNextChunk) => { - // Signal host that we received the chunk and are waiting for more - comm.append(&encode(Response::ExpectingNextChunk)); - comm.reply(Reply(StatusWord::Ok as u16)); - continue; // Waiting for more chunks, loop around - } - Err(sw) => { - comm.reply(Reply(sw as u16)); - continue; - } - }; - - let command = match Command::try_from(raw_instruction) { - Ok(cmd) => cmd, - Err(sw) => { - comm.reply(Reply(sw as u16)); - continue; - } - }; - - let status = match handle_command(&command, &mut tx_ctx) { - Ok(response) => { - comm.append(&encode(response)); - comm.reply_ok(); - StatusWord::Ok - } - Err(sw) => { - comm.reply(Reply(sw as u16)); - sw - } - }; - - show_status_and_home_if_needed(&command, &mut tx_ctx, &status); - } -} - -fn handle_command(cmd: &Command, ctx: &mut AppContext) -> Result { - match cmd { - Command::GetPubkey { p1, data } => { - let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; - handle_get_public_key(req, p1.display()).map(Response::PublicKey) - } - Command::SignTx { p1, data } => match p1 { - SignP1::Start => { - let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; - ctx.data_context = Some(setup_sign_tx(req)?); - Ok(Response::TxSetup) - } - SignP1::Next => { - let (mut tx_ctx, mut review) = match ctx.data_context.take() { - Some(DataContext::TxContext(c, r)) => (c, r), - _ => return Err(StatusWord::WrongContext), - }; - - let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; - - tx_ctx.show_spinner(); - - match handle_sign_tx(req, tx_ctx, &mut review) { - Ok((response, new_ctx)) => { - ctx.data_context = Some(DataContext::TxContext(new_ctx, review)); - Ok(response) - } - Err(sw) => { - ctx.data_context = None; - Err(sw) - } - } - } - }, - Command::SignMessage { p1, data } => match p1 { - SignP1::Start => { - let req = decode_all(data).ok_or(StatusWord::DeserializeFail)?; - ctx.data_context = Some(setup_sign_message(req)); - Ok(Response::MessageSetup) - } - SignP1::Next => { - let msg_ctx = match ctx.data_context.as_mut() { - Some(DataContext::SignMessageContext(ctx)) => ctx, - _ => return Err(StatusWord::WrongContext), - }; - handle_sign_message(data, msg_ctx).map(Response::MessageSignature) - } - }, - Command::Ping => Ok(Response::Pong), - } + mintlayer_main(); }