2
.gitignore
vendored
@@ -15,3 +15,5 @@ tests/node_modules
|
||||
tests/lib
|
||||
tests/yarn-error.log
|
||||
|
||||
|
||||
.vscode
|
||||
|
||||
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.9.0](https://github.com/ledgerhq/app-ethereum/compare/1.8.8...1.9.0) - 2021-8-05
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for EIP-1559 and EIP-2930 style transactions.
|
||||
|
||||
## [1.8.8](https://github.com/ledgerhq/app-ethereum/compare/1.8.7...1.8.8) - 2021-7-21
|
||||
|
||||
### Added
|
||||
|
||||
4
Makefile
@@ -29,8 +29,8 @@ APP_LOAD_PARAMS += --path "45'"
|
||||
APP_LOAD_PARAMS += --path "1517992542'/1101353413'"
|
||||
|
||||
APPVERSION_M=1
|
||||
APPVERSION_N=8
|
||||
APPVERSION_P=8
|
||||
APPVERSION_N=9
|
||||
APPVERSION_P=0
|
||||
APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
|
||||
APP_LOAD_FLAGS= --appFlags 0x240 --dep Ethereum:$(APPVERSION)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ Ethereum application Plugins : Technical Specifications
|
||||
Ledger Firmware Team <hello@ledger.fr>
|
||||
Specification version 1.0 - 24th of September 2020
|
||||
|
||||
|
||||
## 1.0
|
||||
- Initial release
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ except:
|
||||
# Python3 hack import for pyethereum
|
||||
from ethereum.utils import decode_hex, encode_hex, str_to_bytes
|
||||
|
||||
|
||||
def parse_bip32_path(path):
|
||||
if len(path) == 0:
|
||||
return b""
|
||||
@@ -52,14 +53,18 @@ def parse_bip32_path(path):
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--nonce', help="Nonce associated to the account", type=int, required=True)
|
||||
parser.add_argument('--gasprice', help="Network gas price", type=int, required=True)
|
||||
parser.add_argument(
|
||||
'--nonce', help="Nonce associated to the account", type=int, required=True)
|
||||
parser.add_argument('--gasprice', help="Network gas price",
|
||||
type=int, required=True)
|
||||
parser.add_argument('--startgas', help="startgas", default='21000', type=int)
|
||||
parser.add_argument('--amount', help="Amount to send in ether", required=True)
|
||||
parser.add_argument('--to', help="Destination address", type=str, required=True)
|
||||
parser.add_argument('--to', help="Destination address",
|
||||
type=str, required=True)
|
||||
parser.add_argument('--path', help="BIP 32 path to sign with")
|
||||
parser.add_argument('--data', help="Data to add, hex encoded")
|
||||
parser.add_argument('--chainid', help="Chain ID (1 for Ethereum mainnet, 137 for Polygon, etc)", type=int)
|
||||
parser.add_argument(
|
||||
'--chainid', help="Chain ID (1 for Ethereum mainnet, 137 for Polygon, etc)", type=int)
|
||||
parser.add_argument('--descriptor', help="Optional descriptor")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -91,6 +96,8 @@ tx = UnsignedTransaction(
|
||||
)
|
||||
|
||||
encodedTx = encode(tx, UnsignedTransaction)
|
||||
# encodedTx = bytearray.fromhex(
|
||||
# "02ef0306843b9aca008504a817c80082520894b2bb2b958afa2e96dab3f3ce7162b87daea39017872386f26fc1000080c0")
|
||||
|
||||
# To test an EIP-2930 transaction, uncomment this line
|
||||
#encodedTx = bytearray.fromhex("01f8e60380018402625a0094cccccccccccccccccccccccccccccccccccccccc830186a0a4693c61390000000000000000000000000000000000000000000000000000000000000002f85bf859940000000000000000000000000000000000000102f842a00000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000060a780a09b8adcd2a4abd34b42d56fcd90b949f74ca9696dfe2b427bc39aa280bbf1924ca029af4a471bb2953b4e7933ea95880648552a9345424a1ac760189655ceb1832a")
|
||||
@@ -99,7 +106,8 @@ dongle = getDongle(True)
|
||||
|
||||
if args.descriptor != None:
|
||||
descriptor = binascii.unhexlify(args.descriptor)
|
||||
apdu = struct.pack(">BBBBB", 0xE0, 0x0A, 0x00, 0x00, len(descriptor)) + descriptor
|
||||
apdu = struct.pack(">BBBBB", 0xE0, 0x0A, 0x00, 0x00,
|
||||
len(descriptor)) + descriptor
|
||||
dongle.exchange(bytes(apdu))
|
||||
|
||||
donglePath = parse_bip32_path(args.path)
|
||||
@@ -111,16 +119,16 @@ apdu += donglePath + encodedTx
|
||||
result = dongle.exchange(bytes(apdu))
|
||||
|
||||
# Needs to recover (main.c:1121)
|
||||
if (CHAIN_ID*2 + 35) + 1 > 255:
|
||||
ecc_parity = result[0] - ((CHAIN_ID*2 + 35) % 256)
|
||||
v = (CHAIN_ID*2 + 35) + ecc_parity
|
||||
else:
|
||||
v = result[0]
|
||||
# if (CHAIN_ID*2 + 35) + 1 > 255:
|
||||
# ecc_parity = result[0] - ((CHAIN_ID*2 + 35) % 256)
|
||||
# v = (CHAIN_ID*2 + 35) + ecc_parity
|
||||
# else:
|
||||
# v = result[0]
|
||||
|
||||
r = int(binascii.hexlify(result[1:1 + 32]), 16)
|
||||
s = int(binascii.hexlify(result[1 + 32: 1 + 32 + 32]), 16)
|
||||
# r = int(binascii.hexlify(result[1:1 + 32]), 16)
|
||||
# s = int(binascii.hexlify(result[1 + 32: 1 + 32 + 32]), 16)
|
||||
|
||||
tx = Transaction(tx.nonce, tx.gasprice, tx.startgas,
|
||||
tx.to, tx.value, tx.data, v, r, s)
|
||||
# tx = Transaction(tx.nonce, tx.gasprice, tx.startgas,
|
||||
# tx.to, tx.value, tx.data, v, r, s)
|
||||
|
||||
print("Signed transaction", encode_hex(encode(tx)))
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
|
||||
#define PLUGIN_ID_LENGTH 30
|
||||
|
||||
// Interface version. To be updated everytime we introduce breaking changes to the plugin interface.
|
||||
typedef enum {
|
||||
ETH_PLUGIN_INTERFACE_VERSION_1 = 1 // Version 1
|
||||
ETH_PLUGIN_INTERFACE_VERSION_1 = 1, // Version 1
|
||||
} eth_plugin_interface_version_t;
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -5,184 +5,11 @@
|
||||
#include "ui_callbacks.h"
|
||||
#include "eth_plugin_handler.h"
|
||||
#include "ux.h"
|
||||
|
||||
typedef enum {
|
||||
|
||||
PLUGIN_UI_INSIDE = 0,
|
||||
PLUGIN_UI_OUTSIDE
|
||||
|
||||
} plugin_ui_state_t;
|
||||
|
||||
#ifdef TARGET_NANOS
|
||||
// This function is not exported by the SDK
|
||||
void ux_layout_paging_redisplay_by_addr(unsigned int stack_slot);
|
||||
#endif
|
||||
|
||||
void computeFees(char *displayBuffer, uint32_t displayBufferSize);
|
||||
|
||||
void plugin_ui_get_id() {
|
||||
ethQueryContractID_t pluginQueryContractID;
|
||||
eth_plugin_prepare_query_contract_ID(&pluginQueryContractID,
|
||||
strings.tmp.tmp,
|
||||
sizeof(strings.tmp.tmp),
|
||||
strings.tmp.tmp2,
|
||||
sizeof(strings.tmp.tmp2));
|
||||
// Query the original contract for ID if it's not an internal alias
|
||||
eth_plugin_result_t status =
|
||||
eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_ID, (void *) &pluginQueryContractID);
|
||||
if (status != ETH_PLUGIN_RESULT_OK) {
|
||||
PRINTF("Plugin query contract ID call failed\n");
|
||||
io_seproxyhal_touch_tx_cancel(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void plugin_ui_get_item() {
|
||||
ethQueryContractUI_t pluginQueryContractUI;
|
||||
eth_plugin_prepare_query_contract_UI(&pluginQueryContractUI,
|
||||
dataContext.tokenContext.pluginUiCurrentItem,
|
||||
strings.tmp.tmp,
|
||||
sizeof(strings.tmp.tmp),
|
||||
strings.tmp.tmp2,
|
||||
sizeof(strings.tmp.tmp2));
|
||||
eth_plugin_result_t status =
|
||||
eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_UI, (void *) &pluginQueryContractUI);
|
||||
if (status != ETH_PLUGIN_RESULT_OK) {
|
||||
PRINTF("Plugin query contract UI call failed, got: %d\n", status);
|
||||
io_seproxyhal_touch_tx_cancel(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void display_next_plugin_item(bool entering) {
|
||||
if (entering) {
|
||||
if (dataContext.tokenContext.pluginUiState == PLUGIN_UI_OUTSIDE) {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_INSIDE;
|
||||
dataContext.tokenContext.pluginUiCurrentItem = 0;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_next();
|
||||
} else {
|
||||
if (dataContext.tokenContext.pluginUiCurrentItem > 0) {
|
||||
dataContext.tokenContext.pluginUiCurrentItem--;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_next();
|
||||
} else {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_OUTSIDE;
|
||||
dataContext.tokenContext.pluginUiCurrentItem = 0;
|
||||
ux_flow_prev();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (dataContext.tokenContext.pluginUiState == PLUGIN_UI_OUTSIDE) {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_INSIDE;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_prev();
|
||||
} else {
|
||||
if (dataContext.tokenContext.pluginUiCurrentItem <
|
||||
dataContext.tokenContext.pluginUiMaxItems - 1) {
|
||||
dataContext.tokenContext.pluginUiCurrentItem++;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_prev();
|
||||
// Reset multi page layout to the first page
|
||||
G_ux.layout_paging.current = 0;
|
||||
#ifdef TARGET_NANOS
|
||||
ux_layout_paging_redisplay_by_addr(G_ux.stack_count - 1);
|
||||
#else
|
||||
ux_layout_bnnn_paging_redisplay(0);
|
||||
#endif
|
||||
} else {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_OUTSIDE;
|
||||
ux_flow_next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void plugin_ui_compute_fees() {
|
||||
computeFees(strings.common.maxFee, sizeof(strings.common.maxFee));
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
UX_FLOW_DEF_NOCB(
|
||||
ux_plugin_approval_intro_step,
|
||||
pnn,
|
||||
{
|
||||
&C_icon_eye,
|
||||
"Review",
|
||||
"contract call",
|
||||
});
|
||||
|
||||
UX_STEP_NOCB_INIT(
|
||||
ux_plugin_approval_id_step,
|
||||
bnnn_paging,
|
||||
plugin_ui_get_id(),
|
||||
{
|
||||
.title = strings.tmp.tmp,
|
||||
.text = strings.tmp.tmp2
|
||||
});
|
||||
|
||||
UX_STEP_INIT(
|
||||
ux_plugin_approval_before_step,
|
||||
NULL,
|
||||
NULL,
|
||||
{
|
||||
display_next_plugin_item(true);
|
||||
});
|
||||
|
||||
UX_FLOW_DEF_NOCB(
|
||||
ux_plugin_approval_display_step,
|
||||
bnnn_paging,
|
||||
{
|
||||
.title = strings.tmp.tmp,
|
||||
.text = strings.tmp.tmp2
|
||||
});
|
||||
|
||||
UX_STEP_INIT(
|
||||
ux_plugin_approval_after_step,
|
||||
NULL,
|
||||
NULL,
|
||||
{
|
||||
display_next_plugin_item(false);
|
||||
});
|
||||
|
||||
UX_STEP_NOCB_INIT(
|
||||
ux_plugin_approval_fees_step,
|
||||
bnnn_paging,
|
||||
plugin_ui_compute_fees(),
|
||||
{
|
||||
.title = "Max Fees",
|
||||
.text = strings.common.maxFee
|
||||
});
|
||||
|
||||
UX_FLOW_DEF_VALID(
|
||||
ux_plugin_approval_ok_step,
|
||||
pbb,
|
||||
io_seproxyhal_touch_tx_ok(NULL),
|
||||
{
|
||||
&C_icon_validate_14,
|
||||
"Accept",
|
||||
"and send",
|
||||
});
|
||||
UX_FLOW_DEF_VALID(
|
||||
ux_plugin_approval_cancel_step,
|
||||
pb,
|
||||
io_seproxyhal_touch_tx_cancel(NULL),
|
||||
{
|
||||
&C_icon_crossmark,
|
||||
"Reject",
|
||||
});
|
||||
// clang-format on
|
||||
|
||||
UX_FLOW(ux_plugin_approval_flow,
|
||||
&ux_plugin_approval_intro_step,
|
||||
&ux_plugin_approval_id_step,
|
||||
&ux_plugin_approval_before_step,
|
||||
&ux_plugin_approval_display_step,
|
||||
&ux_plugin_approval_after_step,
|
||||
&ux_plugin_approval_fees_step,
|
||||
&ux_plugin_approval_ok_step,
|
||||
&ux_plugin_approval_cancel_step);
|
||||
#include "feature_signTx.h"
|
||||
|
||||
void plugin_ui_start() {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_OUTSIDE;
|
||||
dataContext.tokenContext.pluginUiCurrentItem = 0;
|
||||
ux_flow_init(0, ux_plugin_approval_flow, NULL);
|
||||
|
||||
ux_approve_tx(true);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ void handle_swap_sign_transaction(chain_config_t* config) {
|
||||
storage.contractDetails = 0x00;
|
||||
storage.initialized = 0x01;
|
||||
storage.displayNonce = 0x00;
|
||||
storage.contractDetails = 0x00;
|
||||
nvm_write((void*) &N_storage, (void*) &storage, sizeof(internalStorage_t));
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ strings_t strings;
|
||||
cx_sha3_t global_sha3;
|
||||
|
||||
uint8_t appState;
|
||||
bool dataPresent;
|
||||
bool called_from_swap;
|
||||
bool externalPluginIsSet;
|
||||
#ifdef HAVE_STARKWARE
|
||||
|
||||
@@ -196,7 +196,6 @@ extern cx_sha3_t global_sha3;
|
||||
extern const internalStorage_t N_storage_real;
|
||||
|
||||
extern bool called_from_swap;
|
||||
extern bool dataPresent;
|
||||
extern bool externalPluginIsSet;
|
||||
extern uint8_t appState;
|
||||
#ifdef HAVE_STARKWARE
|
||||
|
||||
@@ -31,10 +31,7 @@ void initTx(txContext_t *context,
|
||||
txContent_t *content,
|
||||
ustreamProcess_t customProcessor,
|
||||
void *extra) {
|
||||
uint8_t save = context->txType;
|
||||
|
||||
memset(context, 0, sizeof(txContext_t));
|
||||
context->txType = save;
|
||||
context->sha3 = sha3;
|
||||
context->content = content;
|
||||
context->customProcessor = customProcessor;
|
||||
@@ -92,7 +89,7 @@ static void processContent(txContext_t *context) {
|
||||
|
||||
static void processAccessList(txContext_t *context) {
|
||||
if (!context->currentFieldIsList) {
|
||||
PRINTF("Invalid type for RLP_DATA\n");
|
||||
PRINTF("Invalid type for RLP_ACCESS_LIST\n");
|
||||
THROW(EXCEPTION);
|
||||
}
|
||||
if (context->currentFieldPos < context->currentFieldLength) {
|
||||
@@ -262,6 +259,26 @@ static void processData(txContext_t *context) {
|
||||
PRINTF("Invalid type for RLP_DATA\n");
|
||||
THROW(EXCEPTION);
|
||||
}
|
||||
if (context->currentFieldPos < context->currentFieldLength) {
|
||||
uint32_t copySize =
|
||||
MIN(context->commandLength, context->currentFieldLength - context->currentFieldPos);
|
||||
// If there is no data, set dataPresent to false.
|
||||
if (copySize == 1 && *context->workBuffer == 0x00) {
|
||||
context->content->dataPresent = false;
|
||||
}
|
||||
copyTxData(context, NULL, copySize);
|
||||
}
|
||||
if (context->currentFieldPos == context->currentFieldLength) {
|
||||
context->currentField++;
|
||||
context->processingField = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void processAndDiscard(txContext_t *context) {
|
||||
if (context->currentFieldIsList) {
|
||||
PRINTF("Invalid type for Discarded field\n");
|
||||
THROW(EXCEPTION);
|
||||
}
|
||||
if (context->currentFieldPos < context->currentFieldLength) {
|
||||
uint32_t copySize =
|
||||
MIN(context->commandLength, context->currentFieldLength - context->currentFieldPos);
|
||||
@@ -294,6 +311,62 @@ static void processV(txContext_t *context) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool processEIP1559Tx(txContext_t *context) {
|
||||
switch (context->currentField) {
|
||||
case EIP1559_RLP_CONTENT: {
|
||||
processContent(context);
|
||||
if ((context->processingFlags & TX_FLAG_TYPE) == 0) {
|
||||
context->currentField++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// This gets hit only by Wanchain
|
||||
case EIP1559_RLP_TYPE: {
|
||||
processType(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_CHAINID: {
|
||||
processChainID(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_NONCE: {
|
||||
processNonce(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_MAX_FEE_PER_GAS: {
|
||||
processGasprice(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_GASLIMIT: {
|
||||
processGasLimit(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_TO: {
|
||||
processTo(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_VALUE: {
|
||||
processValue(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_DATA: {
|
||||
processData(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_ACCESS_LIST: {
|
||||
processAccessList(context);
|
||||
break;
|
||||
}
|
||||
case EIP1559_RLP_MAX_PRIORITY_FEE_PER_GAS:
|
||||
processAndDiscard(context);
|
||||
break;
|
||||
default:
|
||||
PRINTF("Invalid RLP decoder context\n");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool processEIP2930Tx(txContext_t *context) {
|
||||
switch (context->currentField) {
|
||||
case EIP2930_RLP_CONTENT:
|
||||
@@ -302,6 +375,7 @@ static bool processEIP2930Tx(txContext_t *context) {
|
||||
context->currentField++;
|
||||
}
|
||||
break;
|
||||
// This gets hit only by Wanchain
|
||||
case EIP2930_RLP_TYPE:
|
||||
processType(context);
|
||||
break;
|
||||
@@ -323,17 +397,12 @@ static bool processEIP2930Tx(txContext_t *context) {
|
||||
case EIP2930_RLP_VALUE:
|
||||
processValue(context);
|
||||
break;
|
||||
case EIP2930_RLP_YPARITY:
|
||||
processV(context);
|
||||
case EIP2930_RLP_DATA:
|
||||
processData(context);
|
||||
break;
|
||||
case EIP2930_RLP_ACCESS_LIST:
|
||||
processAccessList(context);
|
||||
break;
|
||||
case EIP2930_RLP_DATA:
|
||||
case EIP2930_RLP_SENDER_R:
|
||||
case EIP2930_RLP_SENDER_S:
|
||||
processData(context);
|
||||
break;
|
||||
default:
|
||||
PRINTF("Invalid RLP decoder context\n");
|
||||
return true;
|
||||
@@ -349,6 +418,7 @@ static bool processLegacyTx(txContext_t *context) {
|
||||
context->currentField++;
|
||||
}
|
||||
break;
|
||||
// This gets hit only by Wanchain
|
||||
case LEGACY_RLP_TYPE:
|
||||
processType(context);
|
||||
break;
|
||||
@@ -368,9 +438,11 @@ static bool processLegacyTx(txContext_t *context) {
|
||||
processValue(context);
|
||||
break;
|
||||
case LEGACY_RLP_DATA:
|
||||
processData(context);
|
||||
break;
|
||||
case LEGACY_RLP_R:
|
||||
case LEGACY_RLP_S:
|
||||
processData(context);
|
||||
processAndDiscard(context);
|
||||
break;
|
||||
case LEGACY_RLP_V:
|
||||
processV(context);
|
||||
@@ -417,6 +489,7 @@ static parserStatus_e parseRLP(txContext_t *context) {
|
||||
PRINTF("RLP decode error\n");
|
||||
return USTREAM_FAULT;
|
||||
}
|
||||
// Ready to process this field
|
||||
if (offset == 0) {
|
||||
// Hack for single byte, self encoded
|
||||
context->workBuffer--;
|
||||
@@ -438,9 +511,14 @@ static parserStatus_e processTxInternal(txContext_t *context) {
|
||||
if (PARSING_IS_DONE(context)) {
|
||||
return USTREAM_FINISHED;
|
||||
}
|
||||
// Old style transaction
|
||||
if (((context->txType == LEGACY && context->currentField == LEGACY_RLP_V) ||
|
||||
(context->txType == EIP2930 && context->currentField == EIP2930_RLP_YPARITY)) &&
|
||||
// Old style transaction (pre EIP-155). Transations could just skip `v,r,s` so we needed to
|
||||
// cut parsing here. commandLength == 0 could happen in two cases :
|
||||
// 1. We are in an old style transaction : just return `USTREAM_FINISHED`.
|
||||
// 2. We are at the end of an APDU in a multi-apdu process. This would make us return
|
||||
// `USTREAM_FINISHED` preemptively. Case number 2 should NOT happen as it is up to
|
||||
// `ledgerjs` to correctly decrease the size of the APDU (`commandLength`) so that this
|
||||
// situation doesn't happen.
|
||||
if ((context->txType == LEGACY && context->currentField == LEGACY_RLP_V) &&
|
||||
(context->commandLength == 0)) {
|
||||
context->content->vLength = 0;
|
||||
return USTREAM_FINISHED;
|
||||
@@ -471,7 +549,7 @@ static parserStatus_e processTxInternal(txContext_t *context) {
|
||||
}
|
||||
}
|
||||
if (customStatus == CUSTOM_NOT_HANDLED) {
|
||||
PRINTF("Current field: %u\n", context->currentField);
|
||||
PRINTF("Current field: %d\n", context->currentField);
|
||||
switch (context->txType) {
|
||||
bool fault;
|
||||
case LEGACY:
|
||||
@@ -488,6 +566,13 @@ static parserStatus_e processTxInternal(txContext_t *context) {
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case EIP1559:
|
||||
fault = processEIP1559Tx(context);
|
||||
if (fault) {
|
||||
return USTREAM_FAULT;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
PRINTF("Transaction type %u is not supported\n", context->txType);
|
||||
return USTREAM_FAULT;
|
||||
|
||||
@@ -42,14 +42,15 @@ typedef customStatus_e (*ustreamProcess_t)(struct txContext_t *context);
|
||||
// First variant of every Tx enum.
|
||||
#define RLP_NONE 0
|
||||
|
||||
#define PARSING_IS_DONE(ctx) \
|
||||
((ctx->txType == LEGACY && ctx->currentField == LEGACY_RLP_DONE) || \
|
||||
(ctx->txType == EIP2930 && ctx->currentField == EIP2930_RLP_DONE))
|
||||
#define PARSING_IS_DONE(ctx) \
|
||||
((ctx->txType == LEGACY && ctx->currentField == LEGACY_RLP_DONE) || \
|
||||
(ctx->txType == EIP2930 && ctx->currentField == EIP2930_RLP_DONE) || \
|
||||
(ctx->txType == EIP1559 && ctx->currentField == EIP1559_RLP_DONE))
|
||||
|
||||
typedef enum rlpLegacyTxField_e {
|
||||
LEGACY_RLP_NONE = RLP_NONE,
|
||||
LEGACY_RLP_CONTENT,
|
||||
LEGACY_RLP_TYPE,
|
||||
LEGACY_RLP_TYPE, // For wanchain
|
||||
LEGACY_RLP_NONCE,
|
||||
LEGACY_RLP_GASPRICE,
|
||||
LEGACY_RLP_STARTGAS,
|
||||
@@ -65,7 +66,7 @@ typedef enum rlpLegacyTxField_e {
|
||||
typedef enum rlpEIP2930TxField_e {
|
||||
EIP2930_RLP_NONE = RLP_NONE,
|
||||
EIP2930_RLP_CONTENT,
|
||||
EIP2930_RLP_TYPE,
|
||||
EIP2930_RLP_TYPE, // For wanchain
|
||||
EIP2930_RLP_CHAINID,
|
||||
EIP2930_RLP_NONCE,
|
||||
EIP2930_RLP_GASPRICE,
|
||||
@@ -74,12 +75,25 @@ typedef enum rlpEIP2930TxField_e {
|
||||
EIP2930_RLP_VALUE,
|
||||
EIP2930_RLP_DATA,
|
||||
EIP2930_RLP_ACCESS_LIST,
|
||||
EIP2930_RLP_YPARITY,
|
||||
EIP2930_RLP_SENDER_R,
|
||||
EIP2930_RLP_SENDER_S,
|
||||
EIP2930_RLP_DONE
|
||||
} rlpEIP2930TxField_e;
|
||||
|
||||
typedef enum rlpEIP1559TxField_e {
|
||||
EIP1559_RLP_NONE = RLP_NONE,
|
||||
EIP1559_RLP_CONTENT,
|
||||
EIP1559_RLP_TYPE, // For wanchain
|
||||
EIP1559_RLP_CHAINID,
|
||||
EIP1559_RLP_NONCE,
|
||||
EIP1559_RLP_MAX_PRIORITY_FEE_PER_GAS,
|
||||
EIP1559_RLP_MAX_FEE_PER_GAS,
|
||||
EIP1559_RLP_GASLIMIT,
|
||||
EIP1559_RLP_TO,
|
||||
EIP1559_RLP_VALUE,
|
||||
EIP1559_RLP_DATA,
|
||||
EIP1559_RLP_ACCESS_LIST,
|
||||
EIP1559_RLP_DONE
|
||||
} rlpEIP1559TxField_e;
|
||||
|
||||
#define MIN_TX_TYPE 0x00
|
||||
#define MAX_TX_TYPE 0x7f
|
||||
|
||||
@@ -87,6 +101,7 @@ typedef enum rlpEIP2930TxField_e {
|
||||
// Valid transaction types should be in [0x00, 0x7f]
|
||||
typedef enum txType_e {
|
||||
EIP2930 = 0x01,
|
||||
EIP1559 = 0x02,
|
||||
LEGACY = 0xc0 // Legacy tx are greater than or equal to 0xc0.
|
||||
} txType_e;
|
||||
|
||||
@@ -104,8 +119,8 @@ typedef struct txInt256_t {
|
||||
} txInt256_t;
|
||||
|
||||
typedef struct txContent_t {
|
||||
txInt256_t gasprice;
|
||||
txInt256_t startgas;
|
||||
txInt256_t gasprice; // Used as MaxFeePerGas when dealing with EIP1559 transactions.
|
||||
txInt256_t startgas; // Also known as `gasLimit`.
|
||||
txInt256_t value;
|
||||
txInt256_t nonce;
|
||||
txInt256_t chainID;
|
||||
@@ -113,6 +128,7 @@ typedef struct txContent_t {
|
||||
uint8_t destinationLength;
|
||||
uint8_t v[4];
|
||||
uint8_t vLength;
|
||||
bool dataPresent;
|
||||
} txContent_t;
|
||||
|
||||
typedef struct txContext_t {
|
||||
|
||||
@@ -29,6 +29,7 @@ uint32_t get_chain_id(void) {
|
||||
chain_id = u32_from_BE(txContext.content->v, txContext.content->vLength, true);
|
||||
break;
|
||||
case EIP2930:
|
||||
case EIP1559:
|
||||
chain_id = u32_from_BE(tmpContent.txContent.chainID.value,
|
||||
tmpContent.txContent.chainID.length,
|
||||
true);
|
||||
|
||||
@@ -38,14 +38,17 @@ void handleSign(uint8_t p1,
|
||||
workBuffer += 4;
|
||||
dataLength -= 4;
|
||||
}
|
||||
dataPresent = false;
|
||||
tmpContent.txContent.dataPresent = false;
|
||||
dataContext.tokenContext.pluginStatus = ETH_PLUGIN_RESULT_UNAVAILABLE;
|
||||
|
||||
initTx(&txContext, &global_sha3, &tmpContent.txContent, customProcessor, NULL);
|
||||
|
||||
// EIP 2718: TransactionType might be present before the TransactionPayload.
|
||||
uint8_t txType = *workBuffer;
|
||||
if (txType >= MIN_TX_TYPE && txType <= MAX_TX_TYPE) {
|
||||
// Enumerate through all supported txTypes here...
|
||||
if (txType == EIP2930) {
|
||||
if (txType == EIP2930 || txType == EIP1559) {
|
||||
cx_hash((cx_hash_t *) &global_sha3, 0, workBuffer, 1, NULL, 0);
|
||||
txContext.txType = txType;
|
||||
workBuffer++;
|
||||
dataLength--;
|
||||
@@ -56,7 +59,7 @@ void handleSign(uint8_t p1,
|
||||
} else {
|
||||
txContext.txType = LEGACY;
|
||||
}
|
||||
initTx(&txContext, &global_sha3, &tmpContent.txContent, customProcessor, NULL);
|
||||
PRINTF("TxType: %d\n", txContext.txType);
|
||||
} else if (p1 != P1_MORE) {
|
||||
THROW(0x6B00);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
#include "shared_context.h"
|
||||
|
||||
typedef enum {
|
||||
|
||||
PLUGIN_UI_INSIDE = 0,
|
||||
PLUGIN_UI_OUTSIDE
|
||||
|
||||
} plugin_ui_state_t;
|
||||
|
||||
customStatus_e customProcessor(txContext_t *context);
|
||||
void finalizeParsing(bool direct);
|
||||
void ux_approve_tx(bool dataPresent);
|
||||
void prepareFeeDisplay();
|
||||
void prepareNetworkDisplay();
|
||||
void ux_approve_tx(bool fromPlugin);
|
||||
|
||||
@@ -31,9 +31,10 @@ uint32_t splitBinaryParameterPart(char *result, uint8_t *parameter) {
|
||||
|
||||
customStatus_e customProcessor(txContext_t *context) {
|
||||
if (((context->txType == LEGACY && context->currentField == LEGACY_RLP_DATA) ||
|
||||
(context->txType == EIP2930 && context->currentField == EIP2930_RLP_DATA)) &&
|
||||
(context->txType == EIP2930 && context->currentField == EIP2930_RLP_DATA) ||
|
||||
(context->txType == EIP1559 && context->currentField == EIP1559_RLP_DATA)) &&
|
||||
(context->currentFieldLength != 0)) {
|
||||
dataPresent = true;
|
||||
context->content->dataPresent = true;
|
||||
// If handling a new contract rather than a function call, abort immediately
|
||||
if (tmpContent.txContent.destinationLength == 0) {
|
||||
return CUSTOM_NOT_HANDLED;
|
||||
@@ -195,27 +196,25 @@ void reportFinalizeError(bool direct) {
|
||||
}
|
||||
}
|
||||
|
||||
void computeFees(char *displayBuffer, uint32_t displayBufferSize) {
|
||||
uint256_t gasPrice, startGas, uint256;
|
||||
// Convert `BEgasPrice` and `BEgasLimit` to Uint256 and then stores the multiplication of both in
|
||||
// `output`.
|
||||
static void computeFees(txInt256_t *BEgasPrice, txInt256_t *BEgasLimit, uint256_t *output) {
|
||||
uint256_t gasPrice = {0};
|
||||
uint256_t gasLimit = {0};
|
||||
|
||||
PRINTF("Gas price %.*H\n", BEgasPrice->length, BEgasPrice->value);
|
||||
PRINTF("Gas limit %.*H\n", BEgasLimit->length, BEgasLimit->value);
|
||||
convertUint256BE(BEgasPrice->value, BEgasPrice->length, &gasPrice);
|
||||
convertUint256BE(BEgasLimit->value, BEgasLimit->length, &gasLimit);
|
||||
mul256(&gasPrice, &gasLimit, output);
|
||||
}
|
||||
|
||||
static void feesToString(uint256_t *rawFee, char *displayBuffer, uint32_t displayBufferSize) {
|
||||
char *feeTicker = get_network_ticker();
|
||||
uint8_t tickerOffset = 0;
|
||||
uint32_t i;
|
||||
|
||||
PRINTF("Max fee\n");
|
||||
PRINTF("Gasprice %.*H\n",
|
||||
tmpContent.txContent.gasprice.length,
|
||||
tmpContent.txContent.gasprice.value);
|
||||
PRINTF("Startgas %.*H\n",
|
||||
tmpContent.txContent.startgas.length,
|
||||
tmpContent.txContent.startgas.value);
|
||||
convertUint256BE(tmpContent.txContent.gasprice.value,
|
||||
tmpContent.txContent.gasprice.length,
|
||||
&gasPrice);
|
||||
convertUint256BE(tmpContent.txContent.startgas.value,
|
||||
tmpContent.txContent.startgas.length,
|
||||
&startGas);
|
||||
mul256(&gasPrice, &startGas, &uint256);
|
||||
tostring256(&uint256, 10, (char *) (G_io_apdu_buffer + 100), 100);
|
||||
tostring256(rawFee, 10, (char *) (G_io_apdu_buffer + 100), 100);
|
||||
i = 0;
|
||||
while (G_io_apdu_buffer[100 + i]) {
|
||||
i++;
|
||||
@@ -237,6 +236,61 @@ void computeFees(char *displayBuffer, uint32_t displayBufferSize) {
|
||||
i++;
|
||||
}
|
||||
displayBuffer[tickerOffset + i] = '\0';
|
||||
PRINTF("Displayed fees: %s\n", displayBuffer);
|
||||
}
|
||||
|
||||
// Compute the fees, transform it to a string, prepend a ticker to it and copy everything to
|
||||
// `displayBuffer`.
|
||||
void prepareAndCopyFees(txInt256_t *BEGasPrice,
|
||||
txInt256_t *BEGasLimit,
|
||||
char *displayBuffer,
|
||||
uint32_t displayBufferSize) {
|
||||
uint256_t rawFee = {0};
|
||||
computeFees(BEGasPrice, BEGasLimit, &rawFee);
|
||||
feesToString(&rawFee, displayBuffer, displayBufferSize);
|
||||
}
|
||||
|
||||
void prepareFeeDisplay() {
|
||||
prepareAndCopyFees(&tmpContent.txContent.gasprice,
|
||||
&tmpContent.txContent.startgas,
|
||||
strings.common.maxFee,
|
||||
sizeof(strings.common.maxFee));
|
||||
}
|
||||
|
||||
uint32_t get_chainID() {
|
||||
uint32_t chain_id = 0;
|
||||
|
||||
if (txContext.txType == LEGACY) {
|
||||
chain_id = u32_from_BE(txContext.content->v, txContext.content->vLength, true);
|
||||
} else if (txContext.txType == EIP2930 || txContext.txType == EIP1559) {
|
||||
chain_id = u32_from_BE(tmpContent.txContent.chainID.value,
|
||||
tmpContent.txContent.chainID.length,
|
||||
true);
|
||||
} else {
|
||||
PRINTF("Txtype `%u` not supported while generating chainID\n", txContext.txType);
|
||||
}
|
||||
PRINTF("ChainID: %d\n", chain_id);
|
||||
return chain_id;
|
||||
}
|
||||
|
||||
void prepareNetworkDisplay() {
|
||||
char *name = get_network_name();
|
||||
if (name == NULL) {
|
||||
// No network name found so simply copy the chain ID as the network name.
|
||||
uint32_t chain_id = get_chain_id();
|
||||
uint8_t res = snprintf(strings.common.network_name,
|
||||
sizeof(strings.common.network_name),
|
||||
"%d",
|
||||
chain_id);
|
||||
if (res >= sizeof(strings.common.network_name)) {
|
||||
// If the return value is higher or equal to the size passed in as parameter, then
|
||||
// the output was truncated. Return the appropriate error code.
|
||||
THROW(0x6502);
|
||||
}
|
||||
} else {
|
||||
// Network name found, simply copy it.
|
||||
strlcpy(strings.common.network_name, name, sizeof(strings.common.network_name));
|
||||
}
|
||||
}
|
||||
|
||||
static void get_public_key(uint8_t *out, uint8_t outLength) {
|
||||
@@ -269,6 +323,7 @@ void finalizeParsing(bool direct) {
|
||||
|
||||
// Verify the chain
|
||||
if (chainConfig->chainId != ETHEREUM_MAINNET_CHAINID) {
|
||||
// TODO: Could we remove above check?
|
||||
uint32_t id = get_chain_id();
|
||||
|
||||
if (chainConfig->chainId != id) {
|
||||
@@ -336,7 +391,7 @@ void finalizeParsing(bool direct) {
|
||||
// Handle the right interface
|
||||
switch (pluginFinalize.uiType) {
|
||||
case ETH_UI_TYPE_GENERIC:
|
||||
dataPresent = false;
|
||||
tmpContent.txContent.dataPresent = false;
|
||||
// Add the number of screens + the number of additional screens to get the total
|
||||
// number of screens needed.
|
||||
dataContext.tokenContext.pluginUiMaxItems =
|
||||
@@ -344,7 +399,7 @@ void finalizeParsing(bool direct) {
|
||||
break;
|
||||
case ETH_UI_TYPE_AMOUNT_ADDRESS:
|
||||
genericUI = true;
|
||||
dataPresent = false;
|
||||
tmpContent.txContent.dataPresent = false;
|
||||
if ((pluginFinalize.amount == NULL) || (pluginFinalize.address == NULL)) {
|
||||
PRINTF("Incorrect amount/address set by plugin\n");
|
||||
reportFinalizeError(direct);
|
||||
@@ -373,12 +428,13 @@ void finalizeParsing(bool direct) {
|
||||
}
|
||||
}
|
||||
|
||||
if (dataPresent && !N_storage.dataAllowed) {
|
||||
if (tmpContent.txContent.dataPresent && !N_storage.dataAllowed) {
|
||||
reportFinalizeError(direct);
|
||||
if (!direct) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare destination address to display
|
||||
if (genericUI) {
|
||||
if (tmpContent.txContent.destinationLength != 0) {
|
||||
@@ -396,6 +452,7 @@ void finalizeParsing(bool direct) {
|
||||
strcpy(strings.common.fullAddress, "Contract");
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare amount to display
|
||||
if (genericUI) {
|
||||
amountToString(tmpContent.txContent.value.value,
|
||||
@@ -409,44 +466,18 @@ void finalizeParsing(bool direct) {
|
||||
displayBuffer,
|
||||
called_from_swap);
|
||||
}
|
||||
|
||||
// Prepare nonce to display
|
||||
if (genericUI) {
|
||||
uint256_t nonce;
|
||||
convertUint256BE(tmpContent.txContent.nonce.value,
|
||||
tmpContent.txContent.nonce.length,
|
||||
&nonce);
|
||||
tostring256(&nonce, 10, displayBuffer, sizeof(displayBuffer));
|
||||
strlcpy(strings.common.nonce, displayBuffer, sizeof(strings.common.nonce));
|
||||
}
|
||||
uint256_t nonce;
|
||||
convertUint256BE(tmpContent.txContent.nonce.value, tmpContent.txContent.nonce.length, &nonce);
|
||||
tostring256(&nonce, 10, displayBuffer, sizeof(displayBuffer));
|
||||
strlcpy(strings.common.nonce, displayBuffer, sizeof(strings.common.nonce));
|
||||
|
||||
// Compute maximum fee
|
||||
if (genericUI) {
|
||||
computeFees(displayBuffer, sizeof(displayBuffer));
|
||||
compareOrCopy(strings.common.maxFee,
|
||||
sizeof(strings.common.maxFee),
|
||||
displayBuffer,
|
||||
called_from_swap);
|
||||
}
|
||||
prepareFeeDisplay();
|
||||
|
||||
// Prepare chainID field
|
||||
if (genericUI) {
|
||||
char *name = get_network_name();
|
||||
if (name == NULL) {
|
||||
// No network name found so simply copy the chain ID as the network name.
|
||||
uint32_t chain_id = get_chain_id();
|
||||
uint8_t res = snprintf(strings.common.network_name,
|
||||
sizeof(strings.common.network_name),
|
||||
"%d",
|
||||
chain_id);
|
||||
if (res >= sizeof(strings.common.network_name)) {
|
||||
// If the return value is higher or equal to the size passed in as parameter, then
|
||||
// the output was truncated. Return the appropriate error code.
|
||||
THROW(0x6502);
|
||||
}
|
||||
} else {
|
||||
// Network name found, simply copy it.
|
||||
strlcpy(strings.common.network_name, name, sizeof(strings.common.network_name));
|
||||
}
|
||||
}
|
||||
prepareNetworkDisplay();
|
||||
|
||||
bool no_consent;
|
||||
|
||||
@@ -460,7 +491,7 @@ void finalizeParsing(bool direct) {
|
||||
io_seproxyhal_touch_tx_ok(NULL);
|
||||
} else {
|
||||
if (genericUI) {
|
||||
ux_approve_tx(dataPresent);
|
||||
ux_approve_tx(false);
|
||||
} else {
|
||||
plugin_ui_start();
|
||||
}
|
||||
|
||||
@@ -27,20 +27,28 @@ unsigned int io_seproxyhal_touch_tx_ok(__attribute__((unused)) const bagl_elemen
|
||||
sizeof(signature),
|
||||
&info);
|
||||
explicit_bzero(&privateKey, sizeof(privateKey));
|
||||
// Parity is present in the sequence tag in the legacy API
|
||||
if (tmpContent.txContent.vLength == 0) {
|
||||
// Legacy API
|
||||
G_io_apdu_buffer[0] = 27;
|
||||
if (txContext.txType == EIP1559 || txContext.txType == EIP2930) {
|
||||
if (info & CX_ECCINFO_PARITY_ODD) {
|
||||
G_io_apdu_buffer[0] = 1;
|
||||
} else {
|
||||
G_io_apdu_buffer[0] = 0;
|
||||
}
|
||||
} else {
|
||||
// New API
|
||||
// Note that this is wrong for a large v, but the client can always recover
|
||||
G_io_apdu_buffer[0] = (v * 2) + 35;
|
||||
}
|
||||
if (info & CX_ECCINFO_PARITY_ODD) {
|
||||
G_io_apdu_buffer[0]++;
|
||||
}
|
||||
if (info & CX_ECCINFO_xGTn) {
|
||||
G_io_apdu_buffer[0] += 2;
|
||||
// Parity is present in the sequence tag in the legacy API
|
||||
if (tmpContent.txContent.vLength == 0) {
|
||||
// Legacy API
|
||||
G_io_apdu_buffer[0] = 27;
|
||||
} else {
|
||||
// New API
|
||||
// Note that this is wrong for a large v, but the client can always recover
|
||||
G_io_apdu_buffer[0] = (v * 2) + 35;
|
||||
}
|
||||
if (info & CX_ECCINFO_PARITY_ODD) {
|
||||
G_io_apdu_buffer[0]++;
|
||||
}
|
||||
if (info & CX_ECCINFO_xGTn) {
|
||||
G_io_apdu_buffer[0] += 2;
|
||||
}
|
||||
}
|
||||
format_signature_out(signature);
|
||||
tx = 65;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "utils.h"
|
||||
#include "feature_signTx.h"
|
||||
#include "network.h"
|
||||
#include "eth_plugin_handler.h"
|
||||
|
||||
// clang-format off
|
||||
UX_STEP_NOCB(
|
||||
@@ -110,6 +111,40 @@ UX_STEP_NOCB(
|
||||
.title = "Address",
|
||||
.text = strings.common.fullAddress,
|
||||
});
|
||||
|
||||
UX_STEP_NOCB_INIT(
|
||||
ux_plugin_approval_id_step,
|
||||
bnnn_paging,
|
||||
plugin_ui_get_id(),
|
||||
{
|
||||
.title = strings.common.fullAddress,
|
||||
.text = strings.common.fullAmount
|
||||
});
|
||||
|
||||
UX_STEP_INIT(
|
||||
ux_plugin_approval_before_step,
|
||||
NULL,
|
||||
NULL,
|
||||
{
|
||||
display_next_plugin_item(true);
|
||||
});
|
||||
|
||||
UX_FLOW_DEF_NOCB(
|
||||
ux_plugin_approval_display_step,
|
||||
bnnn_paging,
|
||||
{
|
||||
.title = strings.common.fullAddress,
|
||||
.text = strings.common.fullAmount
|
||||
});
|
||||
|
||||
UX_STEP_INIT(
|
||||
ux_plugin_approval_after_step,
|
||||
NULL,
|
||||
NULL,
|
||||
{
|
||||
display_next_plugin_item(false);
|
||||
});
|
||||
|
||||
UX_STEP_NOCB(
|
||||
ux_approval_fees_step,
|
||||
bnnn_paging,
|
||||
@@ -124,6 +159,7 @@ UX_STEP_NOCB(
|
||||
.title = "Network",
|
||||
.text = strings.common.network_name,
|
||||
});
|
||||
|
||||
UX_STEP_CB(
|
||||
ux_approval_accept_step,
|
||||
pbb,
|
||||
@@ -159,28 +195,42 @@ UX_STEP_NOCB(ux_approval_data_warning_step,
|
||||
});
|
||||
// clang-format on
|
||||
|
||||
const ux_flow_step_t *ux_approval_tx_flow_[10];
|
||||
const ux_flow_step_t *ux_approval_tx_flow[15];
|
||||
|
||||
void ux_approve_tx(bool dataPresent) {
|
||||
void ux_approve_tx(bool fromPlugin) {
|
||||
int step = 0;
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_review_step;
|
||||
if (dataPresent && !N_storage.contractDetails) {
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_data_warning_step;
|
||||
ux_approval_tx_flow[step++] = &ux_approval_review_step;
|
||||
|
||||
if (!fromPlugin && tmpContent.txContent.dataPresent && !N_storage.contractDetails) {
|
||||
ux_approval_tx_flow[step++] = &ux_approval_data_warning_step;
|
||||
}
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_amount_step;
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_address_step;
|
||||
|
||||
if (fromPlugin) {
|
||||
// Add the special dynamic display logic
|
||||
ux_approval_tx_flow[step++] = &ux_plugin_approval_id_step;
|
||||
ux_approval_tx_flow[step++] = &ux_plugin_approval_before_step;
|
||||
ux_approval_tx_flow[step++] = &ux_plugin_approval_display_step;
|
||||
ux_approval_tx_flow[step++] = &ux_plugin_approval_after_step;
|
||||
} else {
|
||||
// We're in a regular transaction, just show the amount and the address
|
||||
ux_approval_tx_flow[step++] = &ux_approval_amount_step;
|
||||
ux_approval_tx_flow[step++] = &ux_approval_address_step;
|
||||
}
|
||||
|
||||
if (N_storage.displayNonce) {
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_nonce_step;
|
||||
ux_approval_tx_flow[step++] = &ux_approval_nonce_step;
|
||||
}
|
||||
|
||||
uint32_t chain_id = get_chain_id();
|
||||
if (chainConfig->chainId == ETHEREUM_MAINNET_CHAINID && chain_id != chainConfig->chainId) {
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_network_step;
|
||||
// TODO: do we need the `&&` above?
|
||||
ux_approval_tx_flow[step++] = &ux_approval_network_step;
|
||||
}
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_fees_step;
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_accept_step;
|
||||
ux_approval_tx_flow_[step++] = &ux_approval_reject_step;
|
||||
ux_approval_tx_flow_[step++] = FLOW_END_STEP;
|
||||
|
||||
ux_flow_init(0, ux_approval_tx_flow_, NULL);
|
||||
ux_approval_tx_flow[step++] = &ux_approval_fees_step;
|
||||
ux_approval_tx_flow[step++] = &ux_approval_accept_step;
|
||||
ux_approval_tx_flow[step++] = &ux_approval_reject_step;
|
||||
ux_approval_tx_flow[step++] = FLOW_END_STEP;
|
||||
|
||||
ux_flow_init(0, ux_approval_tx_flow, NULL);
|
||||
}
|
||||
82
src_features/signTx/ui_plugin.c
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "feature_signTx.h"
|
||||
#include "ux.h"
|
||||
#include "eth_plugin_handler.h"
|
||||
#include "ui_callbacks.h"
|
||||
#include "ui_plugin.h"
|
||||
|
||||
#ifdef TARGET_NANOS
|
||||
// This function is not exported by the SDK
|
||||
void ux_layout_paging_redisplay_by_addr(unsigned int stack_slot);
|
||||
#endif
|
||||
|
||||
void plugin_ui_get_id() {
|
||||
ethQueryContractID_t pluginQueryContractID;
|
||||
eth_plugin_prepare_query_contract_ID(&pluginQueryContractID,
|
||||
strings.common.fullAddress,
|
||||
sizeof(strings.common.fullAddress),
|
||||
strings.common.fullAmount,
|
||||
sizeof(strings.common.fullAmount));
|
||||
// Query the original contract for ID if it's not an internal alias
|
||||
if (!eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_ID, (void *) &pluginQueryContractID)) {
|
||||
PRINTF("Plugin query contract ID call failed\n");
|
||||
io_seproxyhal_touch_tx_cancel(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void plugin_ui_get_item() {
|
||||
ethQueryContractUI_t pluginQueryContractUI;
|
||||
eth_plugin_prepare_query_contract_UI(&pluginQueryContractUI,
|
||||
dataContext.tokenContext.pluginUiCurrentItem,
|
||||
strings.common.fullAddress,
|
||||
sizeof(strings.common.fullAddress),
|
||||
strings.common.fullAmount,
|
||||
sizeof(strings.common.fullAmount));
|
||||
if (!eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_UI, (void *) &pluginQueryContractUI)) {
|
||||
PRINTF("Plugin query contract UI call failed\n");
|
||||
io_seproxyhal_touch_tx_cancel(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void display_next_plugin_item(bool entering) {
|
||||
if (entering) {
|
||||
if (dataContext.tokenContext.pluginUiState == PLUGIN_UI_OUTSIDE) {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_INSIDE;
|
||||
dataContext.tokenContext.pluginUiCurrentItem = 0;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_next();
|
||||
} else {
|
||||
if (dataContext.tokenContext.pluginUiCurrentItem > 0) {
|
||||
dataContext.tokenContext.pluginUiCurrentItem--;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_next();
|
||||
} else {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_OUTSIDE;
|
||||
dataContext.tokenContext.pluginUiCurrentItem = 0;
|
||||
ux_flow_prev();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (dataContext.tokenContext.pluginUiState == PLUGIN_UI_OUTSIDE) {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_INSIDE;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_prev();
|
||||
} else {
|
||||
if (dataContext.tokenContext.pluginUiCurrentItem <
|
||||
dataContext.tokenContext.pluginUiMaxItems - 1) {
|
||||
dataContext.tokenContext.pluginUiCurrentItem++;
|
||||
plugin_ui_get_item();
|
||||
ux_flow_prev();
|
||||
// Reset multi page layout to the first page
|
||||
G_ux.layout_paging.current = 0;
|
||||
#ifdef TARGET_NANOS
|
||||
ux_layout_paging_redisplay_by_addr(G_ux.stack_count - 1);
|
||||
#else
|
||||
ux_layout_bnnn_paging_redisplay(0);
|
||||
#endif
|
||||
} else {
|
||||
dataContext.tokenContext.pluginUiState = PLUGIN_UI_OUTSIDE;
|
||||
ux_flow_next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src_features/signTx/ui_plugin.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
void plugin_ui_get_id();
|
||||
void plugin_ui_get_item();
|
||||
void display_next_plugin_item(bool entering);
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 541 B |
|
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 633 B |
BIN
tests/snapshots/eip1559/nanos/accept.png
Normal file
|
After Width: | Height: | Size: 582 B |
BIN
tests/snapshots/eip1559/nanos/address_1.png
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
tests/snapshots/eip1559/nanos/address_2.png
Normal file
|
After Width: | Height: | Size: 727 B |
BIN
tests/snapshots/eip1559/nanos/address_3.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
tests/snapshots/eip1559/nanos/amount.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
tests/snapshots/eip1559/nanos/fees.png
Normal file
|
After Width: | Height: | Size: 536 B |
BIN
tests/snapshots/eip1559/nanos/review.png
Normal file
|
After Width: | Height: | Size: 541 B |
BIN
tests/snapshots/eip1559/nanox/accept.png
Normal file
|
After Width: | Height: | Size: 667 B |
BIN
tests/snapshots/eip1559/nanox/address.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/snapshots/eip1559/nanox/amount.png
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
tests/snapshots/eip1559/nanox/fees.png
Normal file
|
After Width: | Height: | Size: 659 B |
BIN
tests/snapshots/eip1559/nanox/review.png
Normal file
|
After Width: | Height: | Size: 633 B |
154
tests/src/eip1559.test.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import "core-js/stable";
|
||||
import "regenerator-runtime/runtime";
|
||||
import Eth from "@ledgerhq/hw-app-eth";
|
||||
import { byContractAddress } from "@ledgerhq/hw-app-eth/erc20";
|
||||
import Zemu from "@zondax/zemu";
|
||||
import { TransportStatusError } from "@ledgerhq/errors";
|
||||
import { expect } from "../jest";
|
||||
|
||||
const {NANOS_ELF_PATH, NANOX_ELF_PATH, sim_options_nanos, sim_options_nanox, TIMEOUT} = require("generic.js");
|
||||
|
||||
const ORIGINAL_SNAPSHOT_PATH_PREFIX = "snapshots/eip1559/";
|
||||
const SNAPSHOT_PATH_PREFIX = "snapshots/eip1559/";
|
||||
|
||||
const ORIGINAL_SNAPSHOT_PATH_NANOS = ORIGINAL_SNAPSHOT_PATH_PREFIX + "nanos/";
|
||||
const ORIGINAL_SNAPSHOT_PATH_NANOX = ORIGINAL_SNAPSHOT_PATH_PREFIX + "nanox/";
|
||||
|
||||
const SNAPSHOT_PATH_NANOS = SNAPSHOT_PATH_PREFIX + "nanos/";
|
||||
const SNAPSHOT_PATH_NANOX = SNAPSHOT_PATH_PREFIX + "nanox/";
|
||||
|
||||
test("Transfer nanos eip1559", async () => {
|
||||
jest.setTimeout(TIMEOUT);
|
||||
const sim = new Zemu(NANOS_ELF_PATH);
|
||||
|
||||
try {
|
||||
await sim.start(sim_options_nanos);
|
||||
|
||||
let transport = await sim.getTransport();
|
||||
|
||||
// From this test: https://github.com/ethereum/tests/blob/5d534e37b80e9310e8c7751f805ca481a451123e/GeneralStateTests/stEIP1559/outOfFunds.json#L35
|
||||
let buffer = Buffer.from("058000002c8000003c80000000000000000000000002f87001018502540be4008502540be40086246139ca800094cccccccccccccccccccccccccccccccccccccccc8000c001a0e07fb8a64ea3786c9a6649e54429e2786af3ea31c6d06165346678cf8ce44f9ba00e4a0526db1e905b7164a858fd5ebd2f1759e22e6955499448bd276a6aa62830", "hex");
|
||||
|
||||
// Send transaction
|
||||
let tx = transport.send(0xe0, 0x04, 0x00, 0x00, buffer);
|
||||
let filename;
|
||||
|
||||
await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot());
|
||||
// Review tx
|
||||
filename = "review.png";
|
||||
await sim.snapshot(SNAPSHOT_PATH_NANOS + filename);
|
||||
const review = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOS + filename);
|
||||
const expected_review = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOS + filename);
|
||||
expect(review).toEqual(expected_review);
|
||||
|
||||
// Amount
|
||||
filename = "amount.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOS + filename);
|
||||
const amount = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOS + filename);
|
||||
const expected_amount = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOS + filename);
|
||||
expect(amount).toEqual(expected_amount);
|
||||
|
||||
// Address 1/3
|
||||
filename = "address_1.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOS + filename);
|
||||
const address_1 = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOS + filename);
|
||||
const expected_address_1 = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOS + filename);
|
||||
expect(address_1).toEqual(expected_address_1);
|
||||
|
||||
// Address 2/3
|
||||
filename = "address_2.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOS + filename);
|
||||
const address_2 = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOS + filename);
|
||||
const expected_address_2 = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOS + filename);
|
||||
expect(address_2).toEqual(expected_address_2);
|
||||
|
||||
// Address 3/3
|
||||
filename = "address_3.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOS + filename);
|
||||
const address_3 = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOS + filename);
|
||||
const expected_address_3 = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOS + filename);
|
||||
expect(address_3).toEqual(expected_address_3);
|
||||
|
||||
// Max Fees
|
||||
filename = "fees.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOS + filename);
|
||||
const fees = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOS + filename);
|
||||
const expected_fees = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOS + filename);
|
||||
expect(fees).toEqual(expected_fees);
|
||||
|
||||
// Accept
|
||||
filename = "accept.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOS + filename);
|
||||
const accept = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOS + filename);
|
||||
const expected_accept = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOS + filename);
|
||||
expect(accept).toEqual(expected_accept);
|
||||
|
||||
await sim.clickBoth();
|
||||
|
||||
await expect(tx).resolves.toEqual(
|
||||
Buffer.from([1, 61, 109, 250, 188, 108, 82, 55, 75, 250, 52, 203, 44, 67, 56, 86, 160, 188, 217, 72, 72, 112, 221, 27, 80, 36, 159, 113, 100, 165, 252, 224, 82, 5, 72, 167, 116, 221, 11, 99, 147, 13, 131, 203, 46, 26, 131, 111, 227, 239, 36, 68, 78, 139, 117, 139, 0, 88, 93, 154, 7, 108, 14, 152, 168, 144, 0]));
|
||||
} finally {
|
||||
await sim.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("Transfer nanox", async () => {
|
||||
jest.setTimeout(TIMEOUT);
|
||||
const sim = new Zemu(NANOX_ELF_PATH);
|
||||
|
||||
try {
|
||||
await sim.start(sim_options_nanox);
|
||||
|
||||
let transport = await sim.getTransport();
|
||||
|
||||
// From this test: https://github.com/ethereum/tests/blob/5d534e37b80e9310e8c7751f805ca481a451123e/GeneralStateTests/stEIP1559/outOfFunds.json#L35
|
||||
let buffer = Buffer.from("058000002c8000003c80000000000000000000000002f87001018502540be4008502540be40086246139ca800094cccccccccccccccccccccccccccccccccccccccc8000c001a0e07fb8a64ea3786c9a6649e54429e2786af3ea31c6d06165346678cf8ce44f9ba00e4a0526db1e905b7164a858fd5ebd2f1759e22e6955499448bd276a6aa62830", "hex");
|
||||
|
||||
// Send transaction
|
||||
let tx = transport.send(0xe0, 0x04, 0x00, 0x00, buffer);
|
||||
let filename;
|
||||
|
||||
await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot());
|
||||
// Review tx
|
||||
filename = "review.png";
|
||||
await sim.snapshot(SNAPSHOT_PATH_NANOX + filename);
|
||||
const review = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOX + filename);
|
||||
const expected_review = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOX + filename);
|
||||
expect(review).toEqual(expected_review);
|
||||
|
||||
// Amount
|
||||
filename = "amount.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOX + filename);
|
||||
const amount = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOX + filename);
|
||||
const expected_amount = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOX + filename);
|
||||
expect(amount).toEqual(expected_amount);
|
||||
|
||||
// Address
|
||||
filename = "address.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOX + filename);
|
||||
const address = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOX + filename);
|
||||
const expected_address = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOX + filename);
|
||||
expect(address).toEqual(expected_address);
|
||||
|
||||
// Max Fees
|
||||
filename = "fees.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOX + filename);
|
||||
const fees = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOX + filename);
|
||||
const expected_fees = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOX + filename);
|
||||
expect(fees).toEqual(expected_fees);
|
||||
|
||||
// Accept
|
||||
filename = "accept.png";
|
||||
await sim.clickRight(SNAPSHOT_PATH_NANOX + filename);
|
||||
const accept = Zemu.LoadPng2RGB(SNAPSHOT_PATH_NANOX + filename);
|
||||
const expected_accept = Zemu.LoadPng2RGB(ORIGINAL_SNAPSHOT_PATH_NANOX + filename);
|
||||
expect(accept).toEqual(expected_accept);
|
||||
|
||||
await sim.clickBoth();
|
||||
|
||||
await expect(tx).resolves.toEqual(
|
||||
Buffer.from([1, 61, 109, 250, 188, 108, 82, 55, 75, 250, 52, 203, 44, 67, 56, 86, 160, 188, 217, 72, 72, 112, 221, 27, 80, 36, 159, 113, 100, 165, 252, 224, 82, 5, 72, 167, 116, 221, 11, 99, 147, 13, 131, 203, 46, 26, 131, 111, 227, 239, 36, 68, 78, 139, 117, 139, 0, 88, 93, 154, 7, 108, 14, 152, 168, 144, 0]));
|
||||
} finally {
|
||||
await sim.close();
|
||||
}
|
||||
});
|
||||