Source code: unstreaming for transaction send + warning message unification
What changed, and why it matters
This commit refactors how the Ledger Bitcoin app displays transaction details on newer devices (Stax/Flex/Apex). It replaces a streaming, one-output-at-a-time review flow with a non-streaming 'simplified' flow for transactions with up to 16 external outputs, unifies warning messages, and moves warning screens to appear before the main transaction review. The changes are primarily a user-interface restructuring rather than a clear security fix, but they do widen the simplified-review path from 0/1 external outputs to up to 16 and change the order/timing of user warnings.
Treat this as a UX refactor with potential security-UX side effects. Reviewers should verify that: (1) showing up to 16 outputs in the simplified, non-streaming flow does not overwhelm the user or hide change outputs; (2) moving warnings before the review does not cause users to dismiss them without reading; (3) removing the explicit 'Spend from known account' authorization does not reduce protection for non-default wallet policies; (4) the high-fee warning remains prominent enough in the final screen; (5) buffer sizes for cached outputs and UI strings are sufficient and not overflowed. No immediate patch or incident response is indicated by the diff alone.
Security signals we found
UI flow restructuring: simplified (non-streaming) review now handles up to 16 external outputs instead of only 0 or 1
Warning order changed: security-risk warnings are now shown before the transaction title/review for both simplified and streaming flows
Wallet authorization changed from a separate confirmation screen to a 'From' field embedded in the review
Warning text unified and made more explicit (e.g., 'Security risk detected', 'could spend more than you think')
External-inputs warning flag now set explicitly via st->warnings.external_inputs during input preprocessing
High-fee warning moved into the final validation screen instead of a separate warning screen
No cryptographic, parsing, or signature logic changes observed
Evidence from the diff
The patch renames and restructures UI helpers in src/ui/display.c/h and updates src/handler/sign_psbt.c/h accordingly. Key changes: (1) introduces MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER (16) and uses it for N_CACHED_EXTERNAL_OUTPUTS; (2) replaces the streaming per-output validation API (ui_validate_output, ui_validate_transaction) with a batch simplified API (ui_transaction_simplified_init/add/show) and a renamed streaming API (ui_transaction_streaming_); (3) moves display_warnings() before both simplified and streaming review flows; (4) unifies warning strings and adds new constants like GA_SECURITY_RISK_TITLE, GA_CONTINUE_ANYWAY, GA_BACK_TO_SAFETY; (5) removes the standalone ‘Spend from known account’ wallet authorization screen in favor of embedding the wallet policy name as a ‘From’ field; (6) adds format_output_index() for ‘X of Y’ labels. The commit message frames this as ‘unstreaming for transaction send + warning message unification’.
Changed components
src/handler/sign_psbt.csrc/handler/sign_psbt.hsrc/ui/display.csrc/ui/display.hsrc/ui/display_nbgl.csrc/ui/display_utils.csrc/ui/display_utils.hsrc/constants.hInspect captured patch +338 / −316
diff --git a/src/constants.h b/src/constants.h
index cd4f88a..ad9aa0a 100644
--- a/src/constants.h
+++ b/src/constants.h
@@ -36,6 +36,16 @@
*/
#define MAX_WALLET_NAME_LENGTH 64
+/**
+ * Maximum length of output index string
+ */
+#define MAX_OUTPUT_INDEX_LENGTH sizeof("101 of 203")
+
+/**
+ * Maximum number of external outputs handled simultaneously.
+ */
+#define MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER 16
+
/**
* Maximum length (characters) of a base58check-encoded serialized extended pubkey.
*/
@@ -70,4 +80,4 @@
#define MAX_STANDARD_P2WSH_STACK_ITEMS 100U
#define MAX_STANDARD_P2WSH_SCRIPT_SIZE 3600U
-#define MAX_OPS_PER_SCRIPT 201U
\ No newline at end of file
+#define MAX_OPS_PER_SCRIPT 201U
diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c
index d1bb90b..98083db 100644
--- a/src/handler/sign_psbt.c
+++ b/src/handler/sign_psbt.c
@@ -744,7 +744,7 @@ preprocess_inputs(dispatcher_context_t *dc,
return false;
} else if (is_internal == 0) {
++st->n_external_inputs;
-
+ st->warnings.external_inputs = true;
PRINTF("INPUT %d is external\n", cur_input_index);
continue;
}
@@ -1214,12 +1214,11 @@ display_output(dispatcher_context_t *dc,
}
// Show address to the user
- if (!ui_validate_output(dc,
- external_outputs_count,
- st->n_external_outputs,
- output_description,
- COIN_COINID_SHORT,
- out_amount)) {
+ if (!ui_transaction_streaming_validate_output(dc,
+ external_outputs_count,
+ st->n_external_outputs,
+ output_description,
+ out_amount)) {
SEND_SW(dc, SW_DENY);
return false;
}
@@ -1342,21 +1341,21 @@ static bool __attribute__((noinline)) display_external_outputs(
static bool __attribute__((noinline))
display_warnings(dispatcher_context_t *dc, sign_psbt_state_t *st) {
- // If there are external inputs, it is unsafe to sign, therefore we warn the user
- if (st->n_external_inputs > 0 && !ui_warn_external_inputs(dc)) {
+ // If any input has non-default sighash, we warn the user
+ if (st->warnings.non_default_sighash && !ui_warn_nondefault_sighash(dc)) {
SEND_SW(dc, SW_DENY);
return false;
}
- // If any segwitv0 input is missing the non-witness-utxo, we warn the user and ask for
- // confirmation
- if (st->warnings.missing_nonwitnessutxo && !ui_warn_unverified_segwit_inputs(dc)) {
+ // If there are external inputs, it is unsafe to sign, therefore we warn the user
+ if (st->warnings.external_inputs && !ui_warn_external_inputs(dc)) {
SEND_SW(dc, SW_DENY);
return false;
}
- // If any input has non-default sighash, we warn the user
- if (st->warnings.non_default_sighash && !ui_warn_nondefault_sighash(dc)) {
+ // If any segwitv0 input is missing the non-witness-utxo, we warn the user and ask for
+ // confirmation
+ if (st->warnings.missing_nonwitnessutxo && !ui_warn_unverified_segwit_inputs(dc)) {
SEND_SW(dc, SW_DENY);
return false;
}
@@ -1383,37 +1382,49 @@ static bool __attribute__((noinline)) display_transaction(
// if the value of fees is 10% or more of the amount, and it's more than 100000
st->warnings.high_fee = 10 * fee >= st->inputs_total_amount && st->inputs_total_amount > 100000;
- if (st->n_external_outputs == 0 || st->n_external_outputs == 1) {
- // A simplified flow for most transactions: show everything in a single screen if there is
- // exactly 0 (self-transfer) or 1 external output to show to the user
+ // Display warnings/risks information before the transaction title
+ // for the both classical and streaming cases.
+ if (!display_warnings(dc, st)) {
+ return false;
+ }
+
+ if (st->n_external_outputs <= MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER) {
+ // A simplified flow for most transactions: show it using the classical review if there is
+ // exactly 0 (self-transfer) or <= MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER external outputs to show
+ // to the user
bool is_self_transfer = st->n_external_outputs == 0;
- // show this output's address
- char output_description[MAX_OUTPUT_SCRIPT_DESC_SIZE];
+ /** TRANSACTION CONFIRMATION */
+ /* Init*/
+ ui_transaction_simplified_init(st->is_wallet_default ? NULL : st->wallet_header.name,
+ is_self_transfer ? 1 : st->n_external_outputs,
+ st->warnings);
+ /* Adding outputs */
if (!is_self_transfer) {
- if (!format_script(st->outputs.output_scripts[0],
- st->outputs.output_script_lengths[0],
- output_description)) {
- PRINTF("Invalid or unsupported script for external output\n");
- SEND_SW(dc, SW_NOT_SUPPORTED);
- return false;
+ for (unsigned int i = 0; i < st->n_external_outputs; i++) {
+ char output_description[MAX_OUTPUT_SCRIPT_DESC_SIZE];
+ /* It is possible to return the following error in the middle of
+ * already shown screens of previous outputs
+ */
+ if (!format_script(st->outputs.output_scripts[i],
+ st->outputs.output_script_lengths[i],
+ output_description)) {
+ PRINTF("Invalid or unsupported script for external output\n");
+ SEND_SW(dc, SW_NOT_SUPPORTED);
+ return false;
+ }
+
+ ui_transaction_simplified_add(is_self_transfer ? 0 : st->outputs.output_amounts[i],
+ is_self_transfer ? NULL : output_description);
}
+ } else {
+ ui_transaction_simplified_add(0, NULL);
}
- /** TRANSACTION CONFIRMATION
- *
- * Show transaction amount, destination and fees, ask for final confirmation
- */
- if (!ui_validate_transaction_simplified(
- dc,
- COIN_COINID_SHORT,
- st->is_wallet_default ? NULL : st->wallet_header.name,
- is_self_transfer ? 0 : st->outputs.output_amounts[0],
- is_self_transfer ? NULL : output_description,
- st->warnings,
- fee)) {
+ /* Start the review */
+ if (!ui_transaction_simplified_show(dc, fee)) {
SEND_SW(dc, SW_DENY);
return false;
}
@@ -1421,20 +1432,15 @@ static bool __attribute__((noinline)) display_transaction(
// Transactions with more than one external output; show one output per page,
// using the streaming NBGL API.
- // On NBGL devices, show the pre-approval screen
- // "Review transaction to send Bitcoin"
- if (!ui_transaction_prompt(dc)) {
- SEND_SW(dc, SW_DENY);
- return false;
- }
- // If it's not a default wallet policy, ask the user for confirmation, and abort if they
- // deny
- if (!st->is_wallet_default && !ui_authorize_wallet_spend(dc, st->wallet_header.name)) {
- SEND_SW(dc, SW_DENY);
- return false;
+ // If it's not a default wallet policy, let's save this info to ask the user for
+ // confirmation
+ if (!st->is_wallet_default) {
+ ui_prepare_authorize_wallet_spend(st->wallet_header.name);
}
- if (!display_warnings(dc, st)) {
+ // "Review transaction to send Bitcoin"
+ if (!ui_transaction_streaming_prompt(dc)) {
+ SEND_SW(dc, SW_DENY);
return false;
}
@@ -1444,17 +1450,12 @@ static bool __attribute__((noinline)) display_transaction(
*/
if (!display_external_outputs(dc, st, internal_outputs)) return false;
- if (st->warnings.high_fee && !ui_warn_high_fee(dc)) {
- SEND_SW(dc, SW_DENY);
- return false;
- }
-
/** TRANSACTION CONFIRMATION
*
* Show summary info to the user (transaction fees), ask for final confirmation
*/
// Show final user validation UI
- if (!ui_validate_transaction(dc, COIN_COINID_SHORT, fee, false)) {
+ if (!ui_transaction_streaming_validate(dc, fee, st->warnings, false)) {
SEND_SW(dc, SW_DENY);
return false;
}
diff --git a/src/handler/sign_psbt.h b/src/handler/sign_psbt.h
index 06f0f1d..7373b50 100644
--- a/src/handler/sign_psbt.h
+++ b/src/handler/sign_psbt.h
@@ -102,10 +102,10 @@ typedef struct signing_state_s {
musig_signing_state_t musig;
} signing_state_t;
-// We cache the first 2 external outputs; that's needed for the swap checks
-// Moreover, this helps the code for the simplified UX for transactions that
-// have a single external output.
-#define N_CACHED_EXTERNAL_OUTPUTS 2
+// We cache the first MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER external outputs;
+// This is used by the code for the simplified UX for transactions;
+// Moreover, that is needed for the swap checks.
+#define N_CACHED_EXTERNAL_OUTPUTS MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER
typedef struct {
uint32_t master_key_fingerprint;
diff --git a/src/ui/display.c b/src/ui/display.c
index 2fa75d9..29e3ece 100644
--- a/src/ui/display.c
+++ b/src/ui/display.c
@@ -238,17 +238,11 @@ bool ui_display_wallet_address(dispatcher_context_t *context,
return io_ui_process(context);
}
-bool ui_authorize_wallet_spend(dispatcher_context_t *context, const char *wallet_name) {
- ui_wallet_state_t *state = (ui_wallet_state_t *) &g_ui_state;
-
-#ifdef HAVE_AUTOAPPROVE_FOR_PERF_TESTS
- return true;
-#endif
-
- strncpy(state->wallet_name, wallet_name, sizeof(state->wallet_name));
- ui_display_spend_from_wallet_flow();
+void ui_prepare_authorize_wallet_spend(const char *wallet_name) {
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
- return io_ui_process(context);
+ strncpy(state->wallet_policy_name, wallet_name, sizeof(state->wallet_policy_name));
+ state->has_wallet_policy = true;
}
bool ui_warn_external_inputs(dispatcher_context_t *context) {
@@ -278,83 +272,62 @@ bool ui_warn_nondefault_sighash(dispatcher_context_t *context) {
return io_ui_process(context);
}
-bool ui_transaction_prompt(dispatcher_context_t *context) {
+bool ui_transaction_streaming_prompt(dispatcher_context_t *context) {
#ifdef HAVE_AUTOAPPROVE_FOR_PERF_TESTS
return true;
#endif
- ui_display_transaction_prompt();
+ ui_display_transaction_streaming_prompt();
return io_ui_process(context);
}
-bool ui_validate_output(dispatcher_context_t *context,
- int index,
- int total_count,
- const char *address_or_description,
- const char *coin_name,
- uint64_t amount) {
+bool ui_transaction_streaming_validate_output(dispatcher_context_t *context,
+ int index,
+ int total_count,
+ const char *address_or_description,
+ uint64_t amount) {
#ifdef HAVE_AUTOAPPROVE_FOR_PERF_TESTS
return true;
#endif
- ui_validate_output_state_t *state = (ui_validate_output_state_t *) &g_ui_state;
-
- strncpy(state->address_or_description,
- address_or_description,
- sizeof(state->address_or_description));
- format_sats_amount(coin_name, amount, state->amount);
-
- if (total_count == 1) {
- ui_display_output_address_amount_no_index_flow(index);
- } else {
- ui_display_output_address_amount_flow(index);
- }
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
- return io_ui_process(context);
-}
+ format_output_index(index, total_count, state->output_index_str[0]);
-bool ui_warn_high_fee(dispatcher_context_t *context) {
-#ifdef HAVE_AUTOAPPROVE_FOR_PERF_TESTS
- return true;
-#endif
+ strncpy(state->address_or_description[0],
+ address_or_description,
+ sizeof(state->address_or_description[0]));
+ format_sats_amount(COIN_COINID_SHORT, amount, state->amount[0]);
- ui_warn_high_fee_flow();
+ ui_display_transaction_streaming_output_address_amount();
return io_ui_process(context);
}
-bool ui_validate_transaction(dispatcher_context_t *context,
- const char *coin_name,
- uint64_t fee,
- bool is_self_transfer) {
+bool ui_transaction_streaming_validate(dispatcher_context_t *context,
+ uint64_t fee,
+ tx_ux_warning_t warnings,
+ bool is_self_transfer) {
#ifdef HAVE_AUTOAPPROVE_FOR_PERF_TESTS
return true;
#endif
ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
- format_sats_amount(coin_name, fee, state->fee);
+ format_sats_amount(COIN_COINID_SHORT, fee, state->fee);
+ state->warnings = warnings;
- ui_accept_transaction_flow(is_self_transfer);
+ ui_display_transaction_streaming_flow(is_self_transfer);
return io_ui_process(context);
}
-bool ui_validate_transaction_simplified(dispatcher_context_t *context,
- const char *coin_name,
- const char *wallet_policy_name,
- uint64_t amount,
- const char *address_or_description,
- tx_ux_warning_t warnings,
- uint64_t fee) {
-#ifdef HAVE_AUTOAPPROVE_FOR_PERF_TESTS
- return true;
-#endif
-
- ui_validate_transaction_simplified_state_t *state =
- (ui_validate_transaction_simplified_state_t *) &g_ui_state;
+void ui_transaction_simplified_init(const char *wallet_policy_name,
+ unsigned int outputs_num,
+ tx_ux_warning_t warnings) {
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
- memset(state, 0, sizeof(ui_validate_transaction_simplified_state_t));
+ memset(state, 0, sizeof(ui_validate_transaction_state_t));
if (wallet_policy_name != NULL) {
strncpy(state->wallet_policy_name, wallet_policy_name, sizeof(state->wallet_policy_name));
@@ -362,18 +335,40 @@ bool ui_validate_transaction_simplified(dispatcher_context_t *context,
} else {
memset(state->wallet_policy_name, 0, sizeof(state->wallet_policy_name));
}
- format_sats_amount(coin_name, amount, state->amount);
+ state->n_outputs = outputs_num;
+ state->warnings = warnings;
+
+ ui_display_transaction_simplified_flow_init();
+}
+
+void ui_transaction_simplified_add(uint64_t amount, const char *address_or_description) {
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
+
+ format_sats_amount(COIN_COINID_SHORT, amount, state->amount[state->output_index]);
if (address_or_description == NULL) {
state->is_self_transfer = true;
} else {
- strncpy(state->address_or_description,
+ strncpy(state->address_or_description[state->output_index],
address_or_description,
- sizeof(state->address_or_description));
+ sizeof(state->address_or_description[state->output_index]));
}
- state->warnings = warnings;
- format_sats_amount(coin_name, fee, state->fee);
+ format_output_index(state->output_index + 1,
+ state->n_outputs,
+ state->output_index_str[state->output_index]);
+
+ ui_display_transaction_simplified_flow_add();
+ state->output_index++;
+}
+
+bool ui_transaction_simplified_show(dispatcher_context_t *context, uint64_t fee) {
+#ifdef HAVE_AUTOAPPROVE_FOR_PERF_TESTS
+ return true;
+#endif
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
+
+ format_sats_amount(COIN_COINID_SHORT, fee, state->fee);
- ui_accept_transaction_simplified_flow();
+ ui_display_transaction_simplified_flow_show();
return io_ui_process(context);
}
diff --git a/src/ui/display.h b/src/ui/display.h
index ab7f19e..e8b837d 100644
--- a/src/ui/display.h
+++ b/src/ui/display.h
@@ -22,14 +22,17 @@
#if defined(TARGET_STAX) || defined(TARGET_FLEX)
#define ICON_APP_IMPORTANT IMPORTANT_CIRCLE_ICON
+#define ICON_APP_WARNING LARGE_WARNING_ICON
#define ICON_APP_HOME C_Bitcoin_64px
#define ICON_APP_ACTION C_Bitcoin_64px
#elif defined(TARGET_APEX_P)
#define ICON_APP_IMPORTANT IMPORTANT_CIRCLE_ICON
+#define ICON_APP_WARNING LARGE_WARNING_ICON
#define ICON_APP_HOME C_Bitcoin_48px
#define ICON_APP_ACTION C_Bitcoin_48px
#else
-#define ICON_APP_IMPORTANT C_icon_warning
+#define ICON_APP_IMPORTANT WARNING_ICON
+#define ICON_APP_WARNING WARNING_ICON
#define ICON_APP_HOME C_bitcoin_logo
#define ICON_APP_ACTION C_bitcoin_logo_inv
#endif
@@ -96,24 +99,18 @@ typedef struct {
} ui_cosigner_pubkey_and_index_state_t;
typedef struct {
- char index[sizeof("output #999")];
- char address_or_description[MAX(MAX_ADDRESS_LENGTH_STR + 1, MAX_OPRETURN_OUTPUT_DESC_SIZE)];
- char amount[MAX_AMOUNT_LENGTH + 1];
-} ui_validate_output_state_t;
-
-typedef struct {
- char fee[MAX_AMOUNT_LENGTH + 1];
-} ui_validate_transaction_state_t;
-
-typedef struct {
+ tx_ux_warning_t warnings;
bool has_wallet_policy;
- bool is_self_transfer;
char wallet_policy_name[MAX_WALLET_NAME_LENGTH + 1];
- char address_or_description[MAX(MAX_ADDRESS_LENGTH_STR + 1, MAX_OPRETURN_OUTPUT_DESC_SIZE)];
- char amount[MAX_AMOUNT_LENGTH + 1];
+ bool is_self_transfer;
+ unsigned int n_outputs;
+ unsigned int output_index;
+ char output_index_str[MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER][MAX_OUTPUT_INDEX_LENGTH + 1];
+ char address_or_description[MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER]
+ [MAX(MAX_ADDRESS_LENGTH_STR + 1, MAX_OPRETURN_OUTPUT_DESC_SIZE)];
+ char amount[MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER][MAX_AMOUNT_LENGTH + 1];
char fee[MAX_AMOUNT_LENGTH + 1];
- tx_ux_warning_t warnings;
-} ui_validate_transaction_simplified_state_t;
+} ui_validate_transaction_state_t;
/**
* Union of all the states for each of the UI screens, in order to save memory.
@@ -124,10 +121,8 @@ typedef union {
ui_path_and_message_state_t path_and_message;
ui_wallet_state_t wallet;
ui_cosigner_pubkey_and_index_state_t cosigner_pubkey_and_index;
- ui_validate_output_state_t validate_output;
- ui_validate_transaction_state_t validate_transaction;
ui_register_wallet_policy_state_t register_wallet_policy;
- ui_validate_transaction_simplified_state_t validate_transaction_simplified;
+ ui_validate_transaction_state_t validate_transaction_simplified;
} ui_state_t;
extern ui_state_t g_ui_state;
@@ -174,7 +169,7 @@ bool ui_display_wallet_address(dispatcher_context_t *context,
bool ui_display_unusual_path(dispatcher_context_t *context, const char *bip32_path_str);
-bool ui_authorize_wallet_spend(dispatcher_context_t *context, const char *wallet_name);
+void ui_prepare_authorize_wallet_spend(const char *wallet_name);
bool ui_warn_external_inputs(dispatcher_context_t *context);
@@ -182,30 +177,36 @@ bool ui_warn_unverified_segwit_inputs(dispatcher_context_t *context);
bool ui_warn_nondefault_sighash(dispatcher_context_t *context);
-bool ui_validate_output(dispatcher_context_t *context,
- int index,
- int total_count,
- const char *address_or_description,
- const char *coin_name,
- uint64_t amount);
-
bool ui_warn_high_fee(dispatcher_context_t *context);
-bool ui_validate_transaction(dispatcher_context_t *context,
- const char *coin_name,
- uint64_t fee,
- bool is_self_transfer);
-
-bool ui_validate_transaction_simplified(dispatcher_context_t *context,
- const char *coin_name,
- const char *wallet_policy_name, // can be NULL
- uint64_t amount,
- const char *address_or_description,
- tx_ux_warning_t warnings,
- uint64_t fee);
+/* These 3 functions have to be called in following order:
+ * 1. init - to initialize the transaction signature flow with basic parameters.
+ * 2. add - to add information for an output.
+ * 3. show - to actually start showing the transaction screens.
+ * These functions call respectively init, add and show functions from display_nbgl module.
+ */
+void ui_transaction_simplified_init(const char *wallet_policy_name,
+ unsigned int outputs_num,
+ tx_ux_warning_t warnings);
+void ui_transaction_simplified_add(uint64_t amount, const char *address_or_description);
+bool ui_transaction_simplified_show(dispatcher_context_t *context, uint64_t fee);
+
+bool ui_transaction_streaming_prompt(dispatcher_context_t *context);
+bool ui_transaction_streaming_validate_output(dispatcher_context_t *context,
+ int index,
+ int total_count,
+ const char *address_or_description,
+ uint64_t amount);
+bool ui_transaction_streaming_validate(dispatcher_context_t *context,
+ uint64_t fee,
+ tx_ux_warning_t warnings,
+ bool is_self_transfer);
void set_ux_flow_response(bool approved);
+/* Functions from display_nbgl.c
+ * TODO: to merge display.c and display_nbgl.c
+ */
void ui_display_pubkey_flow(void);
void ui_display_pubkey_suspicious_flow(void);
@@ -228,18 +229,17 @@ void ui_display_unverified_segwit_inputs_flows(void);
void ui_display_nondefault_sighash_flow(void);
-void ui_display_output_address_amount_flow(int index);
-
-void ui_display_output_address_amount_no_index_flow(int index);
-
void ui_warn_high_fee_flow(void);
-void ui_accept_transaction_flow(bool is_self_transfer);
-
void ui_display_register_wallet_policy_flow(void);
-void ui_accept_transaction_simplified_flow(void);
-void ui_display_transaction_prompt(void);
+void ui_display_transaction_simplified_flow_init(void);
+void ui_display_transaction_simplified_flow_add(void);
+void ui_display_transaction_simplified_flow_show(void);
+
+void ui_display_transaction_streaming_prompt(void);
+void ui_display_transaction_streaming_output_address_amount(void);
+void ui_display_transaction_streaming_flow(bool is_self_transfer);
bool ui_post_processing_confirm_wallet_spend(dispatcher_context_t *context, bool success);
@@ -249,7 +249,6 @@ bool ui_post_processing_confirm_message(dispatcher_context_t *context, bool succ
void ui_pre_processing_message(void);
-bool ui_transaction_prompt(dispatcher_context_t *context);
void ui_display_post_processing_confirm_message(bool success);
void ui_display_post_processing_confirm_transaction(bool success);
void ui_set_display_prompt(void);
diff --git a/src/ui/display_nbgl.c b/src/ui/display_nbgl.c
index 7ddbba0..fd96453 100644
--- a/src/ui/display_nbgl.c
+++ b/src/ui/display_nbgl.c
@@ -24,15 +24,37 @@ const char GA_SIGN_MESSAGE[] = "Sign message";
const char GA_REGISTER_ACCOUNT[] = "Register account";
#endif /* #ifdef SCREEN_SIZE_WALLET */
-const char GA_UNVERIFIED_INPUTS[] = "Unverified inputs\nUpdate your wallet software";
+#ifdef SCREEN_SIZE_WALLET
+const char GA_SECURITY_RISK_TITLE[] = "Security risk detected";
+const char GA_WARN_HIGH_FEES_TITLE[] = "High fees warning";
+const char GA_RISK_EXTERNAL_INPUTS[] =
+ "This transaction has external inputs, and could spend more than you think.";
+const char GA_RISK_NON_STD_SIGHASH[] =
+ "This transaction uses non-standard signing rules (modified sighash), and could spend more "
+ "than you think.";
+const char GA_WARN_HIGH_FEES[] =
+ "You're about to review a transaction with fees above 10\% of the total amount.";
+
+#else
+const char GA_SECURITY_RISK_TITLE[] = "Security risk";
+const char GA_WARN_HIGH_FEES_TITLE[] = "High fees warning";
+const char GA_RISK_EXTERNAL_INPUTS[] = "There are external inputs\nReject if not sure";
+const char GA_RISK_NON_STD_SIGHASH[] = "Non-default sighash";
+const char GA_WARN_HIGH_FEES[] = "Fees are above 10%\n of total amount";
+#endif
+
+const char GA_BACK_TO_SAFETY[] = "Back to safety";
+const char GA_CONTINUE_ANYWAY[] = "Continue anyway";
+const char GA_RISK_UNVERIFIED_INPUTS[] = "Unverified inputs\nUpdate your wallet software";
const char GA_REVIEW_TRANSACTION[] = "Review transaction\nto send Bitcoin";
const char GA_REVIEW_MESSAGE[] = "Review message";
const char GA_LOADING_TRANSACTION[] = "Loading transaction";
const char GA_LOADING_MESSAGE[] = "Loading message";
-#define N_UX_PAIRS 18
+#define N_UX_PAIRS 51
static nbgl_layoutTagValue_t pairs[N_UX_PAIRS];
+static unsigned int n_pairs;
static nbgl_layoutTagValueList_t pairList;
static nbgl_genericContents_t genericContent;
@@ -40,6 +62,8 @@ static nbgl_content_t contentList[4];
extern bool G_was_processing_screen_shown;
+static void finish_transaction_flow(bool choice);
+
// ux_flow_response
static void ux_flow_response_false(void) {
set_ux_flow_response(false);
@@ -128,108 +152,69 @@ static void generic_content_callback(int token, uint8_t index, int page) {
}
}
-static void finish_transaction_flow(bool choice) {
- if (choice) {
- nbgl_useCaseReviewStreamingFinish(GA_SIGN_TRANSACTION,
- start_processing_transaction_callback);
- } else {
- status_transaction_cancel();
- }
-}
-
-void ui_accept_transaction_flow(bool is_self_transfer) {
- // Setup list
- pairList.nbMaxLinesForValue = 0;
- pairList.pairs = pairs;
-
- if (!is_self_transfer) {
- pairs[0].item = "Fees";
- pairs[0].value = g_ui_state.validate_transaction.fee;
-
- pairList.nbPairs = 1;
- } else {
- pairs[0].item = "Amount";
- pairs[0].value = "Self-transfer";
-
- pairs[1].item = "Fees";
- pairs[1].value = g_ui_state.validate_transaction.fee;
-
- pairList.nbPairs = 2;
- }
-
- nbgl_useCaseReviewStreamingContinue(&pairList, finish_transaction_flow);
-}
-
#define COMBINE(a, b) a b
// create the string "0 <coind_id> (self-transfer)"
#define SELF_TRANSFER_DESCRIPTION COMBINE("0 ", COMBINE(COIN_COINID_SHORT, " (self-transfer)"))
-void ui_accept_transaction_simplified_flow(void) {
- _Static_assert(N_UX_PAIRS >= 9, "Insufficient pairs for this flow");
-
+void ui_display_transaction_simplified_flow_init(void) {
+ /* 1 From + MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER*3 + 1 Fees + 1 High fees */
+ _Static_assert(N_UX_PAIRS >= (1 + MAX_EXT_OUTPUT_SIMPLIFIED_NUMBER * 3 + 1 + 1),
+ "Insufficient pairs for this flow");
// Setup list
pairList.nbMaxLinesForValue = 0;
pairList.pairs = pairs;
+ n_pairs = 0;
- int n_pairs = 0;
-
- // Add warning screens for unverified inputs, external inputs or non-default sighash
- if (g_ui_state.validate_transaction_simplified.warnings.missing_nonwitnessutxo) {
- pairs[n_pairs++] = (nbgl_contentTagValue_t){.item = GA_UNVERIFIED_INPUTS,
- .value = "",
- .centeredInfo = true,
- .valueIcon = &ICON_APP_IMPORTANT};
- }
- if (g_ui_state.validate_transaction_simplified.warnings.external_inputs) {
- pairs[n_pairs++] =
- (nbgl_contentTagValue_t){.item = "There are external inputs\nReject if not sure",
- .value = "",
- .centeredInfo = true,
- .valueIcon = &ICON_APP_IMPORTANT};
- }
- if (g_ui_state.validate_transaction_simplified.warnings.non_default_sighash) {
- pairs[n_pairs++] =
- (nbgl_contentTagValue_t){.item = "Non-default sighash\nReject if not sure",
- .value = "",
- .centeredInfo = true,
- .valueIcon = &ICON_APP_IMPORTANT};
- }
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
- if (g_ui_state.validate_transaction_simplified.has_wallet_policy) {
+ if (state->has_wallet_policy) {
pairs[n_pairs++] = (nbgl_layoutTagValue_t){
.item = "From",
- .value = g_ui_state.validate_transaction_simplified.wallet_policy_name,
+ .value = state->wallet_policy_name,
};
}
+}
- if (!g_ui_state.validate_transaction_simplified.is_self_transfer) {
- pairs[n_pairs++] = (nbgl_layoutTagValue_t){
- .item = "Amount",
- .value = g_ui_state.validate_transaction_simplified.amount,
- };
+void ui_display_transaction_simplified_flow_add(void) {
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
+
+ unsigned int output_index = state->output_index;
+ if (!state->is_self_transfer) {
+ if (state->n_outputs > 1) {
+ pairs[n_pairs++] =
+ (nbgl_layoutTagValue_t){.item = "Transaction output",
+ .value = state->output_index_str[output_index],
+ .forcePageStart = true};
+ }
+ pairs[n_pairs++] = (nbgl_layoutTagValue_t){.item = "Amount",
+ .value = state->amount[output_index],
+ .forcePageStart = false};
pairs[n_pairs++] = (nbgl_layoutTagValue_t){
.item = "To",
- .value = g_ui_state.validate_transaction_simplified.address_or_description,
+ .value = state->address_or_description[output_index],
};
} else {
pairs[n_pairs++] =
(nbgl_layoutTagValue_t){.item = "Amount", .value = SELF_TRANSFER_DESCRIPTION};
}
+}
- pairs[n_pairs++] = (nbgl_layoutTagValue_t){
- .item = "Fees",
- .value = g_ui_state.validate_transaction_simplified.fee,
- };
+void ui_display_transaction_simplified_flow_show(void) {
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
- if (g_ui_state.validate_transaction_simplified.warnings.high_fee) {
- pairs[n_pairs++] = (nbgl_contentTagValue_t){.item = "Fees are above 10%\n of total amount",
- .value = "",
+ if (state->warnings.high_fee) {
+ pairs[n_pairs++] = (nbgl_contentTagValue_t){.item = GA_WARN_HIGH_FEES_TITLE,
+ .value = GA_WARN_HIGH_FEES,
.centeredInfo = true,
.valueIcon = &ICON_APP_IMPORTANT};
}
+ pairs[n_pairs++] = (nbgl_layoutTagValue_t){.item = "Fees",
+ .value = state->fee,
+ .forcePageStart = state->n_outputs > 1 ? 1 : 0};
+
pairList.nbPairs = n_pairs;
nbgl_useCaseReview(TYPE_TRANSACTION,
@@ -241,28 +226,39 @@ void ui_accept_transaction_simplified_flow(void) {
start_transaction_callback);
}
-void ui_display_transaction_prompt(void) {
+void ui_display_transaction_streaming_prompt(void) {
nbgl_useCaseReviewStreamingStart(TYPE_TRANSACTION,
&ICON_APP_ACTION,
GA_REVIEW_TRANSACTION,
NULL,
start_transaction_callback);
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
+
+ if (state->has_wallet_policy) {
+ pairs[0] = (nbgl_layoutTagValue_t){
+ .item = "From",
+ .value = state->wallet_policy_name,
+ };
+ // Setup list
+ pairList.nbMaxLinesForValue = 0;
+ pairList.nbPairs = 1;
+ pairList.pairs = pairs;
+
+ nbgl_useCaseReviewStreamingContinue(&pairList, start_transaction_callback);
+ }
}
-void ui_display_output_address_amount_flow(int index) {
- snprintf(g_ui_state.validate_output.index,
- sizeof(g_ui_state.validate_output.index),
- "#%d",
- index);
+void ui_display_transaction_streaming_output_address_amount(void) {
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
- pairs[0].item = "Output";
- pairs[0].value = g_ui_state.validate_output.index;
+ pairs[0].item = "Transaction output";
+ pairs[0].value = state->output_index_str[0];
pairs[1].item = "Amount";
- pairs[1].value = g_ui_state.validate_output.amount;
+ pairs[1].value = state->amount[0];
pairs[2].item = "Address";
- pairs[2].value = g_ui_state.validate_output.address_or_description;
+ pairs[2].value = state->address_or_description[0];
// Setup list
pairList.nbMaxLinesForValue = 0;
@@ -272,21 +268,46 @@ void ui_display_output_address_amount_flow(int index) {
nbgl_useCaseReviewStreamingContinue(&pairList, start_transaction_callback);
}
-void ui_display_output_address_amount_no_index_flow(int index) {
- UNUSED(index);
-
- pairs[0].item = "Amount";
- pairs[0].value = g_ui_state.validate_output.amount;
-
- pairs[1].item = "Address";
- pairs[1].value = g_ui_state.validate_output.address_or_description;
-
+void ui_display_transaction_streaming_flow(bool is_self_transfer) {
// Setup list
pairList.nbMaxLinesForValue = 0;
- pairList.nbPairs = 2;
pairList.pairs = pairs;
- nbgl_useCaseReviewStreamingContinue(&pairList, start_transaction_callback);
+ unsigned int l_n_pairs = 0;
+ ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state;
+
+ if (state->warnings.high_fee) {
+ pairs[l_n_pairs++] = (nbgl_contentTagValue_t){.item = GA_WARN_HIGH_FEES_TITLE,
+ .value = GA_WARN_HIGH_FEES,
+ .centeredInfo = true,
+ .valueIcon = &ICON_APP_IMPORTANT};
+ }
+
+ if (!is_self_transfer) {
+ pairs[l_n_pairs].item = "Fees";
+ pairs[l_n_pairs++].value = state->fee;
+
+ pairList.nbPairs = l_n_pairs;
+ } else {
+ pairs[l_n_pairs].item = "Amount";
+ pairs[l_n_pairs++].value = "Self-transfer";
+
+ pairs[l_n_pairs].item = "Fees";
+ pairs[l_n_pairs++].value = state->fee;
+
+ pairList.nbPairs = l_n_pairs;
+ }
+
+ nbgl_useCaseReviewStreamingContinue(&pairList, finish_transaction_flow);
+}
+
+static void finish_transaction_flow(bool choice) {
+ if (choice) {
+ nbgl_useCaseReviewStreamingFinish(GA_SIGN_TRANSACTION,
+ start_processing_transaction_callback);
+ } else {
+ status_transaction_cancel();
+ }
}
// Continue light notify callback
@@ -339,7 +360,7 @@ void ui_display_register_wallet_policy_flow(void) {
confirmed_status = "Account registered";
rejected_status = "Account rejected";
- int n_pairs = 0;
+ n_pairs = 0;
pairList.nbMaxLinesForValue = 0;
pairList.pairs = pairs;
@@ -535,28 +556,6 @@ void ui_set_display_prompt(void) {
show_message_start_page = true;
}
-void ui_display_spend_from_wallet_flow(void) {
- confirmed_status = "Account name\nconfirmed";
- rejected_status = "Account name rejected";
-
- // Setup data to display
- pairs[0].item = "Account name";
- pairs[0].value = g_ui_state.wallet.wallet_name;
-
- // Setup list
- pairList.nbMaxLinesForValue = 0;
- pairList.nbPairs = 1;
- pairList.pairs = pairs;
-
- nbgl_useCaseReviewLight(TYPE_OPERATION,
- &pairList,
- &ICON_APP_ACTION,
- "Spend from\nknown account",
- NULL,
- "Confirm account name",
- status_operation_callback);
-}
-
// Address flow
void ui_display_default_wallet_address_flow(void) {
nbgl_useCaseAddressReview(g_ui_state.wallet.address,
@@ -567,41 +566,42 @@ void ui_display_default_wallet_address_flow(void) {
status_address_callback);
}
-// Warning Flows
-void ui_warn_high_fee_flow(void) {
- nbgl_useCaseChoice(&ICON_APP_IMPORTANT,
- "Warning",
- "Fees are above 10%\n of total amount",
- "Continue",
- "Reject",
+#ifdef SCREEN_SIZE_WALLET
+static void display_warning_generic_callback(bool confirm) {
+ if (!confirm) {
+ ux_flow_response_true();
+ } else {
+ status_transaction_cancel();
+ }
+}
+#endif /* #ifdef SCREEN_SIZE_WALLET */
+
+void ui_display_warning_generic(const char *msg) {
+ nbgl_useCaseChoice(&ICON_APP_WARNING,
+ GA_SECURITY_RISK_TITLE,
+ msg,
+#ifdef SCREEN_SIZE_WALLET
+ GA_BACK_TO_SAFETY,
+ GA_CONTINUE_ANYWAY,
+ display_warning_generic_callback);
+#else
+ GA_CONTINUE_ANYWAY,
+ GA_BACK_TO_SAFETY,
start_transaction_callback);
+#endif /* #ifdef SCREEN_SIZE_WALLET */
}
+// Warning/Security risks flows
void ui_display_warning_external_inputs_flow(void) {
- nbgl_useCaseChoice(&ICON_APP_IMPORTANT,
- "Warning",
- "There are external inputs",
- "Continue",
- "Reject if not sure",
- start_transaction_callback);
+ ui_display_warning_generic(GA_RISK_EXTERNAL_INPUTS);
}
void ui_display_unverified_segwit_inputs_flows(void) {
- nbgl_useCaseChoice(&ICON_APP_IMPORTANT,
- "Warning",
- GA_UNVERIFIED_INPUTS,
- "Continue",
- "Reject if not sure",
- start_transaction_callback);
+ ui_display_warning_generic(GA_RISK_UNVERIFIED_INPUTS);
}
void ui_display_nondefault_sighash_flow(void) {
- nbgl_useCaseChoice(&ICON_APP_IMPORTANT,
- "Warning",
- "Non-default sighash",
- "Continue",
- "Reject if not sure",
- start_transaction_callback);
+ ui_display_warning_generic(GA_RISK_NON_STD_SIGHASH);
}
// Statuses
diff --git a/src/ui/display_utils.c b/src/ui/display_utils.c
index de73157..b422153 100644
--- a/src/ui/display_utils.c
+++ b/src/ui/display_utils.c
@@ -46,3 +46,9 @@ void format_sats_amount(const char *coin_name,
fractional_part ? fractional_str : "",
coin_name);
}
+
+void format_output_index(const unsigned int output_index,
+ const unsigned int n_outputs,
+ char out[static MAX_OUTPUT_INDEX_LENGTH + 1]) {
+ snprintf(out, MAX_OUTPUT_INDEX_LENGTH + 1, "%u of %u", output_index, n_outputs);
+}
diff --git a/src/ui/display_utils.h b/src/ui/display_utils.h
index c29a405..4beb3bf 100644
--- a/src/ui/display_utils.h
+++ b/src/ui/display_utils.h
@@ -20,4 +20,15 @@
*/
void format_sats_amount(const char *coin_name,
uint64_t amount,
- char out[static MAX_AMOUNT_LENGTH + 1]);
\ No newline at end of file
+ char out[static MAX_AMOUNT_LENGTH + 1]);
+
+/**
+ * Formats output index string ("101 of 203").
+ *
+ * @param output_index current output index
+ * @param n_outputs total number of outputs
+ * @param out the output array which must fit to MAX_OUTPUT_INDEX_LENGTH + 1 bytes length
+ */
+void format_output_index(const unsigned int output_index,
+ const unsigned int n_outputs,
+ char out[static MAX_OUTPUT_INDEX_LENGTH + 1]);
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.