diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 2502b11ac..154b0f82a 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -1378,11 +1378,20 @@ confirm_transaction(dispatcher_context_t *dc, sign_psbt_state_t *st) { } } else { // Show final user validation UI - if (!ui_validate_transaction(dc, COIN_COINID_SHORT, fee)) { - SEND_SW(dc, SW_DENY); - ui_post_processing_confirm_transaction(dc, false); - return false; - }; + if (st->outputs.n_external == 0) { + // All outputs are change; show the user it's a self transfer + if (!ui_validate_selftransfer(dc, COIN_COINID_SHORT, fee)) { + SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_transaction(dc, false); + return false; + } + } else { + if (!ui_validate_transaction(dc, COIN_COINID_SHORT, fee)) { + SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_transaction(dc, false); + return false; + } + } } return true; diff --git a/src/ui/display.c b/src/ui/display.c index 4d32ac746..63742c5d1 100644 --- a/src/ui/display.c +++ b/src/ui/display.c @@ -209,6 +209,17 @@ bool ui_validate_transaction(dispatcher_context_t *context, const char *coin_nam return io_ui_process(context, true); } +// Special case when all the outputs are change: show a "Self-transfer" screen in the flow +bool ui_validate_selftransfer(dispatcher_context_t *context, const char *coin_name, uint64_t fee) { + ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state; + + format_sats_amount(coin_name, fee, state->fee); + + ui_accept_selftransfer_flow(); + + return io_ui_process(context, true); +} + #ifdef HAVE_BAGL bool ui_post_processing_confirm_wallet_registration(dispatcher_context_t *context, bool success) { (void) context; diff --git a/src/ui/display.h b/src/ui/display.h index cda6ea5d9..9026b4cfd 100644 --- a/src/ui/display.h +++ b/src/ui/display.h @@ -134,6 +134,8 @@ bool ui_validate_output(dispatcher_context_t *context, bool ui_validate_transaction(dispatcher_context_t *context, const char *coin_name, uint64_t fee); +bool ui_validate_selftransfer(dispatcher_context_t *context, const char *coin_name, uint64_t fee); + void set_ux_flow_response(bool approved); void ui_display_pubkey_flow(void); @@ -164,6 +166,8 @@ void ui_display_output_address_amount_no_index_flow(int index); void ui_accept_transaction_flow(void); +void ui_accept_selftransfer_flow(void); + void ui_display_transaction_prompt(const int external_outputs_total_count); bool ui_post_processing_confirm_wallet_registration(dispatcher_context_t *context, bool success); diff --git a/src/ui/display_bagl.c b/src/ui/display_bagl.c index 9d8e5e808..38486e2c4 100644 --- a/src/ui/display_bagl.c +++ b/src/ui/display_bagl.c @@ -168,6 +168,7 @@ UX_STEP_NOCB(ux_validate_address_step, }); UX_STEP_NOCB(ux_confirm_transaction_step, pnn, {&C_icon_eye, "Confirm", "transaction"}); +UX_STEP_NOCB(ux_confirm_selftransfer_step, pnn, {&C_icon_eye, "Confirm", "self-transfer"}); UX_STEP_NOCB(ux_confirm_transaction_fees_step, bnnn_paging, { @@ -386,7 +387,7 @@ UX_FLOW(ux_display_output_address_amount_flow, &ux_display_reject_step); // Finalize see the transaction fees and finally accept signing -// #1 screen: eye icon + "Confirm Transaction" +// #1 screen: eye icon + "Confirm transaction" // #2 screen: fee amount // #3 screen: "Accept and send", with approve button // #4 screen: reject button @@ -396,6 +397,17 @@ UX_FLOW(ux_accept_transaction_flow, &ux_accept_and_send_step, &ux_display_reject_step); +// Finalize see the transaction fees and finally accept signing +// #1 screen: eye icon + "Confirm self-transfer" +// #2 screen: fee amount +// #3 screen: "Accept and send", with approve button +// #4 screen: reject button +UX_FLOW(ux_accept_selftransfer_flow, + &ux_confirm_selftransfer_step, + &ux_confirm_transaction_fees_step, + &ux_accept_and_send_step, + &ux_display_reject_step); + void ui_display_pubkey_flow(void) { ux_flow_init(0, ux_display_pubkey_flow, NULL); } @@ -458,4 +470,8 @@ void ui_accept_transaction_flow(void) { ux_flow_init(0, ux_accept_transaction_flow, NULL); } +void ui_accept_selftransfer_flow(void) { + ux_flow_init(0, ux_accept_selftransfer_flow, NULL); +} + #endif // HAVE_BAGL diff --git a/src/ui/display_nbgl.c b/src/ui/display_nbgl.c index a02c2ee04..1a423c0f1 100644 --- a/src/ui/display_nbgl.c +++ b/src/ui/display_nbgl.c @@ -22,7 +22,8 @@ enum { CANCEL_TOKEN = 0, CONFIRM_TOKEN, SILENT_CONFIRM_TOKEN, - BACK_TOKEN, + BACK_TOKEN_TRANSACTION, // for most transactions + BACK_TOKEN_SELFTRANSFER, // special case when it's a self-transfer (no external outputs) }; extern bool G_was_processing_screen_shown; @@ -91,9 +92,12 @@ static void transaction_confirm_callback(int token, uint8_t index) { case SILENT_CONFIRM_TOKEN: ux_flow_response(true); break; - case BACK_TOKEN: + case BACK_TOKEN_TRANSACTION: ui_accept_transaction_flow(); break; + case BACK_TOKEN_SELFTRANSFER: + ui_accept_selftransfer_flow(); + break; default: PRINTF("Unhandled token : %d", token); } @@ -149,13 +153,17 @@ static void continue_callback(void) { static void transaction_confirm(int token, uint8_t index) { (void) index; + // If it's a self-transfer, the UX is slightly different + int backToken = + transactionContext.extOutputCount == 0 ? BACK_TOKEN_SELFTRANSFER : BACK_TOKEN_TRANSACTION; + if (token == CONFIRM_TOKEN) { nbgl_pageNavigationInfo_t info = {.activePage = transactionContext.extOutputCount + 1, .nbPages = transactionContext.extOutputCount + 2, .navType = NAV_WITH_TAP, .progressIndicator = true, .navWithTap.backButton = true, - .navWithTap.backToken = BACK_TOKEN, + .navWithTap.backToken = backToken, .navWithTap.nextPageText = NULL, .navWithTap.quitText = "Reject transaction", .quitToken = CANCEL_TOKEN, @@ -204,6 +212,37 @@ void ui_accept_transaction_flow(void) { nbgl_refresh(); } +void ui_accept_selftransfer_flow(void) { + transactionContext.tagValuePair[0].item = "Amount"; + transactionContext.tagValuePair[0].value = "Self-transfer"; + transactionContext.tagValuePair[1].item = "Fees"; + transactionContext.tagValuePair[1].value = g_ui_state.validate_transaction.fee; + + transactionContext.tagValueList.nbPairs = 2; + + transactionContext.confirm = "Sign transaction\nto send Bitcoin?"; + transactionContext.confirmed_status = "TRANSACTION\nSIGNED"; + transactionContext.rejected_status = "Transaction rejected"; + + nbgl_pageNavigationInfo_t info = {.activePage = transactionContext.extOutputCount, + .nbPages = transactionContext.extOutputCount + 2, + .navType = NAV_WITH_TAP, + .progressIndicator = true, + .navWithTap.backButton = false, + .navWithTap.nextPageText = "Tap to continue", + .navWithTap.nextPageToken = CONFIRM_TOKEN, + .navWithTap.quitText = "Reject transaction", + .quitToken = CANCEL_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; + + nbgl_pageContent_t content = {.type = TAG_VALUE_LIST, + .tagValueList.nbPairs = transactionContext.tagValueList.nbPairs, + .tagValueList.pairs = transactionContext.tagValuePair}; + + nbgl_pageDrawGenericContent(&transaction_confirm, &info, &content); + nbgl_refresh(); +} + void ui_display_transaction_prompt(const int external_outputs_total_count) { transactionContext.currentOutput = 0; transactionContext.extOutputCount = external_outputs_total_count; diff --git a/tests/test_sign_psbt.py b/tests/test_sign_psbt.py index 8f9ab440f..4eb8aef61 100644 --- a/tests/test_sign_psbt.py +++ b/tests/test_sign_psbt.py @@ -335,6 +335,25 @@ def test_sign_psbt_singlesig_wpkh_2to2_missing_nonwitnessutxo(client: Client): )] +@has_automation("automations/sign_with_default_wallet_accept.json") +def test_sign_psbt_singlesig_wpkh_selftransfer(client: Client): + # The only output is a change output. + # A "self-transfer" screen should be shown before the fees. + + wallet = WalletPolicy( + "", + "wpkh(@0/**)", + [ + "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P" + ], + ) + + psbt = "cHNidP8BAHECAAAAAfcDVJxLN1tzz5vaIy2onFL/ht/OqwKm2jEWGwMNDE/cAQAAAAD9////As0qAAAAAAAAFgAUJfcXOL7SoYGoDC1n6egGa0OTD9/mtgEAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsTTQlAAABAPYCAAAAAAEBCOcYS1aMP1uQcUKTMJbvlsZXsV4yNnVxynyMfxSX//UAAAAAFxYAFGEWho6AN6qeux0gU3BSWnK+Dw4D/f///wKfJwEAAAAAABepFG1IUtrzpUCfdyFtu46j1ZIxLX7ph0DiAQAAAAAAFgAU4e5IJz0XxNe96ANYDugMQ34E0/cCRzBEAiB1b84pX0QaOUrvCdDxKeB+idM6wYKTLGmqnUU/tL8/lQIgbSinpq4jBlo+SIGyh8XNVrWAeMlKBNmoLenKOBugKzcBIQKXsd8NwO+9naIfeI3nkgYjg6g3QZarGTRDs7SNVZfGPJBJJAABAR9A4gEAAAAAABYAFOHuSCc9F8TXvegDWA7oDEN+BNP3IgYCgffBheEUZI8iAFFfv7b+HNM7j4jolv6lj5/n3j68h3kY9azC/VQAAIABAACAAAAAgAAAAAAHAAAAACICAzQZjNnkwXFEhm1F6oC2nk1ADqH6t/RHBAOblLA4tV5BGPWswv1UAACAAQAAgAAAAIABAAAAEgAAAAAiAgJxtbd5rYcIOFh3l7z28MeuxavnanCdck9I0uJs+HTwoBj1rML9VAAAgAEAAIAAAACAAQAAAAAAAAAA" + result = client.sign_psbt(psbt, wallet, None) + + assert len(result) == 1 + + # def test_sign_psbt_legacy(client: Client): # # legacy address # # PSBT for a legacy 1-input 1-output spend