From 91d1879f15c099cf0d389af722bc93ee58ed6236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 20 Jan 2026 23:06:24 -0500 Subject: [PATCH 1/7] Polish transaction confirmation UX - Redesign transaction confirmation screen with cleaner layout - Add UX test RPC command for rapid iteration - Integrate confirmation dialog into frost_sign flow - Add format helpers for sats and address display - Improve button styling with better press states --- main/frost_signer.c | 46 +++++ main/main.c | 48 +++++ main/protocol.c | 2 + main/protocol.h | 1 + main/ux_display.c | 426 +++++++++++++++++++++++++++++++------------- 5 files changed, 401 insertions(+), 122 deletions(-) diff --git a/main/frost_signer.c b/main/frost_signer.c index 865d4a1..0ebe37e 100644 --- a/main/frost_signer.c +++ b/main/frost_signer.c @@ -13,11 +13,14 @@ #include "crypto_asm.h" #include "secresult.h" #include "anti_glitch.h" +#include "ux_interface.h" #include "esp_log.h" #include #include #ifdef ESP_PLATFORM +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "esp_timer.h" static uint32_t get_time_ms(void) { return (uint32_t)(esp_timer_get_time() / 1000); @@ -39,11 +42,25 @@ static uint32_t elapsed_ms(uint32_t start, uint32_t now) { #define TAG "frost_signer" #define MAX_SESSIONS 4 #define CONSUMED_SESSION_RING_SIZE 64 +#define CONFIRM_TIMEOUT_MS 35000 static uint8_t consumed_sessions[CONSUMED_SESSION_RING_SIZE][SESSION_ID_LEN]; static uint8_t consumed_count = 0; static uint8_t consumed_head = 0; +#ifdef ESP_PLATFORM +static SemaphoreHandle_t confirm_sem = NULL; +static bool confirm_result = false; + +static void confirm_cb(bool approved, void *user_data) { + (void)user_data; + confirm_result = approved; + if (confirm_sem) { + xSemaphoreGive(confirm_sem); + } +} +#endif + static bool is_session_consumed(const uint8_t *session_id) { uint8_t count = (consumed_count < CONSUMED_SESSION_RING_SIZE) ? consumed_count : CONSUMED_SESSION_RING_SIZE; @@ -420,6 +437,35 @@ void frost_sign(const char *group, const char *session_id_hex, const char *commi ag_random_delay_us(100, 1000); +#ifdef ESP_PLATFORM + const ux_backend_t *ux = ux_get_backend(); + if (ux && ux->confirm_transaction && ux->is_available && ux->is_available()) { + if (!confirm_sem) { + confirm_sem = xSemaphoreCreateBinary(); + } + if (confirm_sem) { + ux_tx_info_t tx_info = {0}; + strncpy(tx_info.destination, s->group, sizeof(tx_info.destination) - 1); + tx_info.threshold = s->frost_state.threshold; + tx_info.total_signers = s->frost_state.participants; + tx_info.policy_approved = true; + + confirm_result = false; + ux->confirm_transaction(&tx_info, confirm_cb, NULL); + + if (xSemaphoreTake(confirm_sem, pdMS_TO_TICKS(CONFIRM_TIMEOUT_MS)) != pdTRUE) { + PROTOCOL_ERROR(resp, resp->id, PROTOCOL_ERR_SIGN, "Confirmation timeout"); + return; + } + + if (!confirm_result) { + PROTOCOL_ERROR(resp, resp->id, PROTOCOL_ERR_SIGN, "User rejected signing"); + return; + } + } + } +#endif + bool policy_snapshot = s->has_policy; uint8_t policy_hash_snapshot[32]; memcpy(policy_hash_snapshot, s->policy_hash, 32); diff --git a/main/main.c b/main/main.c index ac2463d..fb54c12 100644 --- a/main/main.c +++ b/main/main.c @@ -298,6 +298,51 @@ static void handle_dkg_checkpoint(const rpc_request_t *req, rpc_response_t *resp protocol_success(resp, req->id, "{\"ok\":true}"); } +static SemaphoreHandle_t ux_test_sem = NULL; +static bool ux_test_result = false; + +static void ux_test_cb(bool approved, void *user_data) { + (void)user_data; + ux_test_result = approved; + if (ux_test_sem) { + xSemaphoreGive(ux_test_sem); + } +} + +static void handle_ux_test(const rpc_request_t *req, rpc_response_t *resp) { + const ux_backend_t *ux = ux_get_backend(); + if (!ux || !ux->confirm_transaction) { + PROTOCOL_ERROR(resp, req->id, -1, "No UX backend available"); + return; + } + + if (!ux_test_sem) { + ux_test_sem = xSemaphoreCreateBinary(); + } + + ux_tx_info_t tx_info = { + .amount_sats = 50000, + .fee_sats = 1200, + .threshold = 2, + .total_signers = 3, + .policy_approved = true, + }; + strncpy(tx_info.destination, "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", sizeof(tx_info.destination) - 1); + strncpy(tx_info.destination_label, "Savings", sizeof(tx_info.destination_label) - 1); + + ux_test_result = false; + ux->confirm_transaction(&tx_info, ux_test_cb, NULL); + + if (xSemaphoreTake(ux_test_sem, pdMS_TO_TICKS(35000)) != pdTRUE) { + protocol_success(resp, req->id, "{\"result\":\"timeout\"}"); + return; + } + + char result[64]; + snprintf(result, sizeof(result), "{\"result\":\"%s\"}", ux_test_result ? "approved" : "rejected"); + protocol_success(resp, req->id, result); +} + static void handle_request(const rpc_request_t *req, rpc_response_t *resp) { resp->id = req->id; frost_signer_cleanup_stale(); @@ -378,6 +423,9 @@ static void handle_request(const rpc_request_t *req, rpc_response_t *resp) { case RPC_METHOD_SESSION_LIST: frost_session_list(resp); break; + case RPC_METHOD_UX_TEST: + handle_ux_test(req, resp); + break; default: PROTOCOL_ERROR(resp, req->id, PROTOCOL_ERR_METHOD, "Method not found"); } diff --git a/main/protocol.c b/main/protocol.c index 0a35ce4..d8f1533 100644 --- a/main/protocol.c +++ b/main/protocol.c @@ -85,6 +85,8 @@ static rpc_method_t parse_method(const char *method) { return RPC_METHOD_SESSION_RESUME; if (strcmp(method, "frost_session_list") == 0) return RPC_METHOD_SESSION_LIST; + if (strcmp(method, "ux_test") == 0) + return RPC_METHOD_UX_TEST; return RPC_METHOD_UNKNOWN; } diff --git a/main/protocol.h b/main/protocol.h index a9f05ac..1e65362 100644 --- a/main/protocol.h +++ b/main/protocol.h @@ -46,6 +46,7 @@ typedef enum { RPC_METHOD_EXPORT_SHARE, RPC_METHOD_SESSION_RESUME, RPC_METHOD_SESSION_LIST, + RPC_METHOD_UX_TEST, RPC_METHOD_UNKNOWN } rpc_method_t; diff --git a/main/ux_display.c b/main/ux_display.c index ae586f6..974ee2d 100644 --- a/main/ux_display.c +++ b/main/ux_display.c @@ -13,17 +13,18 @@ #define TAG "ux_display" -#define SCREEN_WIDTH 320 -#define SCREEN_HEIGHT 240 - -#define COLOR_BG lv_color_hex(0x0d1117) -#define COLOR_SURFACE lv_color_hex(0x161b22) -#define COLOR_ACCENT lv_color_hex(0x58a6ff) -#define COLOR_DANGER lv_color_hex(0xf85149) -#define COLOR_SUCCESS lv_color_hex(0x3fb950) -#define COLOR_WARNING lv_color_hex(0xd29922) -#define COLOR_TEXT lv_color_hex(0xf0f6fc) -#define COLOR_MUTED lv_color_hex(0x8b949e) +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 240 +#define CONFIRM_TIMEOUT_SEC 30 + +#define COLOR_BG lv_color_hex(0x000000) +#define COLOR_SURFACE lv_color_hex(0x1c1c1e) +#define COLOR_ACCENT lv_color_hex(0x0a84ff) +#define COLOR_DANGER lv_color_hex(0xff453a) +#define COLOR_SUCCESS lv_color_hex(0x30d158) +#define COLOR_WARNING lv_color_hex(0xffd60a) +#define COLOR_TEXT lv_color_hex(0xffffff) +#define COLOR_MUTED lv_color_hex(0x8e8e93) static ui_state_t current_state = UI_STATE_IDLE; static ux_decision_cb_t pending_callback = NULL; @@ -31,6 +32,10 @@ static void *pending_user_data = NULL; static lv_obj_t *current_screen = NULL; static lv_obj_t *signing_bar = NULL; static lv_obj_t *signing_label = NULL; +static lv_timer_t *confirm_timer = NULL; +static int timeout_remaining = 0; +static lv_obj_t *timeout_arc = NULL; +static lv_obj_t *timeout_label = NULL; static void create_idle_screen(const char *device_name, bool policy_loaded, uint32_t policy_version); @@ -66,7 +71,54 @@ static void display_deinit(void) { bsp_display_backlight_off(); } +static void stop_confirm_timer(void) { + if (confirm_timer) { + lv_timer_del(confirm_timer); + confirm_timer = NULL; + } + timeout_remaining = 0; + timeout_arc = NULL; + timeout_label = NULL; +} + +static void invoke_pending_callback(bool approved); + +static lv_color_t get_timeout_color(int remaining) { + if (remaining <= 5) { + return COLOR_DANGER; + } else if (remaining <= 10) { + return COLOR_WARNING; + } + return COLOR_ACCENT; +} + +static void confirm_timer_cb(lv_timer_t *timer) { + (void)timer; + if (timeout_remaining > 0) { + timeout_remaining--; + + if (timeout_arc) { + int angle = (timeout_remaining * 360) / CONFIRM_TIMEOUT_SEC; + lv_arc_set_angles(timeout_arc, 270, 270 + angle); + lv_obj_set_style_arc_color(timeout_arc, get_timeout_color(timeout_remaining), + LV_PART_INDICATOR); + } + + if (timeout_label) { + char timeout_str[8]; + snprintf(timeout_str, sizeof(timeout_str), "%d", timeout_remaining); + lv_label_set_text(timeout_label, timeout_str); + lv_obj_set_style_text_color(timeout_label, get_timeout_color(timeout_remaining), 0); + } + + if (timeout_remaining == 0) { + invoke_pending_callback(false); + } + } +} + static void clear_screen(void) { + stop_confirm_timer(); pending_callback = NULL; pending_user_data = NULL; @@ -138,6 +190,7 @@ static void display_show_error(const char *title, const char *message) { } static void invoke_pending_callback(bool approved) { + stop_confirm_timer(); if (!pending_callback) { return; } @@ -166,6 +219,10 @@ static void display_confirm_transaction(const ux_tx_info_t *tx, ux_decision_cb_t pending_user_data = user_data; create_transaction_screen(tx); current_state = UI_STATE_CONFIRM_TX; + + timeout_remaining = CONFIRM_TIMEOUT_SEC; + confirm_timer = lv_timer_create(confirm_timer_cb, 1000, NULL); + bsp_display_unlock(); } @@ -299,7 +356,173 @@ static void create_scanning_screen(void) { lv_obj_align(hint, LV_ALIGN_CENTER, 0, 70); } +static void create_sign_request_screen(const ux_tx_info_t *tx) { + current_screen = lv_obj_create(lv_scr_act()); + lv_obj_set_size(current_screen, SCREEN_WIDTH, SCREEN_HEIGHT); + lv_obj_set_style_bg_color(current_screen, COLOR_BG, 0); + lv_obj_set_style_pad_all(current_screen, 0, 0); + lv_obj_set_style_border_width(current_screen, 0, 0); + lv_obj_center(current_screen); + + lv_obj_t *title = lv_label_create(current_screen); + lv_label_set_text(title, "Sign Request"); + lv_obj_set_style_text_color(title, COLOR_MUTED, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_12, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16); + + lv_obj_t *wallet_label = lv_label_create(current_screen); + lv_label_set_text(wallet_label, tx->destination[0] ? tx->destination : "Unknown"); + lv_obj_set_style_text_color(wallet_label, COLOR_TEXT, 0); + lv_obj_set_style_text_font(wallet_label, &lv_font_montserrat_24, 0); + lv_obj_align(wallet_label, LV_ALIGN_TOP_MID, 0, 36); + + if (tx->threshold > 0) { + char frost_str[24]; + snprintf(frost_str, sizeof(frost_str), "%d-of-%d multisig", tx->threshold, tx->total_signers); + lv_obj_t *frost_label = lv_label_create(current_screen); + lv_label_set_text(frost_label, frost_str); + lv_obj_set_style_text_color(frost_label, COLOR_MUTED, 0); + lv_obj_set_style_text_font(frost_label, &lv_font_montserrat_12, 0); + lv_obj_align(frost_label, LV_ALIGN_TOP_MID, 0, 68); + } + + lv_obj_t *warn_box = lv_obj_create(current_screen); + lv_obj_set_size(warn_box, 280, 44); + lv_obj_set_style_bg_color(warn_box, lv_color_hex(0x1a1a00), 0); + lv_obj_set_style_border_color(warn_box, COLOR_WARNING, 0); + lv_obj_set_style_border_width(warn_box, 1, 0); + lv_obj_set_style_radius(warn_box, 12, 0); + lv_obj_set_style_pad_all(warn_box, 8, 0); + lv_obj_align(warn_box, LV_ALIGN_CENTER, 0, -10); + + lv_obj_t *warn_icon = lv_label_create(warn_box); + lv_label_set_text(warn_icon, LV_SYMBOL_WARNING); + lv_obj_set_style_text_color(warn_icon, COLOR_WARNING, 0); + lv_obj_align(warn_icon, LV_ALIGN_LEFT_MID, 4, 0); + + lv_obj_t *warn_text = lv_label_create(warn_box); + lv_label_set_text(warn_text, "Verify details on coordinator"); + lv_obj_set_style_text_color(warn_text, COLOR_WARNING, 0); + lv_obj_set_style_text_font(warn_text, &lv_font_montserrat_12, 0); + lv_obj_align(warn_text, LV_ALIGN_LEFT_MID, 28, 0); + + timeout_arc = lv_arc_create(current_screen); + lv_obj_set_size(timeout_arc, 50, 50); + lv_arc_set_rotation(timeout_arc, 270); + lv_arc_set_bg_angles(timeout_arc, 0, 360); + lv_arc_set_angles(timeout_arc, 270, 270 + 360); + lv_obj_set_style_arc_width(timeout_arc, 4, LV_PART_MAIN); + lv_obj_set_style_arc_width(timeout_arc, 4, LV_PART_INDICATOR); + lv_obj_set_style_arc_color(timeout_arc, COLOR_SURFACE, LV_PART_MAIN); + lv_obj_set_style_arc_color(timeout_arc, COLOR_ACCENT, LV_PART_INDICATOR); + lv_obj_remove_style(timeout_arc, NULL, LV_PART_KNOB); + lv_obj_remove_flag(timeout_arc, LV_OBJ_FLAG_CLICKABLE); + lv_obj_align(timeout_arc, LV_ALIGN_TOP_RIGHT, -12, 12); + + char timeout_str[8]; + snprintf(timeout_str, sizeof(timeout_str), "%d", CONFIRM_TIMEOUT_SEC); + timeout_label = lv_label_create(current_screen); + lv_label_set_text(timeout_label, timeout_str); + lv_obj_set_style_text_color(timeout_label, COLOR_ACCENT, 0); + lv_obj_set_style_text_font(timeout_label, &lv_font_montserrat_12, 0); + lv_obj_align_to(timeout_label, timeout_arc, LV_ALIGN_CENTER, 0, 0); + + lv_obj_t *reject_btn = lv_btn_create(current_screen); + lv_obj_set_size(reject_btn, 140, 52); + lv_obj_align(reject_btn, LV_ALIGN_BOTTOM_LEFT, 12, -12); + lv_obj_set_style_bg_color(reject_btn, COLOR_SURFACE, 0); + lv_obj_set_style_radius(reject_btn, 26, 0); + lv_obj_add_event_cb(reject_btn, reject_btn_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *reject_icon = lv_label_create(reject_btn); + lv_label_set_text(reject_icon, LV_SYMBOL_CLOSE); + lv_obj_set_style_text_color(reject_icon, COLOR_DANGER, 0); + lv_obj_set_style_text_font(reject_icon, &lv_font_montserrat_16, 0); + lv_obj_align(reject_icon, LV_ALIGN_LEFT_MID, 16, 0); + + lv_obj_t *reject_label = lv_label_create(reject_btn); + lv_label_set_text(reject_label, "Reject"); + lv_obj_set_style_text_color(reject_label, COLOR_TEXT, 0); + lv_obj_set_style_text_font(reject_label, &lv_font_montserrat_14, 0); + lv_obj_align(reject_label, LV_ALIGN_LEFT_MID, 40, 0); + + lv_obj_t *approve_btn = lv_btn_create(current_screen); + lv_obj_set_size(approve_btn, 140, 52); + lv_obj_align(approve_btn, LV_ALIGN_BOTTOM_RIGHT, -12, -12); + lv_obj_set_style_bg_color(approve_btn, COLOR_SUCCESS, 0); + lv_obj_set_style_radius(approve_btn, 26, 0); + lv_obj_add_event_cb(approve_btn, approve_btn_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *approve_icon = lv_label_create(approve_btn); + lv_label_set_text(approve_icon, LV_SYMBOL_OK); + lv_obj_set_style_text_color(approve_icon, COLOR_TEXT, 0); + lv_obj_set_style_text_font(approve_icon, &lv_font_montserrat_16, 0); + lv_obj_align(approve_icon, LV_ALIGN_LEFT_MID, 12, 0); + + lv_obj_t *approve_label = lv_label_create(approve_btn); + lv_label_set_text(approve_label, "Approve"); + lv_obj_set_style_text_color(approve_label, COLOR_TEXT, 0); + lv_obj_set_style_text_font(approve_label, &lv_font_montserrat_14, 0); + lv_obj_align(approve_label, LV_ALIGN_LEFT_MID, 36, 0); +} + +static void format_address(const char *addr, char *out, size_t out_len) { + size_t len = strlen(addr); + if (len <= 20) { + strncpy(out, addr, out_len - 1); + out[out_len - 1] = '\0'; + } else { + snprintf(out, out_len, "%.*s...%s", 10, addr, addr + len - 6); + } +} + +static void format_sats(uint64_t sats, char *out, size_t out_len) { + if (sats >= 100000000) { + double btc = sats / 100000000.0; + if (btc >= 1.0) { + snprintf(out, out_len, "%.4f", btc); + } else { + snprintf(out, out_len, "%.6f", btc); + } + } else if (sats >= 1000000) { + snprintf(out, out_len, "%.2fM", sats / 1000000.0); + } else if (sats >= 1000) { + snprintf(out, out_len, "%llu,%03llu", + (unsigned long long)(sats / 1000), + (unsigned long long)(sats % 1000)); + } else { + snprintf(out, out_len, "%llu", (unsigned long long)sats); + } +} + +static lv_obj_t *create_action_btn(lv_obj_t *parent, const char *text, lv_color_t bg_color, + lv_color_t text_color, lv_event_cb_t click_cb) { + lv_obj_t *btn = lv_btn_create(parent); + lv_obj_set_size(btn, 145, 56); + lv_obj_set_style_bg_color(btn, bg_color, 0); + lv_obj_set_style_bg_color(btn, lv_color_darken(bg_color, 40), LV_STATE_PRESSED); + lv_obj_set_style_radius(btn, 28, 0); + lv_obj_set_style_shadow_width(btn, 0, 0); + lv_obj_set_style_border_width(btn, 0, 0); + lv_obj_add_event_cb(btn, click_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *label = lv_label_create(btn); + lv_label_set_text(label, text); + lv_obj_set_style_text_color(label, text_color, 0); + lv_obj_set_style_text_font(label, &lv_font_montserrat_16, 0); + lv_obj_center(label); + + return btn; +} + static void create_transaction_screen(const ux_tx_info_t *tx) { + bool is_generic_sign = (tx->amount_sats == 0 && tx->fee_sats == 0); + + if (is_generic_sign) { + create_sign_request_screen(tx); + return; + } + current_screen = lv_obj_create(lv_scr_act()); lv_obj_set_size(current_screen, SCREEN_WIDTH, SCREEN_HEIGHT); lv_obj_set_style_bg_color(current_screen, COLOR_BG, 0); @@ -308,147 +531,106 @@ static void create_transaction_screen(const ux_tx_info_t *tx) { lv_obj_center(current_screen); bool high_fee = tx->amount_sats > 0 && tx->fee_sats > tx->amount_sats / 10; - int y_pos = 8; - lv_obj_t *title = lv_label_create(current_screen); - lv_label_set_text(title, "Confirm Transaction"); - lv_obj_set_style_text_color(title, COLOR_TEXT, 0); - lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, y_pos); - y_pos += 28; + timeout_arc = lv_arc_create(current_screen); + lv_obj_set_size(timeout_arc, 44, 44); + lv_arc_set_rotation(timeout_arc, 270); + lv_arc_set_bg_angles(timeout_arc, 0, 360); + lv_arc_set_angles(timeout_arc, 270, 270 + 360); + lv_obj_set_style_arc_width(timeout_arc, 3, LV_PART_MAIN); + lv_obj_set_style_arc_width(timeout_arc, 3, LV_PART_INDICATOR); + lv_obj_set_style_arc_color(timeout_arc, COLOR_SURFACE, LV_PART_MAIN); + lv_obj_set_style_arc_color(timeout_arc, COLOR_ACCENT, LV_PART_INDICATOR); + lv_obj_remove_style(timeout_arc, NULL, LV_PART_KNOB); + lv_obj_remove_flag(timeout_arc, LV_OBJ_FLAG_CLICKABLE); + lv_obj_align(timeout_arc, LV_ALIGN_TOP_RIGHT, -8, 8); + + char timeout_str[8]; + snprintf(timeout_str, sizeof(timeout_str), "%d", CONFIRM_TIMEOUT_SEC); + timeout_label = lv_label_create(current_screen); + lv_label_set_text(timeout_label, timeout_str); + lv_obj_set_style_text_color(timeout_label, COLOR_ACCENT, 0); + lv_obj_set_style_text_font(timeout_label, &lv_font_montserrat_12, 0); + lv_obj_align_to(timeout_label, timeout_arc, LV_ALIGN_CENTER, 0, 0); + + lv_obj_t *send_label = lv_label_create(current_screen); + lv_label_set_text(send_label, "Send"); + lv_obj_set_style_text_color(send_label, COLOR_MUTED, 0); + lv_obj_set_style_text_font(send_label, &lv_font_montserrat_14, 0); + lv_obj_align(send_label, LV_ALIGN_TOP_LEFT, 16, 16); char amount_str[32]; - if (tx->amount_sats >= 100000000) { - snprintf(amount_str, sizeof(amount_str), "%.8f BTC", tx->amount_sats / 100000000.0); - } else if (tx->amount_sats >= 1000000) { - snprintf(amount_str, sizeof(amount_str), "%.2f M sats", tx->amount_sats / 1000000.0); - } else if (tx->amount_sats >= 1000) { - snprintf(amount_str, sizeof(amount_str), "%.1f K sats", tx->amount_sats / 1000.0); - } else { - snprintf(amount_str, sizeof(amount_str), "%llu sats", (unsigned long long)tx->amount_sats); - } + format_sats(tx->amount_sats, amount_str, sizeof(amount_str)); lv_obj_t *amount_label = lv_label_create(current_screen); lv_label_set_text(amount_label, amount_str); lv_obj_set_style_text_color(amount_label, COLOR_TEXT, 0); - lv_obj_set_style_text_font(amount_label, &lv_font_montserrat_24, 0); - lv_obj_align(amount_label, LV_ALIGN_TOP_MID, 0, y_pos); - y_pos += 32; - - lv_obj_t *dest_box = lv_obj_create(current_screen); - lv_obj_set_size(dest_box, 300, tx->destination_label[0] ? 48 : 36); - lv_obj_set_style_bg_color(dest_box, COLOR_SURFACE, 0); - lv_obj_set_style_border_width(dest_box, 0, 0); - lv_obj_set_style_radius(dest_box, 6, 0); - lv_obj_set_style_pad_all(dest_box, 6, 0); - lv_obj_align(dest_box, LV_ALIGN_TOP_MID, 0, y_pos); - - lv_obj_t *to_label = lv_label_create(dest_box); + lv_obj_set_style_text_font(amount_label, &lv_font_montserrat_32, 0); + lv_obj_align(amount_label, LV_ALIGN_TOP_LEFT, 16, 36); + + lv_obj_t *unit_label = lv_label_create(current_screen); + lv_label_set_text(unit_label, tx->amount_sats >= 100000000 ? "BTC" : "sats"); + lv_obj_set_style_text_color(unit_label, COLOR_MUTED, 0); + lv_obj_set_style_text_font(unit_label, &lv_font_montserrat_14, 0); + lv_obj_align_to(unit_label, amount_label, LV_ALIGN_OUT_RIGHT_BOTTOM, 6, -2); + + lv_obj_t *to_label = lv_label_create(current_screen); lv_label_set_text(to_label, "To"); lv_obj_set_style_text_color(to_label, COLOR_MUTED, 0); lv_obj_set_style_text_font(to_label, &lv_font_montserrat_12, 0); - lv_obj_align(to_label, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_align(to_label, LV_ALIGN_TOP_LEFT, 16, 78); - lv_obj_t *dest_label = lv_label_create(dest_box); - lv_label_set_text(dest_label, tx->destination[0] ? tx->destination : "(none)"); + lv_obj_t *dest_label = lv_label_create(current_screen); + lv_label_set_text(dest_label, tx->destination[0] ? tx->destination : "Unknown"); lv_obj_set_style_text_color(dest_label, COLOR_TEXT, 0); lv_obj_set_style_text_font(dest_label, &lv_font_montserrat_12, 0); - lv_obj_set_width(dest_label, 260); + lv_obj_set_width(dest_label, 288); lv_label_set_long_mode(dest_label, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_align(dest_label, LV_ALIGN_TOP_LEFT, 20, 0); + lv_obj_align(dest_label, LV_ALIGN_TOP_LEFT, 16, 94); + int info_y = 114; if (tx->destination_label[0]) { - lv_obj_t *label_tag = lv_label_create(dest_box); + lv_obj_t *label_tag = lv_label_create(current_screen); lv_label_set_text(label_tag, tx->destination_label); lv_obj_set_style_text_color(label_tag, COLOR_SUCCESS, 0); lv_obj_set_style_text_font(label_tag, &lv_font_montserrat_12, 0); - lv_obj_align(label_tag, LV_ALIGN_BOTTOM_LEFT, 0, 0); + lv_obj_align(label_tag, LV_ALIGN_TOP_LEFT, 16, info_y); + info_y += 18; } - y_pos += (tx->destination_label[0] ? 52 : 40); - lv_obj_t *details_box = lv_obj_create(current_screen); - lv_obj_set_size(details_box, 300, 50); - lv_obj_set_style_bg_color(details_box, COLOR_SURFACE, 0); - lv_obj_set_style_border_width(details_box, 0, 0); - lv_obj_set_style_radius(details_box, 6, 0); - lv_obj_set_style_pad_all(details_box, 6, 0); - lv_obj_align(details_box, LV_ALIGN_TOP_MID, 0, y_pos); + lv_obj_t *info_bar = lv_obj_create(current_screen); + lv_obj_set_size(info_bar, 288, 32); + lv_obj_set_style_bg_color(info_bar, COLOR_SURFACE, 0); + lv_obj_set_style_border_width(info_bar, 0, 0); + lv_obj_set_style_radius(info_bar, 8, 0); + lv_obj_set_style_pad_all(info_bar, 8, 0); + lv_obj_align(info_bar, LV_ALIGN_TOP_LEFT, 16, info_y + 4); char fee_str[32]; - snprintf(fee_str, sizeof(fee_str), "%llu sats", (unsigned long long)tx->fee_sats); - lv_obj_t *fee_title = lv_label_create(details_box); - lv_label_set_text(fee_title, "Fee"); - lv_obj_set_style_text_color(fee_title, COLOR_MUTED, 0); - lv_obj_set_style_text_font(fee_title, &lv_font_montserrat_12, 0); - lv_obj_align(fee_title, LV_ALIGN_TOP_LEFT, 0, 0); - - lv_obj_t *fee_val = lv_label_create(details_box); - lv_label_set_text(fee_val, fee_str); - lv_obj_set_style_text_color(fee_val, high_fee ? COLOR_WARNING : COLOR_TEXT, 0); - lv_obj_set_style_text_font(fee_val, &lv_font_montserrat_12, 0); - lv_obj_align(fee_val, LV_ALIGN_TOP_LEFT, 30, 0); + snprintf(fee_str, sizeof(fee_str), "Fee: %llu sats", (unsigned long long)tx->fee_sats); + lv_obj_t *fee_label = lv_label_create(info_bar); + lv_label_set_text(fee_label, fee_str); + lv_obj_set_style_text_color(fee_label, high_fee ? COLOR_WARNING : COLOR_MUTED, 0); + lv_obj_set_style_text_font(fee_label, &lv_font_montserrat_12, 0); + lv_obj_align(fee_label, LV_ALIGN_LEFT_MID, 0, 0); if (tx->threshold > 0) { char frost_str[24]; snprintf(frost_str, sizeof(frost_str), "%d-of-%d", tx->threshold, tx->total_signers); - lv_obj_t *frost_title = lv_label_create(details_box); - lv_label_set_text(frost_title, "FROST"); - lv_obj_set_style_text_color(frost_title, COLOR_MUTED, 0); - lv_obj_set_style_text_font(frost_title, &lv_font_montserrat_12, 0); - lv_obj_align(frost_title, LV_ALIGN_TOP_RIGHT, -60, 0); - - lv_obj_t *frost_val = lv_label_create(details_box); - lv_label_set_text(frost_val, frost_str); - lv_obj_set_style_text_color(frost_val, COLOR_ACCENT, 0); - lv_obj_set_style_text_font(frost_val, &lv_font_montserrat_12, 0); - lv_obj_align(frost_val, LV_ALIGN_TOP_RIGHT, 0, 0); + lv_obj_t *frost_label = lv_label_create(info_bar); + lv_label_set_text(frost_label, frost_str); + lv_obj_set_style_text_color(frost_label, COLOR_ACCENT, 0); + lv_obj_set_style_text_font(frost_label, &lv_font_montserrat_12, 0); + lv_obj_align(frost_label, LV_ALIGN_RIGHT_MID, 0, 0); } - lv_obj_t *policy_ind = lv_label_create(details_box); - lv_label_set_text(policy_ind, tx->policy_approved ? "Policy OK" : "Policy denied"); - lv_obj_set_style_text_color(policy_ind, tx->policy_approved ? COLOR_SUCCESS : COLOR_DANGER, 0); - lv_obj_set_style_text_font(policy_ind, &lv_font_montserrat_12, 0); - lv_obj_align(policy_ind, LV_ALIGN_BOTTOM_LEFT, 0, 0); - - if (tx->is_external) { - lv_obj_t *ext_warn = lv_label_create(details_box); - lv_label_set_text(ext_warn, "External"); - lv_obj_set_style_text_color(ext_warn, COLOR_WARNING, 0); - lv_obj_set_style_text_font(ext_warn, &lv_font_montserrat_12, 0); - lv_obj_align(ext_warn, LV_ALIGN_BOTTOM_RIGHT, 0, 0); - } - y_pos += 54; - - if (high_fee) { - lv_obj_t *warn_label = lv_label_create(current_screen); - lv_label_set_text(warn_label, "High fee (>10%)"); - lv_obj_set_style_text_color(warn_label, COLOR_WARNING, 0); - lv_obj_set_style_text_font(warn_label, &lv_font_montserrat_12, 0); - lv_obj_align(warn_label, LV_ALIGN_TOP_MID, 0, y_pos); - } + lv_obj_t *reject_btn = create_action_btn(current_screen, "Reject", + COLOR_SURFACE, COLOR_DANGER, reject_btn_cb); + lv_obj_align(reject_btn, LV_ALIGN_BOTTOM_LEFT, 10, -10); - lv_obj_t *reject_btn = lv_btn_create(current_screen); - lv_obj_set_size(reject_btn, 145, 42); - lv_obj_align(reject_btn, LV_ALIGN_BOTTOM_LEFT, 10, -8); - lv_obj_set_style_bg_color(reject_btn, COLOR_SURFACE, 0); - lv_obj_set_style_radius(reject_btn, 6, 0); - lv_obj_add_event_cb(reject_btn, reject_btn_cb, LV_EVENT_CLICKED, NULL); - - lv_obj_t *reject_label = lv_label_create(reject_btn); - lv_label_set_text(reject_label, "Reject"); - lv_obj_set_style_text_color(reject_label, COLOR_DANGER, 0); - lv_obj_center(reject_label); - - lv_obj_t *approve_btn = lv_btn_create(current_screen); - lv_obj_set_size(approve_btn, 145, 42); - lv_obj_align(approve_btn, LV_ALIGN_BOTTOM_RIGHT, -10, -8); - lv_obj_set_style_bg_color(approve_btn, COLOR_SUCCESS, 0); - lv_obj_set_style_radius(approve_btn, 6, 0); - lv_obj_add_event_cb(approve_btn, approve_btn_cb, LV_EVENT_CLICKED, NULL); - - lv_obj_t *approve_label = lv_label_create(approve_btn); - lv_label_set_text(approve_label, "Approve"); - lv_obj_center(approve_label); + lv_obj_t *approve_btn = create_action_btn(current_screen, "Approve", + COLOR_SUCCESS, COLOR_TEXT, approve_btn_cb); + lv_obj_align(approve_btn, LV_ALIGN_BOTTOM_RIGHT, -10, -10); } static void create_signing_screen(int current, int total) { From 83583cf689db879bc394159d4261c0030a356758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 20 Jan 2026 23:07:42 -0500 Subject: [PATCH 2/7] Add device test script for UX iteration --- tools/test_device.py | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tools/test_device.py diff --git a/tools/test_device.py b/tools/test_device.py new file mode 100644 index 0000000..018213b --- /dev/null +++ b/tools/test_device.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +import serial +import json +import time +import sys + +DEVICE = "/dev/ttyACM0" +BAUD = 115200 + +request_id = 0 + +def send_command(ser, method, params=None, timeout=5): + global request_id + request_id += 1 + + cmd = {"id": request_id, "method": method} + if params: + cmd["params"] = params + line = json.dumps(cmd) + "\n" + + # Clear any pending data + ser.reset_input_buffer() + time.sleep(0.1) + + ser.write(line.encode()) + ser.flush() + + # Read response, skip non-JSON lines, match ID + old_timeout = ser.timeout + ser.timeout = timeout + try: + for _ in range(20): + resp_line = ser.readline().decode().strip() + if not resp_line: + continue + if resp_line.startswith("{"): + try: + resp = json.loads(resp_line) + if resp.get("id") == request_id: + return resp + except json.JSONDecodeError: + continue + finally: + ser.timeout = old_timeout + return None + +def main(): + print(f"Connecting to {DEVICE}...") + ser = serial.Serial(DEVICE, BAUD, timeout=5) + time.sleep(0.5) + + # Clear any buffered data + ser.reset_input_buffer() + + # Test ping + print("\n--- Testing ping ---") + resp = send_command(ser, "ping") + if resp: + print(f"Response: {json.dumps(resp, indent=2)}") + else: + print("No response") + + # List shares + print("\n--- Listing shares ---") + resp = send_command(ser, "list_shares") + if resp: + print(f"Response: {json.dumps(resp, indent=2)}") + else: + print("No response") + + # Test UX confirmation screen + if "--test-ux" in sys.argv or len(sys.argv) == 1: + print("\n--- Testing transaction confirmation UI ---") + print("Watch the device screen. You have 30 seconds to approve/reject.") + print("Tap Approve or Reject on the device...") + + resp = send_command(ser, "ux_test", timeout=40) + if resp: + print(f"Response: {json.dumps(resp, indent=2)}") + if "result" in resp: + result = resp["result"].get("result", "unknown") + if result == "approved": + print(">>> User APPROVED the transaction") + elif result == "rejected": + print(">>> User REJECTED the transaction") + elif result == "timeout": + print(">>> Confirmation TIMED OUT") + else: + print("No response (possible timeout)") + + ser.close() + print("\nDone!") + +if __name__ == "__main__": + main() From 11758d7c3f1c7bf47be06d8d017b39cf2dce9e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 20 Jan 2026 23:21:18 -0500 Subject: [PATCH 3/7] Polish idle, success, and error screens --- main/ux_display.c | 81 +++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/main/ux_display.c b/main/ux_display.c index 974ee2d..45deccc 100644 --- a/main/ux_display.c +++ b/main/ux_display.c @@ -271,6 +271,9 @@ static void scan_btn_cb(lv_event_t *e) { (void)e; } +static lv_obj_t *create_action_btn(lv_obj_t *parent, const char *text, lv_color_t bg_color, + lv_color_t text_color, lv_event_cb_t click_cb); + static void create_idle_screen(const char *device_name, bool policy_loaded, uint32_t policy_version) { current_screen = lv_obj_create(lv_scr_act()); @@ -284,52 +287,46 @@ static void create_idle_screen(const char *device_name, bool policy_loaded, lv_label_set_text(title, "KEEP"); lv_obj_set_style_text_color(title, COLOR_TEXT, 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_32, 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 30); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 40); lv_obj_t *subtitle = lv_label_create(current_screen); - lv_label_set_text(subtitle, "FROST Threshold Signer"); + lv_label_set_text(subtitle, "Threshold Signer"); lv_obj_set_style_text_color(subtitle, COLOR_MUTED, 0); - lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_12, 0); - lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 65); - - lv_obj_t *status_container = lv_obj_create(current_screen); - lv_obj_set_size(status_container, 280, 36); - lv_obj_set_style_bg_color(status_container, COLOR_SURFACE, 0); - lv_obj_set_style_border_width(status_container, 0, 0); - lv_obj_set_style_radius(status_container, 8, 0); - lv_obj_set_style_pad_all(status_container, 8, 0); - lv_obj_align(status_container, LV_ALIGN_TOP_MID, 0, 90); - - lv_obj_t *policy_label = lv_label_create(status_container); + lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0); + lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 78); + + lv_obj_t *status_pill = lv_obj_create(current_screen); + lv_obj_set_size(status_pill, LV_SIZE_CONTENT, 28); + lv_obj_set_style_bg_color(status_pill, policy_loaded ? lv_color_hex(0x1a3d1a) : lv_color_hex(0x3d3d1a), 0); + lv_obj_set_style_border_width(status_pill, 0, 0); + lv_obj_set_style_radius(status_pill, 14, 0); + lv_obj_set_style_pad_hor(status_pill, 12, 0); + lv_obj_set_style_pad_ver(status_pill, 4, 0); + lv_obj_align(status_pill, LV_ALIGN_TOP_MID, 0, 105); + + lv_obj_t *policy_label = lv_label_create(status_pill); if (policy_loaded) { char policy_str[48]; snprintf(policy_str, sizeof(policy_str), "Policy v%lu", (unsigned long)policy_version); lv_label_set_text(policy_label, policy_str); lv_obj_set_style_text_color(policy_label, COLOR_SUCCESS, 0); } else { - lv_label_set_text(policy_label, "No policy"); + lv_label_set_text(policy_label, "No policy loaded"); lv_obj_set_style_text_color(policy_label, COLOR_WARNING, 0); } lv_obj_set_style_text_font(policy_label, &lv_font_montserrat_12, 0); lv_obj_center(policy_label); - lv_obj_t *scan_btn = lv_btn_create(current_screen); - lv_obj_set_size(scan_btn, 200, 50); - lv_obj_align(scan_btn, LV_ALIGN_CENTER, 0, 30); - lv_obj_set_style_bg_color(scan_btn, COLOR_ACCENT, 0); - lv_obj_set_style_radius(scan_btn, 8, 0); - lv_obj_add_event_cb(scan_btn, scan_btn_cb, LV_EVENT_CLICKED, NULL); - - lv_obj_t *scan_label = lv_label_create(scan_btn); - lv_label_set_text(scan_label, "Scan QR"); - lv_obj_set_style_text_font(scan_label, &lv_font_montserrat_16, 0); - lv_obj_center(scan_label); + lv_obj_t *scan_btn = create_action_btn(current_screen, "Scan QR", + COLOR_ACCENT, COLOR_TEXT, scan_btn_cb); + lv_obj_set_size(scan_btn, 180, 52); + lv_obj_align(scan_btn, LV_ALIGN_CENTER, 0, 35); lv_obj_t *device_label = lv_label_create(current_screen); lv_label_set_text(device_label, device_name ? device_name : "Unknown Device"); lv_obj_set_style_text_color(device_label, COLOR_MUTED, 0); lv_obj_set_style_text_font(device_label, &lv_font_montserrat_12, 0); - lv_obj_align(device_label, LV_ALIGN_BOTTOM_MID, 0, -15); + lv_obj_align(device_label, LV_ALIGN_BOTTOM_MID, 0, -20); } static void create_scanning_screen(void) { @@ -709,17 +706,24 @@ static void create_error_screen(const char *title, const char *message) { lv_obj_set_style_border_width(current_screen, 0, 0); lv_obj_center(current_screen); - lv_obj_t *icon = lv_label_create(current_screen); + lv_obj_t *icon_bg = lv_obj_create(current_screen); + lv_obj_set_size(icon_bg, 72, 72); + lv_obj_set_style_bg_color(icon_bg, lv_color_hex(0x3d1a1a), 0); + lv_obj_set_style_border_width(icon_bg, 0, 0); + lv_obj_set_style_radius(icon_bg, 36, 0); + lv_obj_align(icon_bg, LV_ALIGN_CENTER, 0, -50); + + lv_obj_t *icon = lv_label_create(icon_bg); lv_label_set_text(icon, LV_SYMBOL_CLOSE); lv_obj_set_style_text_color(icon, COLOR_DANGER, 0); lv_obj_set_style_text_font(icon, &lv_font_montserrat_32, 0); - lv_obj_align(icon, LV_ALIGN_CENTER, 0, -50); + lv_obj_center(icon); lv_obj_t *title_label = lv_label_create(current_screen); lv_label_set_text(title_label, title ? title : "Error"); lv_obj_set_style_text_color(title_label, COLOR_TEXT, 0); lv_obj_set_style_text_font(title_label, &lv_font_montserrat_16, 0); - lv_obj_align(title_label, LV_ALIGN_CENTER, 0, -10); + lv_obj_align(title_label, LV_ALIGN_CENTER, 0, 5); lv_obj_t *msg_label = lv_label_create(current_screen); lv_label_set_text(msg_label, message ? message : "An error occurred"); @@ -727,7 +731,7 @@ static void create_error_screen(const char *title, const char *message) { lv_obj_set_style_text_align(msg_label, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_font(msg_label, &lv_font_montserrat_12, 0); lv_obj_set_width(msg_label, 280); - lv_obj_align(msg_label, LV_ALIGN_CENTER, 0, 25); + lv_obj_align(msg_label, LV_ALIGN_CENTER, 0, 35); lv_obj_t *hint = lv_label_create(current_screen); lv_label_set_text(hint, "Tap to continue"); @@ -743,23 +747,30 @@ static void create_success_screen(const char *message) { lv_obj_set_style_border_width(current_screen, 0, 0); lv_obj_center(current_screen); - lv_obj_t *check = lv_label_create(current_screen); + lv_obj_t *icon_bg = lv_obj_create(current_screen); + lv_obj_set_size(icon_bg, 72, 72); + lv_obj_set_style_bg_color(icon_bg, lv_color_hex(0x1a3d1a), 0); + lv_obj_set_style_border_width(icon_bg, 0, 0); + lv_obj_set_style_radius(icon_bg, 36, 0); + lv_obj_align(icon_bg, LV_ALIGN_CENTER, 0, -50); + + lv_obj_t *check = lv_label_create(icon_bg); lv_label_set_text(check, LV_SYMBOL_OK); lv_obj_set_style_text_color(check, COLOR_SUCCESS, 0); lv_obj_set_style_text_font(check, &lv_font_montserrat_32, 0); - lv_obj_align(check, LV_ALIGN_CENTER, 0, -40); + lv_obj_center(check); lv_obj_t *title = lv_label_create(current_screen); lv_label_set_text(title, "Success"); lv_obj_set_style_text_color(title, COLOR_TEXT, 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0); - lv_obj_align(title, LV_ALIGN_CENTER, 0, 10); + lv_obj_align(title, LV_ALIGN_CENTER, 0, 5); lv_obj_t *msg_label = lv_label_create(current_screen); lv_label_set_text(msg_label, message ? message : ""); lv_obj_set_style_text_color(msg_label, COLOR_MUTED, 0); lv_obj_set_style_text_font(msg_label, &lv_font_montserrat_12, 0); - lv_obj_align(msg_label, LV_ALIGN_CENTER, 0, 45); + lv_obj_align(msg_label, LV_ALIGN_CENTER, 0, 40); lv_obj_t *hint = lv_label_create(current_screen); lv_label_set_text(hint, "Tap to continue"); From caf948fe5f536264700668172dbbf1cc4c626a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 20 Jan 2026 23:21:31 -0500 Subject: [PATCH 4/7] Skip CI jobs on draft PRs --- .github/workflows/fuzz.yml | 1 + .github/workflows/static-analysis.yml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 010a05f..298b807 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -7,6 +7,7 @@ on: jobs: fuzz: runs-on: ubuntu-latest + if: github.event.pull_request.draft != true steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 62cb891..d838014 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -9,6 +9,7 @@ on: jobs: clang-format: runs-on: ubuntu-latest + if: github.event.pull_request.draft != true steps: - uses: actions/checkout@v4 @@ -20,6 +21,7 @@ jobs: cppcheck: runs-on: ubuntu-latest + if: github.event.pull_request.draft != true steps: - uses: actions/checkout@v4 with: @@ -58,6 +60,7 @@ jobs: clang-tidy: runs-on: ubuntu-latest + if: github.event.pull_request.draft != true steps: - uses: actions/checkout@v4 with: @@ -106,6 +109,7 @@ jobs: scan-build: runs-on: ubuntu-latest + if: github.event.pull_request.draft != true steps: - uses: actions/checkout@v4 with: From 4d04fe6cfaa80aa5b8ff2d94ff977162dd64ee22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 20 Jan 2026 23:22:51 -0500 Subject: [PATCH 5/7] Add spinner to signing progress screen --- main/ux_display.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/main/ux_display.c b/main/ux_display.c index 45deccc..eb863b9 100644 --- a/main/ux_display.c +++ b/main/ux_display.c @@ -637,21 +637,29 @@ static void create_signing_screen(int current, int total) { lv_obj_set_style_border_width(current_screen, 0, 0); lv_obj_center(current_screen); + lv_obj_t *spinner = lv_spinner_create(current_screen); + lv_obj_set_size(spinner, 50, 50); + lv_obj_set_style_arc_color(spinner, COLOR_SURFACE, LV_PART_MAIN); + lv_obj_set_style_arc_color(spinner, COLOR_ACCENT, LV_PART_INDICATOR); + lv_obj_set_style_arc_width(spinner, 5, LV_PART_MAIN); + lv_obj_set_style_arc_width(spinner, 5, LV_PART_INDICATOR); + lv_obj_align(spinner, LV_ALIGN_CENTER, 0, -55); + lv_obj_t *title = lv_label_create(current_screen); lv_label_set_text(title, "Signing"); lv_obj_set_style_text_color(title, COLOR_TEXT, 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0); - lv_obj_align(title, LV_ALIGN_CENTER, 0, -50); + lv_obj_align(title, LV_ALIGN_CENTER, 0, 5); int progress = (total > 0) ? (current * 100) / total : 0; signing_bar = lv_bar_create(current_screen); - lv_obj_set_size(signing_bar, 260, 12); - lv_obj_align(signing_bar, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_size(signing_bar, 260, 8); + lv_obj_align(signing_bar, LV_ALIGN_CENTER, 0, 50); lv_obj_set_style_bg_color(signing_bar, COLOR_SURFACE, LV_PART_MAIN); lv_obj_set_style_bg_color(signing_bar, COLOR_ACCENT, LV_PART_INDICATOR); - lv_obj_set_style_radius(signing_bar, 6, LV_PART_MAIN); - lv_obj_set_style_radius(signing_bar, 6, LV_PART_INDICATOR); + lv_obj_set_style_radius(signing_bar, 4, LV_PART_MAIN); + lv_obj_set_style_radius(signing_bar, 4, LV_PART_INDICATOR); lv_bar_set_range(signing_bar, 0, 100); lv_bar_set_value(signing_bar, progress, LV_ANIM_OFF); @@ -661,7 +669,7 @@ static void create_signing_screen(int current, int total) { lv_label_set_text(signing_label, progress_str); lv_obj_set_style_text_color(signing_label, COLOR_MUTED, 0); lv_obj_set_style_text_font(signing_label, &lv_font_montserrat_12, 0); - lv_obj_align(signing_label, LV_ALIGN_CENTER, 0, 30); + lv_obj_align(signing_label, LV_ALIGN_CENTER, 0, 75); } static void create_qr_screen(const char *data, size_t len) { From c6900ab30dedba35dc099abda21254495e24e0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 20 Jan 2026 23:28:48 -0500 Subject: [PATCH 6/7] Polish QR display screen --- main/ux_display.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/main/ux_display.c b/main/ux_display.c index eb863b9..5a2afeb 100644 --- a/main/ux_display.c +++ b/main/ux_display.c @@ -677,24 +677,28 @@ static void create_qr_screen(const char *data, size_t len) { lv_obj_set_size(current_screen, SCREEN_WIDTH, SCREEN_HEIGHT); lv_obj_set_style_bg_color(current_screen, COLOR_BG, 0); lv_obj_set_style_border_width(current_screen, 0, 0); + lv_obj_set_style_pad_all(current_screen, 0, 0); lv_obj_center(current_screen); lv_obj_t *title = lv_label_create(current_screen); - lv_label_set_text(title, "Scan with wallet"); + lv_label_set_text(title, "Scan with Wallet"); lv_obj_set_style_text_color(title, COLOR_TEXT, 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14); lv_obj_t *qr_container = lv_obj_create(current_screen); - lv_obj_set_size(qr_container, 188, 188); + lv_obj_set_size(qr_container, 172, 172); lv_obj_set_style_bg_color(qr_container, lv_color_white(), 0); lv_obj_set_style_border_width(qr_container, 0, 0); - lv_obj_set_style_radius(qr_container, 8, 0); - lv_obj_set_style_pad_all(qr_container, 4, 0); - lv_obj_align(qr_container, LV_ALIGN_CENTER, 0, 5); + lv_obj_set_style_radius(qr_container, 12, 0); + lv_obj_set_style_pad_all(qr_container, 6, 0); + lv_obj_set_style_shadow_width(qr_container, 20, 0); + lv_obj_set_style_shadow_color(qr_container, lv_color_hex(0x0a84ff), 0); + lv_obj_set_style_shadow_opa(qr_container, 60, 0); + lv_obj_align(qr_container, LV_ALIGN_CENTER, 0, 8); lv_obj_t *qr = lv_qrcode_create(qr_container); - lv_qrcode_set_size(qr, 180); + lv_qrcode_set_size(qr, 160); lv_qrcode_set_dark_color(qr, lv_color_black()); lv_qrcode_set_light_color(qr, lv_color_white()); lv_qrcode_update(qr, data, len); @@ -704,7 +708,7 @@ static void create_qr_screen(const char *data, size_t len) { lv_label_set_text(hint, "Tap to continue"); lv_obj_set_style_text_color(hint, COLOR_MUTED, 0); lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); - lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -15); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -20); } static void create_error_screen(const char *title, const char *message) { From 5fbd5a575531e9cd00b36aab29c901dc9727ef97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 20 Jan 2026 23:40:05 -0500 Subject: [PATCH 7/7] Fix callback race condition and integer overflow in ux_display --- main/ux_display.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/main/ux_display.c b/main/ux_display.c index 5a2afeb..89db01e 100644 --- a/main/ux_display.c +++ b/main/ux_display.c @@ -164,7 +164,7 @@ static void display_show_signing(int current, int total) { create_signing_screen(current, total); current_state = UI_STATE_SIGNING; } else if (signing_bar && signing_label) { - lv_bar_set_value(signing_bar, (current * 100) / total, LV_ANIM_ON); + lv_bar_set_value(signing_bar, (int)((current * 100LL) / total), LV_ANIM_ON); char progress_str[32]; snprintf(progress_str, sizeof(progress_str), "Input %d of %d", current, total); lv_label_set_text(signing_label, progress_str); @@ -191,14 +191,13 @@ static void display_show_error(const char *title, const char *message) { static void invoke_pending_callback(bool approved) { stop_confirm_timer(); - if (!pending_callback) { - return; - } ux_decision_cb_t cb = pending_callback; void *data = pending_user_data; pending_callback = NULL; pending_user_data = NULL; - cb(approved, data); + if (cb) { + cb(approved, data); + } } static void approve_btn_cb(lv_event_t *e) { @@ -651,7 +650,7 @@ static void create_signing_screen(int current, int total) { lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0); lv_obj_align(title, LV_ALIGN_CENTER, 0, 5); - int progress = (total > 0) ? (current * 100) / total : 0; + int progress = (total > 0) ? (int)((current * 100LL) / total) : 0; signing_bar = lv_bar_create(current_screen); lv_obj_set_size(signing_bar, 260, 8);