#include #include #include "shared_context.h" #include "ui_callbacks.h" #include "ui_nbgl.h" #include "ui_signing.h" #include "plugins.h" #include "domain_name.h" #include "caller_api.h" #include "network_icons.h" #include "network.h" #define TEXT_TX "transaction" // 1 more than actually displayed on screen, because of calculations in StaticReview #define MAX_PLUGIN_ITEMS_PER_SCREEN 4 #define TAG_MAX_LEN 43 #define VALUE_MAX_LEN 79 enum { REJECT_TOKEN, START_REVIEW_TOKEN, }; static nbgl_layoutTagValue_t pair; // these buffers are used as circular static char title_buffer[MAX_PLUGIN_ITEMS_PER_SCREEN][TAG_MAX_LEN]; static char msg_buffer[MAX_PLUGIN_ITEMS_PER_SCREEN][VALUE_MAX_LEN]; static nbgl_layoutTagValueList_t useCaseTagValueList; static nbgl_pageInfoLongPress_t infoLongPress; struct tx_approval_context_t { bool fromPlugin; bool blindSigning; bool displayNetwork; #ifdef HAVE_DOMAIN_NAME bool domain_name_match; #endif }; static struct tx_approval_context_t tx_approval_context; static void reviewContinueCommon(void); static void reviewReject(void) { io_seproxyhal_touch_tx_cancel(NULL); memset(&tx_approval_context, 0, sizeof(tx_approval_context)); } static void confirmTransation(void) { io_seproxyhal_touch_tx_ok(NULL); memset(&tx_approval_context, 0, sizeof(tx_approval_context)); } static void onConfirmAbandon(void) { nbgl_useCaseStatus("Transaction rejected", false, reviewReject); } static void rejectTransactionQuestion(void) { nbgl_useCaseConfirm(REJECT_QUESTION(TEXT_TX), NULL, REJECT_CONFIRM_BUTTON, RESUME(TEXT_TX), onConfirmAbandon); } static void reviewChoice(bool confirm) { if (confirm) { nbgl_useCaseStatus("TRANSACTION\nSIGNED", true, confirmTransation); } else { rejectTransactionQuestion(); } } // called by NBGL to get the tag/value pair corresponding to pairIndex static nbgl_layoutTagValue_t *getTagValuePair(uint8_t pairIndex) { static int counter = 0; if (tx_approval_context.fromPlugin) { if (pairIndex < dataContext.tokenContext.pluginUiMaxItems) { // for the next dataContext.tokenContext.pluginUiMaxItems items, get tag/value from // plugin_ui_get_item_internal() dataContext.tokenContext.pluginUiCurrentItem = pairIndex; plugin_ui_get_item_internal((uint8_t *) title_buffer[counter], TAG_MAX_LEN, (uint8_t *) msg_buffer[counter], VALUE_MAX_LEN); pair.item = title_buffer[counter]; pair.value = msg_buffer[counter]; } else { pairIndex -= dataContext.tokenContext.pluginUiMaxItems; // for the last 1 (or 2), tags are fixed if (tx_approval_context.displayNetwork && (pairIndex == 0)) { pair.item = "Network"; pair.value = strings.common.network_name; } else { pair.item = "Max fees"; pair.value = strings.common.maxFee; } } } else { uint8_t target_index = 0; if (pairIndex == target_index++) { pair.item = "Amount"; pair.value = strings.common.fullAmount; } #ifdef HAVE_DOMAIN_NAME if (tx_approval_context.domain_name_match) { if (pairIndex == target_index++) { pair.item = "Domain"; pair.value = g_domain_name; } } if (!tx_approval_context.domain_name_match || N_storage.verbose_domain_name) { #endif // HAVE_DOMAIN_NAME if (pairIndex == target_index++) { pair.item = "Address"; pair.value = strings.common.fullAddress; } #ifdef HAVE_DOMAIN_NAME } #endif // HAVE_DOMAIN_NAME if (N_storage.displayNonce) { if (pairIndex == target_index++) { pair.item = "Nonce"; pair.value = strings.common.nonce; } } if (pairIndex == target_index++) { pair.item = "Max fees"; pair.value = strings.common.maxFee; } if (pairIndex == target_index++) { pair.item = "Network"; pair.value = strings.common.network_name; } } // counter is used as index to circular buffer counter++; if (counter == MAX_PLUGIN_ITEMS_PER_SCREEN) { counter = 0; } return &pair; } static void pageCallback(int token, uint8_t index) { (void) index; nbgl_pageRelease(pageContext); if (token == REJECT_TOKEN) { reviewReject(); } else if (token == START_REVIEW_TOKEN) { reviewContinueCommon(); } } static void reviewContinue(void) { if (tx_approval_context.blindSigning) { nbgl_pageInfoDescription_t info = { .centeredInfo.icon = &C_round_warning_64px, .centeredInfo.text1 = "Blind Signing", .centeredInfo.text2 = "This transaction cannot be\nsecurely interpreted by Ledger\nStax. It might put " "your assets\nat risk.", .centeredInfo.text3 = NULL, .centeredInfo.style = LARGE_CASE_INFO, .centeredInfo.offsetY = -32, .footerText = REJECT(TEXT_TX), .footerToken = REJECT_TOKEN, .tapActionText = "Tap to continue", .tapActionToken = START_REVIEW_TOKEN, .topRightStyle = NO_BUTTON_STYLE, .actionButtonText = NULL, .tuneId = TUNE_TAP_CASUAL}; if (pageContext != NULL) { nbgl_pageRelease(pageContext); pageContext = NULL; } pageContext = nbgl_pageDrawInfo(&pageCallback, NULL, &info); } else { reviewContinueCommon(); } } static const nbgl_icon_details_t *get_tx_icon(void) { const nbgl_icon_details_t *icon = NULL; if (tx_approval_context.fromPlugin && (pluginType == EXTERNAL)) { if (caller_app && caller_app->name) { if ((strlen(strings.common.fullAddress) == strlen(caller_app->name)) && (strcmp(strings.common.fullAddress, caller_app->name) == 0)) { icon = get_app_icon(true); } } } else { uint64_t chain_id = get_tx_chain_id(); if (chain_id == chainConfig->chainId) { icon = get_app_icon(false); } else { icon = get_network_icon_from_chain_id(&chain_id); } } return icon; } static void reviewContinueCommon(void) { uint8_t nbPairs = 0; if (tx_approval_context.fromPlugin) { // plugin id + max items + fees nbPairs += dataContext.tokenContext.pluginUiMaxItems + 1; } else { nbPairs += 3; if (N_storage.displayNonce) { nbPairs++; } #ifdef HAVE_DOMAIN_NAME uint64_t chain_id = get_tx_chain_id(); tx_approval_context.domain_name_match = has_domain_name(&chain_id, tmpContent.txContent.destination); if (tx_approval_context.domain_name_match && N_storage.verbose_domain_name) { nbPairs += 1; } #endif // HAVE_DOMAIN_NAME } if (tx_approval_context.displayNetwork) { nbPairs++; } useCaseTagValueList.pairs = NULL; useCaseTagValueList.callback = getTagValuePair; useCaseTagValueList.startIndex = 0; useCaseTagValueList.nbPairs = nbPairs; ///< number of pairs in pairs array useCaseTagValueList.smallCaseForValue = false; useCaseTagValueList.wrapping = false; infoLongPress.icon = get_tx_icon(); infoLongPress.text = tx_approval_context.fromPlugin ? g_stax_shared_buffer : SIGN(TEXT_TX); infoLongPress.longPressText = SIGN_BUTTON; nbgl_useCaseStaticReview(&useCaseTagValueList, &infoLongPress, REJECT(TEXT_TX), reviewChoice); } // Replace "Review" by "Sign" and add questionmark static void prepare_sign_text(void) { uint8_t sign_length = strlen("Sign"); uint8_t review_length = strlen("Review"); memmove(g_stax_shared_buffer, "Sign", sign_length); memmove(g_stax_shared_buffer + sign_length, g_stax_shared_buffer + review_length, strlen(g_stax_shared_buffer) - review_length + 1); strlcat(g_stax_shared_buffer, "?", sizeof(g_stax_shared_buffer)); } // Force operation to be lowercase static void get_lowercase_operation(char *dst, size_t dst_len) { const char *src = strings.common.fullAmount; size_t idx; for (idx = 0; (idx < dst_len - 1) && (src[idx] != '\0'); ++idx) { dst[idx] = (char) tolower((int) src[idx]); } dst[idx] = '\0'; } static void buildFirstPage(void) { if (tx_approval_context.fromPlugin) { char op_name[sizeof(strings.common.fullAmount)]; plugin_ui_get_id(); get_lowercase_operation(op_name, sizeof(op_name)); if (pluginType == EXTERNAL) { snprintf(g_stax_shared_buffer, sizeof(g_stax_shared_buffer), "Review transaction\nto %s\non %s", op_name, strings.common.fullAddress); } else { snprintf(g_stax_shared_buffer, sizeof(g_stax_shared_buffer), "Review transaction\nto %s\n%s", op_name, strings.common.fullAddress); } nbgl_useCaseReviewStart(get_tx_icon(), g_stax_shared_buffer, NULL, REJECT(TEXT_TX), reviewContinue, rejectTransactionQuestion); prepare_sign_text(); } else { nbgl_useCaseReviewStart(get_tx_icon(), REVIEW(TEXT_TX), NULL, REJECT(TEXT_TX), reviewContinue, rejectTransactionQuestion); } } void ux_approve_tx(bool fromPlugin) { tx_approval_context.blindSigning = !fromPlugin && tmpContent.txContent.dataPresent && !N_storage.contractDetails; tx_approval_context.fromPlugin = fromPlugin; tx_approval_context.displayNetwork = false; uint64_t chain_id = get_tx_chain_id(); if (chainConfig->chainId == ETHEREUM_MAINNET_CHAINID && chain_id != chainConfig->chainId) { tx_approval_context.displayNetwork = true; } buildFirstPage(); }