mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-24 07:18:09 +00:00
770cd19899
so that we actually fit if all the biggest characters are used [no changelog]
1442 lines
47 KiB
C
1442 lines
47 KiB
C
/*
|
|
* This file is part of the Trezor project, https://trezor.io/
|
|
*
|
|
* Copyright (C) 2014 Pavol Rusnak <stick@satoshilabs.com>
|
|
*
|
|
* This library is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <ctype.h>
|
|
#include <inttypes.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "bignum.h"
|
|
#include "bitmaps.h"
|
|
#include "config.h"
|
|
#include "crypto.h"
|
|
#include "gettext.h"
|
|
#include "layout2.h"
|
|
#include "memzero.h"
|
|
#include "nem2.h"
|
|
#include "oled.h"
|
|
#include "qrcodegen.h"
|
|
#include "secp256k1.h"
|
|
#include "string.h"
|
|
#include "timer.h"
|
|
#include "util.h"
|
|
|
|
#define LOCKTIME_TIMESTAMP_MIN_VALUE 500000000
|
|
|
|
#if !BITCOIN_ONLY
|
|
|
|
static const char *slip44_extras(uint32_t coin_type) {
|
|
if ((coin_type & PATH_HARDENED) == 0) {
|
|
return 0;
|
|
}
|
|
switch (coin_type & PATH_UNHARDEN_MASK) {
|
|
case 40:
|
|
return "EXP"; // Expanse
|
|
case 43:
|
|
return "NEM"; // NEM
|
|
case 60:
|
|
return "ETH"; // Ethereum Mainnet
|
|
case 61:
|
|
return "ETC"; // Ethereum Classic Mainnet
|
|
case 108:
|
|
return "UBQ"; // UBIQ
|
|
case 137:
|
|
return "RSK"; // Rootstock Mainnet
|
|
case 37310:
|
|
return "tRSK"; // Rootstock Testnet
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
static const char *address_n_str(const uint32_t *address_n,
|
|
size_t address_n_count,
|
|
bool address_is_account) {
|
|
if (address_n_count > 8) {
|
|
return _("Unknown long path");
|
|
}
|
|
if (address_n_count == 0) {
|
|
return _("Path: m");
|
|
}
|
|
|
|
enum {
|
|
ACCOUNT_NONE,
|
|
ACCOUNT_BIP44,
|
|
ACCOUNT_BIP49,
|
|
ACCOUNT_BIP84,
|
|
ACCOUNT_BIP86,
|
|
ACCOUNT_SLIP25
|
|
} account_type = ACCOUNT_NONE;
|
|
|
|
if ((address_n[1] & PATH_HARDENED) && (address_n[2] & PATH_HARDENED) &&
|
|
(address_n[address_n_count - 2] <= PATH_MAX_CHANGE) &&
|
|
(address_n[address_n_count - 1] <= PATH_MAX_ADDRESS_INDEX)) {
|
|
if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 44) {
|
|
account_type = ACCOUNT_BIP44;
|
|
} else if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 49) {
|
|
account_type = ACCOUNT_BIP49;
|
|
} else if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 84) {
|
|
account_type = ACCOUNT_BIP84;
|
|
} else if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 86) {
|
|
account_type = ACCOUNT_BIP86;
|
|
} else if (address_n_count == 6 && address_n[0] == PATH_SLIP25_PURPOSE &&
|
|
(address_n[3] & PATH_HARDENED)) {
|
|
account_type = ACCOUNT_SLIP25;
|
|
}
|
|
}
|
|
|
|
// known BIP44/49/84/86 path
|
|
static char path[100];
|
|
if (account_type != ACCOUNT_NONE) {
|
|
bool legacy = false;
|
|
const CoinInfo *coin = coinBySlip44(address_n[1]);
|
|
const char *abbr = 0;
|
|
if (account_type == ACCOUNT_BIP86 || account_type == ACCOUNT_SLIP25) {
|
|
if (coin && coin->has_taproot && coin->bech32_prefix) {
|
|
abbr = coin->coin_shortcut;
|
|
}
|
|
} else if (account_type == ACCOUNT_BIP84) {
|
|
if (coin && coin->has_segwit && coin->bech32_prefix) {
|
|
abbr = coin->coin_shortcut;
|
|
}
|
|
} else if (account_type == ACCOUNT_BIP49) {
|
|
if (coin && coin->has_segwit) {
|
|
abbr = coin->coin_shortcut;
|
|
}
|
|
} else {
|
|
if (coin) {
|
|
if (coin->has_segwit) {
|
|
legacy = true;
|
|
}
|
|
abbr = coin->coin_shortcut;
|
|
#if !BITCOIN_ONLY
|
|
} else {
|
|
abbr = slip44_extras(address_n[1]);
|
|
#endif
|
|
}
|
|
}
|
|
const uint32_t accnum = address_is_account
|
|
? ((address_n[4] & PATH_UNHARDEN_MASK) + 1)
|
|
: (address_n[2] & PATH_UNHARDEN_MASK) + 1;
|
|
if (abbr && accnum < 100) {
|
|
memzero(path, sizeof(path));
|
|
strlcpy(path, abbr, sizeof(path));
|
|
// Account naming:
|
|
// "Legacy", "Legacy SegWit", "SegWit", "Taproot" and "Coinjoin" for
|
|
// BIP44/P2PKH, BIP49/P2SH-P2WPKH, BIP84/P2WPKH, BIP86/P2TR, SLIP25/P2TR
|
|
// respectively. For non-segwit coins we use only BIP44 with no special
|
|
// naming.
|
|
if (legacy) {
|
|
strlcat(path, " Legacy", sizeof(path));
|
|
} else if (account_type == ACCOUNT_BIP49) {
|
|
strlcat(path, " L.SegWit", sizeof(path));
|
|
} else if (account_type == ACCOUNT_BIP84) {
|
|
strlcat(path, " SegWit", sizeof(path));
|
|
} else if (account_type == ACCOUNT_BIP86) {
|
|
strlcat(path, " Taproot", sizeof(path));
|
|
} else if (account_type == ACCOUNT_SLIP25) {
|
|
strlcat(path, " Coinjoin", sizeof(path));
|
|
}
|
|
|
|
if (address_is_account) {
|
|
strlcat(path, " address", sizeof(path));
|
|
} else {
|
|
strlcat(path, " account", sizeof(path));
|
|
}
|
|
|
|
if (!(account_type == ACCOUNT_SLIP25 && accnum == 1)) {
|
|
char acc[5] = {' ', '#'};
|
|
if (accnum < 10) {
|
|
acc[2] = '0' + accnum;
|
|
acc[3] = '\0';
|
|
} else {
|
|
acc[2] = '0' + (accnum / 10);
|
|
acc[3] = '0' + (accnum % 10);
|
|
acc[4] = '\0';
|
|
}
|
|
strlcat(path, acc, sizeof(path));
|
|
}
|
|
return path;
|
|
}
|
|
}
|
|
|
|
// "Path: m" / i '
|
|
static char address_str[7 + 8 * (1 + 10 + 1) + 1];
|
|
char *c = address_str + sizeof(address_str) - 1;
|
|
|
|
*c = 0;
|
|
c--;
|
|
|
|
for (int n = (int)address_n_count - 1; n >= 0; n--) {
|
|
uint32_t i = address_n[n];
|
|
if (i & PATH_HARDENED) {
|
|
*c = '\'';
|
|
c--;
|
|
}
|
|
i = i & PATH_UNHARDEN_MASK;
|
|
do {
|
|
*c = '0' + (i % 10);
|
|
c--;
|
|
i /= 10;
|
|
} while (i > 0);
|
|
*c = '/';
|
|
c--;
|
|
}
|
|
*c = 'm';
|
|
c--;
|
|
*c = ' ';
|
|
c--;
|
|
*c = ':';
|
|
c--;
|
|
*c = 'h';
|
|
c--;
|
|
*c = 't';
|
|
c--;
|
|
*c = 'a';
|
|
c--;
|
|
*c = 'P';
|
|
|
|
return c;
|
|
}
|
|
|
|
// split longer string into 4 rows, rowlen chars each
|
|
const char **split_message(const uint8_t *msg, uint32_t len, uint32_t rowlen) {
|
|
static char str[4][32 + 1];
|
|
if (rowlen > 32) {
|
|
rowlen = 32;
|
|
}
|
|
|
|
memzero(str, sizeof(str));
|
|
for (int i = 0; i < 4; ++i) {
|
|
size_t show_len = strnlen((char *)msg, MIN(rowlen, len));
|
|
memcpy(str[i], (char *)msg, show_len);
|
|
str[i][show_len] = '\0';
|
|
msg += show_len;
|
|
len -= show_len;
|
|
}
|
|
|
|
if (len > 0) {
|
|
str[3][rowlen - 1] = '.';
|
|
str[3][rowlen - 2] = '.';
|
|
str[3][rowlen - 3] = '.';
|
|
}
|
|
static const char *ret[4] = {str[0], str[1], str[2], str[3]};
|
|
return ret;
|
|
}
|
|
|
|
const char **split_message_hex(const uint8_t *msg, uint32_t len) {
|
|
char hex[32 * 2 + 1] = {0};
|
|
memzero(hex, sizeof(hex));
|
|
uint32_t size = len;
|
|
if (len > 32) {
|
|
size = 32;
|
|
}
|
|
data2hex(msg, size, hex);
|
|
if (len > 32) {
|
|
hex[63] = '.';
|
|
hex[62] = '.';
|
|
}
|
|
return split_message((const uint8_t *)hex, size * 2, 16);
|
|
}
|
|
|
|
void *layoutLast = NULL;
|
|
|
|
void layoutDialogSwipeWrapping(const BITMAP *icon, const char *btnNo,
|
|
const char *btnYes, const char *heading,
|
|
const char *description, const char *wrap_text) {
|
|
const uint32_t row_len = 18;
|
|
const char **str =
|
|
split_message((const uint8_t *)wrap_text, strlen(wrap_text), row_len);
|
|
layoutDialogSwipe(icon, btnNo, btnYes, NULL, heading, description, str[0],
|
|
str[1], str[2], str[3]);
|
|
}
|
|
|
|
void layoutDialogSwipe(const BITMAP *icon, const char *btnNo,
|
|
const char *btnYes, const char *desc, const char *line1,
|
|
const char *line2, const char *line3, const char *line4,
|
|
const char *line5, const char *line6) {
|
|
layoutDialogSwipeEx(icon, btnNo, btnYes, desc, line1, line2, line3, line4,
|
|
line5, line6, FONT_STANDARD);
|
|
}
|
|
|
|
void layoutDialogSwipeEx(const BITMAP *icon, const char *btnNo,
|
|
const char *btnYes, const char *desc,
|
|
const char *line1, const char *line2,
|
|
const char *line3, const char *line4,
|
|
const char *line5, const char *line6, uint8_t font) {
|
|
layoutLast = layoutDialogSwipe;
|
|
layoutSwipe();
|
|
layoutDialogEx(icon, btnNo, btnYes, desc, line1, line2, line3, line4, line5,
|
|
line6, font);
|
|
}
|
|
|
|
void layoutProgressSwipe(const char *desc, int permil) {
|
|
if (layoutLast == layoutProgressSwipe) {
|
|
oledClear();
|
|
} else {
|
|
layoutLast = layoutProgressSwipe;
|
|
layoutSwipe();
|
|
}
|
|
layoutProgress(desc, permil);
|
|
}
|
|
|
|
void layoutScreensaver(void) {
|
|
if (system_millis_busy_deadline > timer_ms()) {
|
|
// Busy screen overrides the screensaver.
|
|
layoutBusyscreen();
|
|
} else {
|
|
layoutLast = layoutScreensaver;
|
|
oledClear();
|
|
oledRefresh();
|
|
}
|
|
}
|
|
|
|
void layoutHome(void) {
|
|
if (layoutLast != layoutHomescreen && layoutLast != layoutBusyscreen) {
|
|
// Reset lock screen timeout
|
|
system_millis_lock_start = timer_ms();
|
|
}
|
|
|
|
if (system_millis_busy_deadline > timer_ms()) {
|
|
layoutBusyscreen();
|
|
} else {
|
|
layoutHomescreen();
|
|
}
|
|
}
|
|
|
|
void layoutHomescreen(void) {
|
|
if (layoutLast == layoutHomescreen || layoutLast == layoutScreensaver) {
|
|
oledClear();
|
|
} else {
|
|
layoutSwipe();
|
|
}
|
|
layoutLast = layoutHomescreen;
|
|
|
|
char label[MAX_LABEL_LEN + 1] = _("Go to trezor.io/start");
|
|
if (config_isInitialized()) {
|
|
config_getLabel(label, sizeof(label));
|
|
}
|
|
|
|
uint8_t homescreen[HOMESCREEN_SIZE] = {0};
|
|
if (config_getHomescreen(homescreen, sizeof(homescreen))) {
|
|
BITMAP b = {0};
|
|
b.width = 128;
|
|
b.height = 64;
|
|
b.data = homescreen;
|
|
oledDrawBitmap(0, 0, &b);
|
|
} else {
|
|
if (label[0] != '\0') {
|
|
oledDrawBitmap(44, 4, &bmp_logo48);
|
|
oledDrawStringCenter(OLED_WIDTH / 2, OLED_HEIGHT - 8, label,
|
|
FONT_STANDARD);
|
|
} else {
|
|
oledDrawBitmap(40, 0, &bmp_logo64_half);
|
|
oledDrawBitmapFlip(40 + 24, 0, &bmp_logo64_half);
|
|
}
|
|
}
|
|
|
|
bool no_backup = false;
|
|
bool unfinished_backup = false;
|
|
bool needs_backup = false;
|
|
config_getNoBackup(&no_backup);
|
|
config_getUnfinishedBackup(&unfinished_backup);
|
|
config_getNeedsBackup(&needs_backup);
|
|
if (no_backup) {
|
|
oledBox(0, 0, 127, 8, false);
|
|
oledDrawStringCenter(OLED_WIDTH / 2, 0, "SEEDLESS", FONT_STANDARD);
|
|
} else if (unfinished_backup) {
|
|
oledBox(0, 0, 127, 8, false);
|
|
oledDrawStringCenter(OLED_WIDTH / 2, 0, "BACKUP FAILED!", FONT_STANDARD);
|
|
} else if (needs_backup) {
|
|
oledBox(0, 0, 127, 8, false);
|
|
oledDrawStringCenter(OLED_WIDTH / 2, 0, "NEEDS BACKUP!", FONT_STANDARD);
|
|
}
|
|
oledRefresh();
|
|
}
|
|
|
|
void layoutBusyscreen(void) {
|
|
if (layoutLast == layoutBusyscreen || layoutLast == layoutScreensaver) {
|
|
oledClear();
|
|
} else {
|
|
layoutSwipe();
|
|
}
|
|
layoutLast = layoutBusyscreen;
|
|
|
|
layoutDialog(&bmp_icon_warning, NULL, NULL, NULL, _("Please wait"), NULL,
|
|
_("Coinjoin in progress."), NULL, _("Do not disconnect"),
|
|
_("your Trezor."));
|
|
}
|
|
|
|
static void render_address_dialog(const CoinInfo *coin, const char *address,
|
|
const char *line1, const char *line2,
|
|
const char *extra_line) {
|
|
if (coin && coin->cashaddr_prefix) {
|
|
/* If this is a cashaddr address, remove the prefix from the
|
|
* string presented to the user
|
|
*/
|
|
int prefix_len = strlen(coin->cashaddr_prefix);
|
|
if (strncmp(address, coin->cashaddr_prefix, prefix_len) == 0 &&
|
|
address[prefix_len] == ':') {
|
|
address += prefix_len + 1;
|
|
}
|
|
}
|
|
int addrlen = strlen(address);
|
|
int numlines = addrlen <= 42 ? 2 : 3;
|
|
int linelen = (addrlen - 1) / numlines + 1;
|
|
if (linelen > 21) {
|
|
linelen = 21;
|
|
}
|
|
const char **str = split_message((const uint8_t *)address, addrlen, linelen);
|
|
layoutLast = layoutDialogSwipe;
|
|
layoutSwipe();
|
|
oledClear();
|
|
oledDrawBitmap(0, 0, &bmp_icon_question);
|
|
oledDrawString(20, 0 * 9, line1, FONT_STANDARD);
|
|
oledDrawString(20, 1 * 9, line2, FONT_STANDARD);
|
|
int left = linelen > 18 ? 0 : 20;
|
|
oledDrawString(left, 2 * 9, str[0], FONT_FIXED);
|
|
oledDrawString(left, 3 * 9, str[1], FONT_FIXED);
|
|
oledDrawString(left, 4 * 9, str[2], FONT_FIXED);
|
|
oledDrawString(left, 5 * 9, str[3], FONT_FIXED);
|
|
if (!str[3][0]) {
|
|
if (extra_line) {
|
|
oledDrawString(0, 5 * 9, extra_line, FONT_STANDARD);
|
|
} else {
|
|
oledHLine(OLED_HEIGHT - 13);
|
|
}
|
|
}
|
|
layoutButtonNo(_("Cancel"), &bmp_btn_cancel);
|
|
layoutButtonYes(_("Confirm"), &bmp_btn_confirm);
|
|
oledRefresh();
|
|
}
|
|
|
|
static size_t format_coin_amount(uint64_t amount, const char *prefix,
|
|
const CoinInfo *coin, AmountUnit amount_unit,
|
|
char *output, size_t output_len) {
|
|
// " " + (optional "m"/u") + shortcut + ending zero -> 16 should suffice
|
|
char suffix[16];
|
|
memzero(suffix, sizeof(suffix));
|
|
suffix[0] = ' ';
|
|
uint32_t decimals = coin->decimals;
|
|
switch (amount_unit) {
|
|
case AmountUnit_SATOSHI:
|
|
decimals = 0;
|
|
strlcpy(suffix + 1, "sat", sizeof(suffix) - 1);
|
|
if (strcmp(coin->coin_shortcut, "BTC") != 0) {
|
|
strlcpy(suffix + 4, " ", sizeof(suffix) - 4);
|
|
strlcpy(suffix + 5, coin->coin_shortcut, sizeof(suffix) - 5);
|
|
}
|
|
break;
|
|
case AmountUnit_MILLIBITCOIN:
|
|
if (decimals >= 6) {
|
|
decimals -= 6;
|
|
suffix[1] = 'u';
|
|
strlcpy(suffix + 2, coin->coin_shortcut, sizeof(suffix) - 2);
|
|
} else {
|
|
strlcpy(suffix + 1, coin->coin_shortcut, sizeof(suffix) - 1);
|
|
}
|
|
break;
|
|
case AmountUnit_MICROBITCOIN:
|
|
if (decimals >= 3) {
|
|
decimals -= 3;
|
|
suffix[1] = 'm';
|
|
strlcpy(suffix + 2, coin->coin_shortcut, sizeof(suffix) - 2);
|
|
} else {
|
|
strlcpy(suffix + 1, coin->coin_shortcut, sizeof(suffix) - 1);
|
|
}
|
|
break;
|
|
default: // AmountUnit_BITCOIN
|
|
strlcpy(suffix + 1, coin->coin_shortcut, sizeof(suffix) - 1);
|
|
break;
|
|
}
|
|
return bn_format_amount(amount, prefix, suffix, decimals, output, output_len);
|
|
}
|
|
|
|
void layoutConfirmOutput(const CoinInfo *coin, AmountUnit amount_unit,
|
|
const TxOutputType *out) {
|
|
char str_out[32 + 3] = {0};
|
|
format_coin_amount(out->amount, NULL, coin, amount_unit, str_out,
|
|
sizeof(str_out) - 3);
|
|
strlcat(str_out, " to", sizeof(str_out));
|
|
const char *address = out->address;
|
|
const char *extra_line =
|
|
(out->address_n_count > 0)
|
|
? address_n_str(out->address_n, out->address_n_count, false)
|
|
: 0;
|
|
render_address_dialog(coin, address, _("Confirm sending"), str_out,
|
|
extra_line);
|
|
}
|
|
|
|
void layoutConfirmOmni(const uint8_t *data, uint32_t size) {
|
|
const char *desc = NULL;
|
|
char str_out[32] = {0};
|
|
uint32_t tx_type = 0, currency = 0;
|
|
REVERSE32(*(const uint32_t *)(data + 4), tx_type);
|
|
if (tx_type == 0x00000000 && size == 20) { // OMNI simple send
|
|
desc = _("Simple send of ");
|
|
REVERSE32(*(const uint32_t *)(data + 8), currency);
|
|
const char *suffix = " UNKN";
|
|
bool divisible = false;
|
|
switch (currency) {
|
|
case 1:
|
|
suffix = " OMNI";
|
|
divisible = true;
|
|
break;
|
|
case 2:
|
|
suffix = " tOMNI";
|
|
divisible = true;
|
|
break;
|
|
case 3:
|
|
suffix = " MAID";
|
|
divisible = false;
|
|
break;
|
|
case 31:
|
|
suffix = " USDT";
|
|
divisible = true;
|
|
break;
|
|
}
|
|
uint64_t amount_be = 0, amount = 0;
|
|
memcpy(&amount_be, data + 12, sizeof(uint64_t));
|
|
REVERSE64(amount_be, amount);
|
|
bn_format_amount(amount, NULL, suffix, divisible ? 8 : 0, str_out,
|
|
sizeof(str_out));
|
|
} else {
|
|
desc = _("Unknown transaction");
|
|
str_out[0] = 0;
|
|
}
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Confirm OMNI Transaction:"), NULL, desc, NULL, str_out,
|
|
NULL);
|
|
}
|
|
|
|
bool is_valid_ascii(const uint8_t *data, uint32_t size) {
|
|
for (uint32_t i = 0; i < size; i++) {
|
|
if (data[i] < ' ' || data[i] > '~') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void layoutConfirmOpReturn(const uint8_t *data, uint32_t size) {
|
|
const char **str = NULL;
|
|
if (!is_valid_ascii(data, size)) {
|
|
str = split_message_hex(data, size);
|
|
} else {
|
|
str = split_message(data, size, 20);
|
|
}
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Confirm OP_RETURN:"), str[0], str[1], str[2], str[3],
|
|
NULL);
|
|
}
|
|
|
|
static bool formatAmountDifference(const CoinInfo *coin, AmountUnit amount_unit,
|
|
uint64_t amount1, uint64_t amount2,
|
|
char *output, size_t output_length) {
|
|
uint64_t abs_diff = 0;
|
|
const char *sign = NULL;
|
|
if (amount1 >= amount2) {
|
|
abs_diff = amount1 - amount2;
|
|
} else {
|
|
abs_diff = amount2 - amount1;
|
|
sign = "-";
|
|
}
|
|
|
|
return format_coin_amount(abs_diff, sign, coin, amount_unit, output,
|
|
output_length) != 0;
|
|
}
|
|
|
|
// Computes numer / denom and rounds to the nearest integer.
|
|
static uint64_t div_round(uint64_t numer, uint64_t denom) {
|
|
return numer / denom + (2 * (numer % denom) >= denom);
|
|
}
|
|
|
|
static bool formatComputedFeeRate(uint64_t fee, uint64_t tx_weight,
|
|
char *output, size_t output_length,
|
|
bool segwit, bool parentheses) {
|
|
// Convert transaction weight to virtual transaction size, which is defined
|
|
// as tx_weight / 4 rounded up to the next integer.
|
|
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
|
|
uint64_t tx_size = (tx_weight + 3) / 4;
|
|
|
|
// Compute fee rate and modify it in place for the bn_format_amount()
|
|
// function. We multiply by 100, because we want bn_format_amount() to display
|
|
// two decimal digits.
|
|
uint64_t fee_rate_multiplied = div_round(100 * fee, tx_size);
|
|
|
|
size_t length =
|
|
bn_format_amount(fee_rate_multiplied, parentheses ? "(" : NULL,
|
|
segwit ? " sat/vB" : " sat/B", 2, output, output_length);
|
|
if (length == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (parentheses) {
|
|
if (length + 2 > output_length) {
|
|
return false;
|
|
}
|
|
output[length] = ')';
|
|
output[length + 1] = '\0';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool formatFeeRate(uint64_t fee_per_kvbyte, char *output,
|
|
size_t output_length, bool segwit) {
|
|
return formatComputedFeeRate(fee_per_kvbyte, 4000, output, output_length,
|
|
segwit, false);
|
|
}
|
|
|
|
void layoutConfirmTx(const CoinInfo *coin, AmountUnit amount_unit,
|
|
uint64_t total_in, uint64_t external_in,
|
|
uint64_t total_out, uint64_t change_out,
|
|
uint64_t tx_weight) {
|
|
char str_out[32] = {0};
|
|
formatAmountDifference(coin, amount_unit, total_in, change_out, str_out,
|
|
sizeof(str_out));
|
|
|
|
if (external_in == 0) {
|
|
char str_fee[32] = {0};
|
|
formatAmountDifference(coin, amount_unit, total_in, total_out, str_fee,
|
|
sizeof(str_fee));
|
|
|
|
char str_fee_rate[32] = {0};
|
|
bool show_fee_rate = total_in >= total_out;
|
|
|
|
if (show_fee_rate) {
|
|
formatComputedFeeRate(total_in - total_out, tx_weight, str_fee_rate,
|
|
sizeof(str_fee_rate), coin->has_segwit, true);
|
|
}
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Confirm sending:"), str_out, _("including fee:"),
|
|
str_fee, show_fee_rate ? str_fee_rate : NULL, NULL);
|
|
} else {
|
|
char str_spend[32] = {0};
|
|
formatAmountDifference(coin, amount_unit, total_in - external_in,
|
|
change_out, str_spend, sizeof(str_spend));
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("You are contributing:"), str_spend,
|
|
_("to the total amount:"), str_out, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
void layoutConfirmReplacement(const char *description, uint8_t txid[32]) {
|
|
const char **str = split_message_hex(txid, 32);
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
description, str[0], str[1], str[2], str[3], NULL);
|
|
}
|
|
|
|
void layoutConfirmModifyOutput(const CoinInfo *coin, AmountUnit amount_unit,
|
|
TxOutputType *out, TxOutputType *orig_out,
|
|
int page) {
|
|
if (page == 0) {
|
|
render_address_dialog(coin, out->address, _("Modify amount for"),
|
|
_("address:"), NULL);
|
|
} else {
|
|
char *question = NULL;
|
|
uint64_t amount_change = 0;
|
|
if (orig_out->amount < out->amount) {
|
|
question = _("Increase amount by:");
|
|
amount_change = out->amount - orig_out->amount;
|
|
} else {
|
|
question = _("Decrease amount by:");
|
|
amount_change = orig_out->amount - out->amount;
|
|
}
|
|
|
|
char str_amount_change[32] = {0};
|
|
format_coin_amount(amount_change, NULL, coin, amount_unit,
|
|
str_amount_change, sizeof(str_amount_change));
|
|
|
|
char str_amount_new[32] = {0};
|
|
format_coin_amount(out->amount, NULL, coin, amount_unit, str_amount_new,
|
|
sizeof(str_amount_new));
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
question, str_amount_change, NULL, _("New amount:"),
|
|
str_amount_new, NULL);
|
|
}
|
|
}
|
|
|
|
void layoutConfirmModifyFee(const CoinInfo *coin, AmountUnit amount_unit,
|
|
uint64_t fee_old, uint64_t fee_new,
|
|
uint64_t tx_weight) {
|
|
char str_fee_change[32] = {0};
|
|
char str_fee_new[32] = {0};
|
|
char *question = NULL;
|
|
|
|
uint64_t fee_change = 0;
|
|
if (fee_old < fee_new) {
|
|
question = _("Increase your fee by:");
|
|
fee_change = fee_new - fee_old;
|
|
} else {
|
|
question = _("Decrease your fee by:");
|
|
fee_change = fee_old - fee_new;
|
|
}
|
|
format_coin_amount(fee_change, NULL, coin, amount_unit, str_fee_change,
|
|
sizeof(str_fee_change));
|
|
|
|
format_coin_amount(fee_new, NULL, coin, amount_unit, str_fee_new,
|
|
sizeof(str_fee_new));
|
|
|
|
char str_fee_rate[32] = {0};
|
|
|
|
formatComputedFeeRate(fee_new, tx_weight, str_fee_rate, sizeof(str_fee_rate),
|
|
coin->has_segwit, true);
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
question, str_fee_change, _("Transaction fee:"),
|
|
str_fee_new, str_fee_rate, NULL);
|
|
}
|
|
|
|
void layoutFeeOverThreshold(const CoinInfo *coin, AmountUnit amount_unit,
|
|
uint64_t fee) {
|
|
char str_fee[32] = {0};
|
|
format_coin_amount(fee, NULL, coin, amount_unit, str_fee, sizeof(str_fee));
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Fee"), str_fee, _("is unexpectedly high."), NULL,
|
|
_("Send anyway?"), NULL);
|
|
}
|
|
|
|
void layoutFeeRateOverThreshold(const CoinInfo *coin, uint32_t fee_per_kvbyte) {
|
|
char str_fee_rate[32] = {0};
|
|
formatFeeRate(fee_per_kvbyte, str_fee_rate, sizeof(str_fee_rate),
|
|
coin->has_segwit);
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Fee rate"), str_fee_rate, _("is unexpectedly high."),
|
|
NULL, _("Proceed anyway?"), NULL);
|
|
}
|
|
|
|
void layoutChangeCountOverThreshold(uint32_t change_count) {
|
|
char str_change[21] = {0};
|
|
snprintf(str_change, sizeof(str_change), "There are %" PRIu32, change_count);
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Warning!"), str_change, _("change-outputs."), NULL,
|
|
_("Continue?"), NULL);
|
|
}
|
|
|
|
void layoutConfirmUnverifiedExternalInputs(void) {
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Warning!"), _("The transaction"),
|
|
_("contains unverified"), _("external inputs."),
|
|
_("Continue?"), NULL);
|
|
}
|
|
|
|
void layoutConfirmNondefaultLockTime(uint32_t lock_time,
|
|
bool lock_time_disabled) {
|
|
if (lock_time_disabled) {
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Warning!"), _("Locktime is set but"),
|
|
_("will have no effect."), NULL, _("Continue?"), NULL);
|
|
|
|
} else {
|
|
char str_locktime[20] = {0};
|
|
char *str_type = NULL;
|
|
if (lock_time < LOCKTIME_TIMESTAMP_MIN_VALUE) {
|
|
str_type = "blockheight:";
|
|
snprintf(str_locktime, sizeof(str_locktime), "%" PRIu32, lock_time);
|
|
} else {
|
|
str_type = "timestamp (UTC):";
|
|
time_t time = lock_time;
|
|
const struct tm *tm = gmtime(&time);
|
|
strftime(str_locktime, sizeof(str_locktime), "%F %T", tm);
|
|
}
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Locktime for this"), _("transaction is set to"),
|
|
str_type, str_locktime, _("Continue?"), NULL);
|
|
}
|
|
}
|
|
|
|
void layoutAuthorizeCoinJoin(const CoinInfo *coin, uint64_t max_rounds,
|
|
uint32_t max_fee_per_kvbyte) {
|
|
char str_max_rounds[32] = {0};
|
|
char str_fee_rate[32] = {0};
|
|
bn_format_amount(max_rounds, NULL, NULL, 0, str_max_rounds,
|
|
sizeof(str_max_rounds));
|
|
formatFeeRate(max_fee_per_kvbyte, str_fee_rate, sizeof(str_fee_rate),
|
|
coin->has_segwit);
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"),
|
|
_("Authorize coinjoin"), _("Maximum rounds:"),
|
|
str_max_rounds, _("Maximum mining fee:"), str_fee_rate,
|
|
NULL, NULL);
|
|
}
|
|
|
|
void layoutConfirmCoinjoinAccess(void) {
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Do you want to allow"), _("access to your"),
|
|
_("coinjoin account?"), NULL, NULL, NULL);
|
|
}
|
|
|
|
void layoutVerifyAddress(const CoinInfo *coin, const char *address) {
|
|
render_address_dialog(coin, address, _("Confirm address?"),
|
|
_("Message signed by:"), 0);
|
|
}
|
|
|
|
void layoutCipherKeyValue(bool encrypt, const char *key) {
|
|
const char **str = split_message((const uint8_t *)key, strlen(key), 16);
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"),
|
|
encrypt ? _("Encrypt value of this key?")
|
|
: _("Decrypt value of this key?"),
|
|
str[0], str[1], str[2], str[3], NULL, NULL);
|
|
}
|
|
|
|
void layoutEncryptMessage(const uint8_t *msg, uint32_t len, bool signing) {
|
|
const char **str = split_message(msg, len, 16);
|
|
layoutDialogSwipe(
|
|
&bmp_icon_question, _("Cancel"), _("Confirm"),
|
|
signing ? _("Encrypt+Sign message?") : _("Encrypt message?"), str[0],
|
|
str[1], str[2], str[3], NULL, NULL);
|
|
}
|
|
|
|
void layoutDecryptMessage(const uint8_t *msg, uint32_t len,
|
|
const char *address) {
|
|
const char **str = split_message(msg, len, 16);
|
|
layoutDialogSwipe(
|
|
&bmp_icon_info, NULL, _("OK"),
|
|
address ? _("Decrypted signed message") : _("Decrypted message"), str[0],
|
|
str[1], str[2], str[3], NULL, NULL);
|
|
}
|
|
|
|
void layoutResetWord(const char *word, int pass, int word_pos, bool last) {
|
|
layoutLast = layoutResetWord;
|
|
layoutSwipe();
|
|
|
|
const char *btnYes = NULL;
|
|
if (last) {
|
|
if (pass == 1) {
|
|
btnYes = _("Finish");
|
|
} else {
|
|
btnYes = _("Again");
|
|
}
|
|
} else {
|
|
btnYes = _("Next");
|
|
}
|
|
|
|
const char *action = NULL;
|
|
if (pass == 1) {
|
|
action = _("Please check the seed");
|
|
} else {
|
|
action = _("Write down the seed");
|
|
}
|
|
|
|
char index_str[] = "##th word is:";
|
|
if (word_pos < 10) {
|
|
index_str[0] = ' ';
|
|
} else {
|
|
index_str[0] = '0' + word_pos / 10;
|
|
}
|
|
index_str[1] = '0' + word_pos % 10;
|
|
if (word_pos == 1 || word_pos == 21) {
|
|
index_str[2] = 's';
|
|
index_str[3] = 't';
|
|
} else if (word_pos == 2 || word_pos == 22) {
|
|
index_str[2] = 'n';
|
|
index_str[3] = 'd';
|
|
} else if (word_pos == 3 || word_pos == 23) {
|
|
index_str[2] = 'r';
|
|
index_str[3] = 'd';
|
|
}
|
|
|
|
int left = 0;
|
|
oledClear();
|
|
oledDrawBitmap(0, 0, &bmp_icon_info);
|
|
left = bmp_icon_info.width + 4;
|
|
|
|
oledDrawString(left, 0 * 9, action, FONT_STANDARD);
|
|
oledDrawString(left, 2 * 9, word_pos < 10 ? index_str + 1 : index_str,
|
|
FONT_STANDARD);
|
|
oledDrawStringCenter(OLED_WIDTH / 2, 4 * 9 - 3, word,
|
|
FONT_FIXED | FONT_DOUBLE);
|
|
// 30 is the maximum pixels used for a pixel row in the BIP39 word "abstract"
|
|
oledSCA(4 * 9 - 3 - 2, 4 * 9 - 3 + 15 + 2, 30);
|
|
oledInvert(0, 4 * 9 - 3 - 2, OLED_WIDTH - 1, 4 * 9 - 3 + 15 + 2);
|
|
layoutButtonYes(btnYes, &bmp_btn_confirm);
|
|
oledRefresh();
|
|
}
|
|
|
|
#define QR_MAX_VERSION 9
|
|
|
|
void layoutAddress(const char *address, const char *desc, bool qrcode,
|
|
bool ignorecase, const uint32_t *address_n,
|
|
size_t address_n_count, bool address_is_account) {
|
|
if (layoutLast != layoutAddress && layoutLast != layoutXPUBMultisig) {
|
|
layoutSwipe();
|
|
} else {
|
|
oledClear();
|
|
}
|
|
layoutLast = layoutAddress;
|
|
|
|
uint32_t addrlen = strlen(address);
|
|
if (qrcode) {
|
|
char address_upcase[addrlen + 1];
|
|
memset(address_upcase, 0, sizeof(address_upcase));
|
|
if (ignorecase) {
|
|
for (uint32_t i = 0; i < addrlen + 1; i++) {
|
|
address_upcase[i] = address[i] >= 'a' && address[i] <= 'z'
|
|
? address[i] + 'A' - 'a'
|
|
: address[i];
|
|
}
|
|
}
|
|
uint8_t codedata[qrcodegen_BUFFER_LEN_FOR_VERSION(QR_MAX_VERSION)] = {0};
|
|
uint8_t tempdata[qrcodegen_BUFFER_LEN_FOR_VERSION(QR_MAX_VERSION)] = {0};
|
|
|
|
int side = 0;
|
|
if (qrcodegen_encodeText(ignorecase ? address_upcase : address, tempdata,
|
|
codedata, qrcodegen_Ecc_LOW, qrcodegen_VERSION_MIN,
|
|
QR_MAX_VERSION, qrcodegen_Mask_AUTO, true)) {
|
|
side = qrcodegen_getSize(codedata);
|
|
}
|
|
|
|
oledInvert(0, 0, 63, 63);
|
|
if (side > 0 && side <= 29) {
|
|
int offset = 32 - side;
|
|
for (int i = 0; i < side; i++) {
|
|
for (int j = 0; j < side; j++) {
|
|
if (qrcodegen_getModule(codedata, i, j)) {
|
|
oledBox(offset + i * 2, offset + j * 2, offset + 1 + i * 2,
|
|
offset + 1 + j * 2, false);
|
|
}
|
|
}
|
|
}
|
|
} else if (side > 0 && side <= 60) {
|
|
int offset = 32 - (side / 2);
|
|
for (int i = 0; i < side; i++) {
|
|
for (int j = 0; j < side; j++) {
|
|
if (qrcodegen_getModule(codedata, i, j)) {
|
|
oledClearPixel(offset + i, offset + j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (desc) {
|
|
oledDrawString(0, 0 * 9, desc, FONT_STANDARD);
|
|
}
|
|
if (addrlen > 10) { // don't split short addresses
|
|
uint32_t rowcount = 4;
|
|
if (addrlen <= 42) {
|
|
rowcount = 2;
|
|
} else if (addrlen <= 63) {
|
|
rowcount = 3;
|
|
}
|
|
|
|
uint32_t rowlen = (addrlen - 1) / rowcount + 1;
|
|
const char **str =
|
|
split_message((const uint8_t *)address, addrlen, rowlen);
|
|
for (int i = 0; i < 4; i++) {
|
|
oledDrawString(0, (i + 1) * 9 + 4, str[i], FONT_FIXED);
|
|
}
|
|
} else {
|
|
oledDrawString(0, (0 + 1) * 9 + 4, address, FONT_FIXED);
|
|
}
|
|
oledDrawString(
|
|
0, 42, address_n_str(address_n, address_n_count, address_is_account),
|
|
FONT_STANDARD);
|
|
}
|
|
|
|
if (!qrcode) {
|
|
layoutButtonNo(_("QR Code"), NULL);
|
|
}
|
|
|
|
layoutButtonYes(_("Confirm"), &bmp_btn_confirm);
|
|
oledRefresh();
|
|
}
|
|
|
|
void layoutPublicKey(const uint8_t *pubkey) {
|
|
char desc[16] = {0};
|
|
strlcpy(desc, "Public Key: 00", sizeof(desc));
|
|
if (pubkey[0] == 1) {
|
|
/* ed25519 public key */
|
|
// pass - leave 00
|
|
} else {
|
|
data2hex(pubkey, 1, desc + 12);
|
|
}
|
|
const char **str = split_message_hex(pubkey + 1, 32 * 2);
|
|
layoutDialogSwipe(&bmp_icon_question, NULL, _("Continue"), NULL, desc, str[0],
|
|
str[1], str[2], str[3], NULL);
|
|
}
|
|
|
|
static void _layout_xpub(const char *xpub, const char *desc, int page) {
|
|
// 21 characters per line, 4 lines, minus 3 chars for "..." = 81
|
|
// skip 81 characters per page
|
|
xpub += page * 81;
|
|
const char **str = split_message((const uint8_t *)xpub, strlen(xpub), 21);
|
|
oledDrawString(0, 0 * 9, desc, FONT_STANDARD);
|
|
for (int i = 0; i < 4; i++) {
|
|
oledDrawString(0, (i + 1) * 9 + 4, str[i], FONT_FIXED);
|
|
}
|
|
}
|
|
|
|
void layoutXPUB(const char *xpub, int page) {
|
|
if (layoutLast != layoutAddress && layoutLast != layoutXPUB) {
|
|
layoutSwipe();
|
|
} else {
|
|
oledClear();
|
|
}
|
|
layoutLast = layoutXPUB;
|
|
char desc[] = "XPUB _/2";
|
|
desc[5] = '1' + page;
|
|
_layout_xpub(xpub, desc, page);
|
|
layoutButtonNo(_("Cancel"), &bmp_btn_cancel);
|
|
layoutButtonYes(_("Confirm"), &bmp_btn_confirm);
|
|
oledRefresh();
|
|
}
|
|
|
|
void layoutXPUBMultisig(const char *xpub, int index, int page, bool ours) {
|
|
if (layoutLast != layoutAddress && layoutLast != layoutXPUBMultisig) {
|
|
layoutSwipe();
|
|
} else {
|
|
oledClear();
|
|
}
|
|
layoutLast = layoutXPUBMultisig;
|
|
char desc[] = "XPUB #__ _/2 (________)";
|
|
if (index + 1 >= 10) {
|
|
desc[6] = '0' + (((index + 1) / 10) % 10);
|
|
desc[7] = '0' + ((index + 1) % 10);
|
|
} else {
|
|
desc[6] = '0' + ((index + 1) % 10);
|
|
desc[7] = ' ';
|
|
}
|
|
desc[9] = '1' + page;
|
|
if (ours) {
|
|
desc[14] = 'y';
|
|
desc[15] = 'o';
|
|
desc[16] = 'u';
|
|
desc[17] = 'r';
|
|
desc[18] = 's';
|
|
desc[19] = ')';
|
|
desc[20] = 0;
|
|
} else {
|
|
desc[14] = 'c';
|
|
desc[15] = 'o';
|
|
desc[16] = 's';
|
|
desc[17] = 'i';
|
|
desc[18] = 'g';
|
|
desc[19] = 'n';
|
|
desc[20] = 'e';
|
|
desc[21] = 'r';
|
|
}
|
|
_layout_xpub(xpub, desc, page);
|
|
layoutButtonNo(_("Next"), NULL);
|
|
layoutButtonYes(_("Confirm"), &bmp_btn_confirm);
|
|
oledRefresh();
|
|
}
|
|
|
|
void layoutSignIdentity(const IdentityType *identity, const char *challenge) {
|
|
char row_proto[8 + 11 + 1] = {0};
|
|
char row_hostport[64 + 6 + 1] = {0};
|
|
char row_user[64 + 8 + 1] = {0};
|
|
|
|
bool is_gpg = (strcmp(identity->proto, "gpg") == 0);
|
|
|
|
if (identity->has_proto && identity->proto[0]) {
|
|
if (strcmp(identity->proto, "https") == 0) {
|
|
strlcpy(row_proto, _("Web sign in to:"), sizeof(row_proto));
|
|
} else if (is_gpg) {
|
|
strlcpy(row_proto, _("GPG sign for:"), sizeof(row_proto));
|
|
} else {
|
|
strlcpy(row_proto, identity->proto, sizeof(row_proto));
|
|
char *p = row_proto;
|
|
while (*p) {
|
|
*p = toupper((int)*p);
|
|
p++;
|
|
}
|
|
strlcat(row_proto, _(" login to:"), sizeof(row_proto));
|
|
}
|
|
} else {
|
|
strlcpy(row_proto, _("Login to:"), sizeof(row_proto));
|
|
}
|
|
|
|
if (identity->has_host && identity->host[0]) {
|
|
strlcpy(row_hostport, identity->host, sizeof(row_hostport));
|
|
if (identity->has_port && identity->port[0]) {
|
|
strlcat(row_hostport, ":", sizeof(row_hostport));
|
|
strlcat(row_hostport, identity->port, sizeof(row_hostport));
|
|
}
|
|
} else {
|
|
row_hostport[0] = 0;
|
|
}
|
|
|
|
if (identity->has_user && identity->user[0]) {
|
|
strlcpy(row_user, _("user: "), sizeof(row_user));
|
|
strlcat(row_user, identity->user, sizeof(row_user));
|
|
} else {
|
|
row_user[0] = 0;
|
|
}
|
|
|
|
if (is_gpg) {
|
|
// Split "First Last <first@last.com>" into 2 lines:
|
|
// "First Last"
|
|
// "first@last.com"
|
|
char *email_start = strchr(row_hostport, '<');
|
|
if (email_start) {
|
|
strlcpy(row_user, email_start + 1, sizeof(row_user));
|
|
*email_start = 0;
|
|
char *email_end = strchr(row_user, '>');
|
|
if (email_end) {
|
|
*email_end = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"),
|
|
_("Do you want to sign in?"),
|
|
row_proto[0] ? row_proto : NULL,
|
|
row_hostport[0] ? row_hostport : NULL,
|
|
row_user[0] ? row_user : NULL, challenge, NULL, NULL);
|
|
}
|
|
|
|
void layoutDecryptIdentity(const IdentityType *identity) {
|
|
char row_proto[8 + 11 + 1] = {0};
|
|
char row_hostport[64 + 6 + 1] = {0};
|
|
char row_user[64 + 8 + 1] = {0};
|
|
|
|
if (identity->has_proto && identity->proto[0]) {
|
|
strlcpy(row_proto, identity->proto, sizeof(row_proto));
|
|
char *p = row_proto;
|
|
while (*p) {
|
|
*p = toupper((int)*p);
|
|
p++;
|
|
}
|
|
strlcat(row_proto, _(" decrypt for:"), sizeof(row_proto));
|
|
} else {
|
|
strlcpy(row_proto, _("Decrypt for:"), sizeof(row_proto));
|
|
}
|
|
|
|
if (identity->has_host && identity->host[0]) {
|
|
strlcpy(row_hostport, identity->host, sizeof(row_hostport));
|
|
if (identity->has_port && identity->port[0]) {
|
|
strlcat(row_hostport, ":", sizeof(row_hostport));
|
|
strlcat(row_hostport, identity->port, sizeof(row_hostport));
|
|
}
|
|
} else {
|
|
row_hostport[0] = 0;
|
|
}
|
|
|
|
if (identity->has_user && identity->user[0]) {
|
|
strlcpy(row_user, _("user: "), sizeof(row_user));
|
|
strlcat(row_user, identity->user, sizeof(row_user));
|
|
} else {
|
|
row_user[0] = 0;
|
|
}
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"),
|
|
_("Do you want to decrypt?"),
|
|
row_proto[0] ? row_proto : NULL,
|
|
row_hostport[0] ? row_hostport : NULL,
|
|
row_user[0] ? row_user : NULL, NULL, NULL, NULL);
|
|
}
|
|
|
|
#if U2F_ENABLED
|
|
|
|
void layoutU2FDialog(const char *verb, const char *appname) {
|
|
layoutDialog(&bmp_webauthn, NULL, verb, NULL, verb, _("U2F security key?"),
|
|
NULL, appname, NULL, NULL);
|
|
}
|
|
|
|
#endif
|
|
|
|
void layoutShowPassphrase(const char *passphrase) {
|
|
if (layoutLast != layoutShowPassphrase) {
|
|
layoutSwipe();
|
|
} else {
|
|
oledClear();
|
|
}
|
|
const char **str =
|
|
split_message((const uint8_t *)passphrase, strlen(passphrase), 21);
|
|
for (int i = 0; i < 3; i++) {
|
|
oledDrawString(0, i * 9 + 4, str[i], FONT_FIXED);
|
|
}
|
|
oledDrawStringCenter(OLED_WIDTH / 2, OLED_HEIGHT - 2 * 9 - 1,
|
|
_("Use this passphrase?"), FONT_STANDARD);
|
|
oledHLine(OLED_HEIGHT - 21);
|
|
layoutButtonNo(_("Cancel"), &bmp_btn_cancel);
|
|
layoutButtonYes(_("Confirm"), &bmp_btn_confirm);
|
|
oledRefresh();
|
|
}
|
|
|
|
#if !BITCOIN_ONLY
|
|
|
|
void layoutNEMDialog(const BITMAP *icon, const char *btnNo, const char *btnYes,
|
|
const char *desc, const char *line1, const char *address) {
|
|
static char first_third[NEM_ADDRESS_SIZE / 3 + 1];
|
|
strlcpy(first_third, address, sizeof(first_third));
|
|
|
|
static char second_third[NEM_ADDRESS_SIZE / 3 + 1];
|
|
strlcpy(second_third, &address[NEM_ADDRESS_SIZE / 3], sizeof(second_third));
|
|
|
|
const char *third_third = &address[NEM_ADDRESS_SIZE * 2 / 3];
|
|
|
|
layoutDialogSwipe(icon, btnNo, btnYes, desc, line1, first_third, second_third,
|
|
third_third, NULL, NULL);
|
|
}
|
|
|
|
void layoutNEMTransferXEM(const char *desc, uint64_t quantity,
|
|
const bignum256 *multiplier, uint64_t fee) {
|
|
char str_out[32] = {0}, str_fee[32] = {0};
|
|
|
|
nem_mosaicFormatAmount(NEM_MOSAIC_DEFINITION_XEM, quantity, multiplier,
|
|
str_out, sizeof(str_out));
|
|
nem_mosaicFormatAmount(NEM_MOSAIC_DEFINITION_XEM, fee, NULL, str_fee,
|
|
sizeof(str_fee));
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Next"), desc,
|
|
_("Confirm transfer of"), str_out, _("and network fee of"),
|
|
str_fee, NULL, NULL);
|
|
}
|
|
|
|
void layoutNEMNetworkFee(const char *desc, bool confirm, const char *fee1_desc,
|
|
uint64_t fee1, const char *fee2_desc, uint64_t fee2) {
|
|
char str_fee1[32] = {0}, str_fee2[32] = {0};
|
|
|
|
nem_mosaicFormatAmount(NEM_MOSAIC_DEFINITION_XEM, fee1, NULL, str_fee1,
|
|
sizeof(str_fee1));
|
|
|
|
if (fee2_desc) {
|
|
nem_mosaicFormatAmount(NEM_MOSAIC_DEFINITION_XEM, fee2, NULL, str_fee2,
|
|
sizeof(str_fee2));
|
|
}
|
|
|
|
layoutDialogSwipe(
|
|
&bmp_icon_question, _("Cancel"), confirm ? _("Confirm") : _("Next"), desc,
|
|
fee1_desc, str_fee1, fee2_desc, fee2_desc ? str_fee2 : NULL, NULL, NULL);
|
|
}
|
|
|
|
void layoutNEMTransferMosaic(const NEMMosaicDefinition *definition,
|
|
uint64_t quantity, const bignum256 *multiplier,
|
|
uint8_t network) {
|
|
char str_out[32] = {0}, str_levy[32] = {0};
|
|
|
|
nem_mosaicFormatAmount(definition, quantity, multiplier, str_out,
|
|
sizeof(str_out));
|
|
|
|
if (definition->has_levy) {
|
|
nem_mosaicFormatLevy(definition, quantity, multiplier, network, str_levy,
|
|
sizeof(str_levy));
|
|
}
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Next"),
|
|
definition->has_name ? definition->name : _("Mosaic"),
|
|
_("Confirm transfer of"), str_out,
|
|
definition->has_levy ? _("and levy of") : NULL,
|
|
definition->has_levy ? str_levy : NULL, NULL, NULL);
|
|
}
|
|
|
|
void layoutNEMTransferUnknownMosaic(const char *namespace, const char *mosaic,
|
|
uint64_t quantity,
|
|
const bignum256 *multiplier) {
|
|
char mosaic_name[32] = {0};
|
|
nem_mosaicFormatName(namespace, mosaic, mosaic_name, sizeof(mosaic_name));
|
|
|
|
char str_out[32] = {0};
|
|
nem_mosaicFormatAmount(NULL, quantity, multiplier, str_out, sizeof(str_out));
|
|
|
|
char *decimal = strchr(str_out, '.');
|
|
if (decimal != NULL) {
|
|
*decimal = '\0';
|
|
}
|
|
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("I take the risk"),
|
|
_("Unknown Mosaic"), _("Confirm transfer of"), str_out,
|
|
_("raw units of"), mosaic_name, NULL, NULL);
|
|
}
|
|
|
|
void layoutNEMTransferPayload(const uint8_t *payload, size_t length,
|
|
bool encrypted) {
|
|
if (length >= 1 && payload[0] == 0xFE) {
|
|
char encoded[(length - 1) * 2 + 1];
|
|
memset(encoded, 0, sizeof(encoded));
|
|
|
|
data2hex(&payload[1], length - 1, encoded);
|
|
|
|
const char **str =
|
|
split_message((uint8_t *)encoded, sizeof(encoded) - 1, 16);
|
|
layoutDialogSwipe(
|
|
&bmp_icon_question, _("Cancel"), _("Next"),
|
|
encrypted ? _("Encrypted hex data") : _("Unencrypted hex data"), str[0],
|
|
str[1], str[2], str[3], NULL, NULL);
|
|
} else {
|
|
const char **str = split_message(payload, length, 16);
|
|
layoutDialogSwipe(
|
|
&bmp_icon_question, _("Cancel"), _("Next"),
|
|
encrypted ? _("Encrypted message") : _("Unencrypted message"), str[0],
|
|
str[1], str[2], str[3], NULL, NULL);
|
|
}
|
|
}
|
|
|
|
void layoutNEMMosaicDescription(const char *description) {
|
|
const char **str =
|
|
split_message((uint8_t *)description, strlen(description), 16);
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Next"),
|
|
_("Mosaic Description"), str[0], str[1], str[2], str[3],
|
|
NULL, NULL);
|
|
}
|
|
|
|
void layoutNEMLevy(const NEMMosaicDefinition *definition, uint8_t network) {
|
|
const NEMMosaicDefinition *mosaic = NULL;
|
|
if (nem_mosaicMatches(definition, definition->levy_namespace,
|
|
definition->levy_mosaic, network)) {
|
|
mosaic = definition;
|
|
} else {
|
|
mosaic = nem_mosaicByName(definition->levy_namespace,
|
|
definition->levy_mosaic, network);
|
|
}
|
|
|
|
char mosaic_name[32] = {0};
|
|
if (mosaic == NULL) {
|
|
nem_mosaicFormatName(definition->levy_namespace, definition->levy_mosaic,
|
|
mosaic_name, sizeof(mosaic_name));
|
|
}
|
|
|
|
char str_out[32] = {0};
|
|
|
|
switch (definition->levy) {
|
|
case NEMMosaicLevy_MosaicLevy_Percentile:
|
|
bn_format_amount(definition->fee, NULL, NULL, 0, str_out,
|
|
sizeof(str_out));
|
|
|
|
layoutDialogSwipe(
|
|
&bmp_icon_question, _("Cancel"), _("Next"), _("Percentile Levy"),
|
|
_("Raw levy value is"), str_out, _("in"),
|
|
mosaic ? (mosaic == definition ? _("the same mosaic") : mosaic->name)
|
|
: mosaic_name,
|
|
NULL, NULL);
|
|
break;
|
|
|
|
case NEMMosaicLevy_MosaicLevy_Absolute:
|
|
default:
|
|
nem_mosaicFormatAmount(mosaic, definition->fee, NULL, str_out,
|
|
sizeof(str_out));
|
|
layoutDialogSwipe(
|
|
&bmp_icon_question, _("Cancel"), _("Next"), _("Absolute Levy"),
|
|
_("Levy is"), str_out,
|
|
mosaic ? (mosaic == definition ? _("in the same mosaic") : NULL)
|
|
: _("in raw units of"),
|
|
mosaic ? NULL : mosaic_name, NULL, NULL);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
static inline bool is_slip18(const uint32_t *address_n,
|
|
size_t address_n_count) {
|
|
// m / 10018' / [0-9]'
|
|
return address_n_count == 2 && address_n[0] == (PATH_HARDENED + 10018) &&
|
|
(address_n[1] & PATH_HARDENED) &&
|
|
(address_n[1] & PATH_UNHARDEN_MASK) <= 9;
|
|
}
|
|
|
|
void layoutCosiSign(const uint32_t *address_n, size_t address_n_count,
|
|
const uint8_t *data, uint32_t len) {
|
|
char *desc = _("CoSi sign message?");
|
|
char desc_buf[32] = {0};
|
|
if (is_slip18(address_n, address_n_count)) {
|
|
strlcpy(desc_buf, _("CoSi sign index #?"), sizeof(desc_buf));
|
|
desc_buf[16] = '0' + (address_n[1] & PATH_UNHARDEN_MASK);
|
|
desc = desc_buf;
|
|
}
|
|
char str[4][17] = {0};
|
|
if (len == 32) {
|
|
data2hex(data, 8, str[0]);
|
|
data2hex(data + 8, 8, str[1]);
|
|
data2hex(data + 16, 8, str[2]);
|
|
data2hex(data + 24, 8, str[3]);
|
|
} else {
|
|
strlcpy(str[0], "Data", sizeof(str[0]));
|
|
strlcpy(str[1], "of", sizeof(str[1]));
|
|
strlcpy(str[2], "unsupported", sizeof(str[2]));
|
|
strlcpy(str[3], "length", sizeof(str[3]));
|
|
}
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), desc, str[0],
|
|
str[1], str[2], str[3], NULL, NULL);
|
|
}
|
|
|
|
void layoutConfirmAutoLockDelay(uint32_t delay_ms) {
|
|
char line[sizeof("after 4294967296 minutes?")] = {0};
|
|
|
|
const char *unit = _("second");
|
|
uint32_t num = delay_ms / 1000U;
|
|
|
|
if (delay_ms >= 60 * 60 * 1000) {
|
|
unit = _("hour");
|
|
num /= 60 * 60U;
|
|
} else if (delay_ms >= 60 * 1000) {
|
|
unit = _("minute");
|
|
num /= 60U;
|
|
}
|
|
|
|
strlcpy(line, _("after "), sizeof(line));
|
|
size_t off = strlen(line);
|
|
bn_format_amount(num, NULL, NULL, 0, &line[off], sizeof(line) - off);
|
|
strlcat(line, " ", sizeof(line));
|
|
strlcat(line, unit, sizeof(line));
|
|
if (num > 1) {
|
|
strlcat(line, "s", sizeof(line));
|
|
}
|
|
strlcat(line, "?", sizeof(line));
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Do you really want to"), _("auto-lock your device"),
|
|
line, NULL, NULL, NULL);
|
|
}
|
|
|
|
void layoutConfirmSafetyChecks(SafetyCheckLevel safety_ckeck_level) {
|
|
if (safety_ckeck_level == SafetyCheckLevel_Strict) {
|
|
// Disallow unsafe actions. This is the default.
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Do you really want to"), _("enforce strict safety"),
|
|
_("checks?"), _("(Recommended.)"), NULL, NULL);
|
|
} else if (safety_ckeck_level == SafetyCheckLevel_PromptTemporarily) {
|
|
// Ask user before unsafe action. Reverts to Strict after reboot.
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Do you really want to"), _("be temporarily able"),
|
|
_("to approve some"), _("actions which might"),
|
|
_("be unsafe?"), NULL);
|
|
}
|
|
}
|
|
|
|
void layoutConfirmHash(const BITMAP *icon, const char *description,
|
|
const uint8_t *hash, uint32_t len) {
|
|
const char **str = split_message_hex(hash, len);
|
|
|
|
layoutSwipe();
|
|
oledClear();
|
|
oledDrawBitmap(0, 0, icon);
|
|
oledDrawString(20, 0 * 9, description, FONT_STANDARD);
|
|
oledDrawString(20, 1 * 9, str[0], FONT_FIXED);
|
|
oledDrawString(20, 2 * 9, str[1], FONT_FIXED);
|
|
oledDrawString(20, 3 * 9, str[2], FONT_FIXED);
|
|
oledDrawString(20, 4 * 9, str[3], FONT_FIXED);
|
|
oledHLine(OLED_HEIGHT - 13);
|
|
|
|
layoutButtonNo(_("Cancel"), &bmp_btn_cancel);
|
|
layoutButtonYes(_("Confirm"), &bmp_btn_confirm);
|
|
oledRefresh();
|
|
}
|
|
|
|
void layoutConfirmOwnershipProof(void) {
|
|
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
|
|
_("Do you want to"), _("create a proof of"),
|
|
_("ownership?"), NULL, NULL, NULL);
|
|
}
|