diff --git a/.gitignore b/.gitignore index e388d15..1138673 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ tests/node_modules tests/lib tests/yarn-error.log + +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3da2a..0ba57b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index f79dfd8..6b88b48 100755 --- a/Makefile +++ b/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) diff --git a/doc/ethapp_plugins.asc b/doc/ethapp_plugins.asc index d142852..e1233fa 100644 --- a/doc/ethapp_plugins.asc +++ b/doc/ethapp_plugins.asc @@ -3,6 +3,7 @@ Ethereum application Plugins : Technical Specifications Ledger Firmware Team Specification version 1.0 - 24th of September 2020 + ## 1.0 - Initial release diff --git a/ethereum-plugin-sdk b/ethereum-plugin-sdk index b5325d0..51c4cad 160000 --- a/ethereum-plugin-sdk +++ b/ethereum-plugin-sdk @@ -1 +1 @@ -Subproject commit b5325d0f08af7f233e22d766183fb70cdb8ead59 +Subproject commit 51c4cad6297a99a220eb09e6d30d229ac02d469c diff --git a/examples/signTx.py b/examples/signTx.py index f1d0337..527c0ce 100755 --- a/examples/signTx.py +++ b/examples/signTx.py @@ -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))) diff --git a/src/eth_plugin_interface.h b/src/eth_plugin_interface.h index 9c76b52..2698e21 100644 --- a/src/eth_plugin_interface.h +++ b/src/eth_plugin_interface.h @@ -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 { diff --git a/src/eth_plugin_ui.c b/src/eth_plugin_ui.c index db5c4fc..681ef66 100644 --- a/src/eth_plugin_ui.c +++ b/src/eth_plugin_ui.c @@ -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); } diff --git a/src/handle_swap_sign_transaction.c b/src/handle_swap_sign_transaction.c index e4c7111..d610e9e 100644 --- a/src/handle_swap_sign_transaction.c +++ b/src/handle_swap_sign_transaction.c @@ -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)); } diff --git a/src/main.c b/src/main.c index 6099e6b..ca26b41 100644 --- a/src/main.c +++ b/src/main.c @@ -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 diff --git a/src/shared_context.h b/src/shared_context.h index fe0711c..e3e9c6f 100644 --- a/src/shared_context.h +++ b/src/shared_context.h @@ -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 diff --git a/src_common/ethUstream.c b/src_common/ethUstream.c index 83e9055..fc091cc 100644 --- a/src_common/ethUstream.c +++ b/src_common/ethUstream.c @@ -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; diff --git a/src_common/ethUstream.h b/src_common/ethUstream.h index 1a51747..01b9f37 100644 --- a/src_common/ethUstream.h +++ b/src_common/ethUstream.h @@ -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 { diff --git a/src_common/network.c b/src_common/network.c index 096f56d..5f33b8a 100644 --- a/src_common/network.c +++ b/src_common/network.c @@ -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); diff --git a/src_features/signTx/cmd_signTx.c b/src_features/signTx/cmd_signTx.c index d4293cc..4e4fdbe 100644 --- a/src_features/signTx/cmd_signTx.c +++ b/src_features/signTx/cmd_signTx.c @@ -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); } diff --git a/src_features/signTx/feature_signTx.h b/src_features/signTx/feature_signTx.h index 8b58591..24400da 100644 --- a/src_features/signTx/feature_signTx.h +++ b/src_features/signTx/feature_signTx.h @@ -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); \ No newline at end of file +void prepareFeeDisplay(); +void prepareNetworkDisplay(); +void ux_approve_tx(bool fromPlugin); diff --git a/src_features/signTx/logic_signTx.c b/src_features/signTx/logic_signTx.c index 8020859..a7cb59e 100644 --- a/src_features/signTx/logic_signTx.c +++ b/src_features/signTx/logic_signTx.c @@ -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(); } diff --git a/src_features/signTx/ui_common_signTx.c b/src_features/signTx/ui_common_signTx.c index 5f6ca3b..eb2ee11 100644 --- a/src_features/signTx/ui_common_signTx.c +++ b/src_features/signTx/ui_common_signTx.c @@ -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; diff --git a/src_features/signTx/ui_flow_signTx.c b/src_features/signTx/ui_flow_signTx.c index 6a8aaf4..6b91f80 100644 --- a/src_features/signTx/ui_flow_signTx.c +++ b/src_features/signTx/ui_flow_signTx.c @@ -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); } \ No newline at end of file diff --git a/src_features/signTx/ui_plugin.c b/src_features/signTx/ui_plugin.c new file mode 100644 index 0000000..9ebaa24 --- /dev/null +++ b/src_features/signTx/ui_plugin.c @@ -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(); + } + } + } +} \ No newline at end of file diff --git a/src_features/signTx/ui_plugin.h b/src_features/signTx/ui_plugin.h new file mode 100644 index 0000000..28179b2 --- /dev/null +++ b/src_features/signTx/ui_plugin.h @@ -0,0 +1,5 @@ +#pragma once + +void plugin_ui_get_id(); +void plugin_ui_get_item(); +void display_next_plugin_item(bool entering); \ No newline at end of file diff --git a/tests/snapshots/approve/nanos/review.png b/tests/snapshots/approve/nanos/review.png index a99bc5a..2994983 100644 Binary files a/tests/snapshots/approve/nanos/review.png and b/tests/snapshots/approve/nanos/review.png differ diff --git a/tests/snapshots/approve/nanox/review.png b/tests/snapshots/approve/nanox/review.png index bc99f61..8794afe 100644 Binary files a/tests/snapshots/approve/nanox/review.png and b/tests/snapshots/approve/nanox/review.png differ diff --git a/tests/snapshots/eip1559/nanos/accept.png b/tests/snapshots/eip1559/nanos/accept.png new file mode 100644 index 0000000..3158ea6 Binary files /dev/null and b/tests/snapshots/eip1559/nanos/accept.png differ diff --git a/tests/snapshots/eip1559/nanos/address_1.png b/tests/snapshots/eip1559/nanos/address_1.png new file mode 100644 index 0000000..54bb773 Binary files /dev/null and b/tests/snapshots/eip1559/nanos/address_1.png differ diff --git a/tests/snapshots/eip1559/nanos/address_2.png b/tests/snapshots/eip1559/nanos/address_2.png new file mode 100644 index 0000000..21eac94 Binary files /dev/null and b/tests/snapshots/eip1559/nanos/address_2.png differ diff --git a/tests/snapshots/eip1559/nanos/address_3.png b/tests/snapshots/eip1559/nanos/address_3.png new file mode 100644 index 0000000..4baa24f Binary files /dev/null and b/tests/snapshots/eip1559/nanos/address_3.png differ diff --git a/tests/snapshots/eip1559/nanos/amount.png b/tests/snapshots/eip1559/nanos/amount.png new file mode 100644 index 0000000..0bf93d8 Binary files /dev/null and b/tests/snapshots/eip1559/nanos/amount.png differ diff --git a/tests/snapshots/eip1559/nanos/fees.png b/tests/snapshots/eip1559/nanos/fees.png new file mode 100644 index 0000000..5512e8a Binary files /dev/null and b/tests/snapshots/eip1559/nanos/fees.png differ diff --git a/tests/snapshots/eip1559/nanos/review.png b/tests/snapshots/eip1559/nanos/review.png new file mode 100644 index 0000000..2994983 Binary files /dev/null and b/tests/snapshots/eip1559/nanos/review.png differ diff --git a/tests/snapshots/eip1559/nanox/accept.png b/tests/snapshots/eip1559/nanox/accept.png new file mode 100644 index 0000000..7f2b8c8 Binary files /dev/null and b/tests/snapshots/eip1559/nanox/accept.png differ diff --git a/tests/snapshots/eip1559/nanox/address.png b/tests/snapshots/eip1559/nanox/address.png new file mode 100644 index 0000000..fe5d609 Binary files /dev/null and b/tests/snapshots/eip1559/nanox/address.png differ diff --git a/tests/snapshots/eip1559/nanox/amount.png b/tests/snapshots/eip1559/nanox/amount.png new file mode 100644 index 0000000..3da7c25 Binary files /dev/null and b/tests/snapshots/eip1559/nanox/amount.png differ diff --git a/tests/snapshots/eip1559/nanox/fees.png b/tests/snapshots/eip1559/nanox/fees.png new file mode 100644 index 0000000..bc32080 Binary files /dev/null and b/tests/snapshots/eip1559/nanox/fees.png differ diff --git a/tests/snapshots/eip1559/nanox/review.png b/tests/snapshots/eip1559/nanox/review.png new file mode 100644 index 0000000..8794afe Binary files /dev/null and b/tests/snapshots/eip1559/nanox/review.png differ diff --git a/tests/src/eip1559.test.js b/tests/src/eip1559.test.js new file mode 100644 index 0000000..d102d3c --- /dev/null +++ b/tests/src/eip1559.test.js @@ -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(); + } +}); \ No newline at end of file