Merge pull request #157 from LedgerHQ/support_eip1559

Support EIP1559
This commit is contained in:
pscott
2021-08-05 10:43:50 +02:00
committed by GitHub
36 changed files with 598 additions and 310 deletions

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ tests/node_modules
tests/lib
tests/yarn-error.log
.vscode

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)))

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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);
}

View 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();
}
}
}
}

View File

@@ -0,0 +1,5 @@
#pragma once
void plugin_ui_get_id();
void plugin_ui_get_item();
void display_next_plugin_item(bool entering);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

154
tests/src/eip1559.test.js Normal file
View 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();
}
});