changed 3 years ago
Linked with GitHub

Table of contents

Overview

Let's start by having a high-level overview of what's a plugin, how it interacts with the ethereum app, and what steps are required for you to write your own plugin!

Even though this guide is relatively beginner friendly, we expect you to have some prior experience with C and Solidity development.

Why Plugins

If you've already interacted with any smart-contract using a Ledger Device, then you've already seen this screen:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

This screen is a UX disaster. The end user has no guarantee that he's interacting with the correct smart-contract, or that he's signing the correct parameters: the only thing he can do is blindly sign the transaction.

The display is specific to every smart-contract: when performing a swap on a DEX, you would probably like to see screens like "Swapping X ETH for Y DAI". When depositing your DAI on Aave, you would probably like to see how much DAI you're depositing. As you can see, the information we'd like to display on the screen would be very specific to the smart-contract the user is interacting with.

This information couldn't possibly be added to the Ethereum App without its size quickly becoming out of control. Rather, the ideal solution would be to develop small, lightweight "add-ons" that would work hand-in-hand with the Ethereum App. Those "add-ons" would be in charge of parsing the smart-contract data, and deciding what to display for the best user experience.

Well that is exactly what a plugin is! A plugin is a small add-on that the user installs on his device that lets him see exactly what he's about to sign! Since plugins are lightweight, users can install multiple addons without having to worry about the space it's going to take on their device!

Developing a plugin

Plugins work hand-in-hand with the Ethereum App. Worry not, implementing a plugin is a very easy task! The Ethereum App will take care of handling the logic of parsing, signing, screen display etc The only thing your plugin needs to do is:

  1. Extract the important information from the data.
  2. Send back the string you wish to display on screen to the Ethereum App.

Here's a rough overview of what the flow looks like for plugins:

Eth App -> Plugin: Are you installed on this device?
Note right of Plugin: Fails if no plugin found
Plugin --> Eth App: Yes, bring it on!
Note left of Eth App: Parses transaction
Eth App -> Plugin: Here's the contract data
Note right of Plugin: Extracts important fields
Plugin --> Eth App: Ok
Note left of Eth App: Computes stuff
Note left of Eth App: Prepares display
Eth App -> Plugin: Tell me what I should display
Note right of Plugin: Decides what to display
Plugin --> Eth App: 'Swap ETH 1 for DAI 10000'
Note left of Eth App: Displays the message

As you can see, it's basically a series of back and forth messages between the Ethereum App and the plugin.

Environment Setup

Let's get you setup with the correct development environment.

Builder

First step is getting the plugin to compile. Rather than installing all the dependencies on your computer, we've created some docker images that will allow you to start coding in a couple of minutes!

You will need to install two things (that you've probably installed before but just in case):

  1. Docker: instructions here
  2. Docker-compose: instructions here

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Make sure you have properly installed the tools above!

Let's create a new directory, which will contain everything plugin related.

mkdir plugin_dev
cd plugin_dev

To make sure out setup is working fine, let's try compiling the ethereum application. Clone the ethereum-app in the plugin_dev folder: https://github.com/LedgerHQ/app-ethereum.

Stay in the plugin_dev folder, and clone the plugin-dev repo which contains some tools for your plugin journey: https://github.com/LedgerHQ/plugin-tools.

The plugin-tools repo contains a docker-compose file that will do all the magic for you. To start it, simply run start.sh.

cd plugin-tools
./start.sh

If you're getting this error:

ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
This means you either:

  • Failed to installdocker.
  • Need to add sudo before running the script sudo ./start.sh!

You should now be connected to the container. The plugin_dev directory you see in the container is the one we created in the steps above (they're shared via the volume in the docker-compose).

root@997ccff31349:/plugin_dev$ ls
app-ethereum  plugin-tools

To compile the ethereum app, simply run cd to it and run make!

cd app-ethereum
make

During the first compilation, a couple of things might come as a surprise:

  1. Stuff like BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH. This is expected, don't panic.
  2. You might get asked to if you wish to conitnue connecting: type yes.
  3. You might see a couple (or a lot) of gcc / clang warnings about the C code. This is (unfortunately) expected.

If everything goes well, you once compilation is done, you should have

[LINK] bin/app.elf

If that is the case, congratulations! You've succesfully compiled the ethereum app! Let's jump to the second part of the setup: testing.

Code overview

We are going to start coding our first plugin! Remember, the plugin gets called successively by the Ethereum Application, and answers back with the appropriate message status. An overview of the full flow is available at the bottom of the page, in the the flow section.

Boilerplate plugin

We've created a boilerplate plugin that should help you write your first plugin! We'll be following along this boilerplate code for this guide. You can start by forking the repo and cloning your fork in the plugin_dev folder:

cd ..
git clone --recurse-submodules YOUR_FORKED_URL

No need to build it right now. We'll build it later on when we will add some tests!

Let's go through the different folders that we can find here:

  • ethereum-plugin-sdk: This submodule (read sub-repo) holds the shared information between the ethereum app and your plugin (stuff like structure definitions, utility functions etc).
  • icons: icons of the plugin (as displayed on the device). We will get to it later but you can read about it here.
  • src: the actual source code (in C).
  • tests: the testing folder (uses a js framework).

Finally, there's the Makefile directly in the root of the repo. The first thing you need to do is open this Makefile and change the APPNAME variable to your own plugin name! (e.g Paraswap, 1inch, Lido).

Main

Let's start by tackling the first EDIT THIS comments in main.c. We'll start by implementing a single selector, and add others later on.

The first selector we are going to support is Uniswap's V2 'SwapExactEthForToken'. We first need its selector, or methodID: you can find it via etherscan (you could also compute it yourself using the abi).

By having a look at the recent transactions on Uniswap, we can find a transaction that has the method "SwapExactEthForToken". Here's an example. If you scroll down and click "Click to see more", you will see the data:

Bingo! The corresponding method ID is: 0x7ff36ab5.

So now let's edit our code:

static const uint8_t SWAP_EXACT_ETH_FOR_TOKENS_SELECTOR[SELECTOR_SIZE] = {0x7f,
									     0xf3,
									     0x6a,
									     0xb5};

Just a couple of lines below, we can see that those selectors are all grouped in an array. Let's add our newly created selector to this array, as well as adapt the naming of the array (I'm using BOILERPLATE_SELECTORS but you should adapt to your plugin):

// Array of all the different boilerplate selectors. 
// Make sure this follows the same order as the
// enum defined in `boilerplate_plugin.h`
const uint8_t *const BOILERPLATE_SELECTORS[NUM_SELECTORS] = {
    SWAP_EXACT_ETH_FOR_TOKENS_SELECTOR,
};

Interesting, the comments mention boilerplate_plugin.h. Let's have a look.

Start out by editing the NUM_BOILERPLATE_SELECTORS. Indeed, for the moment we only have a single selector

#define NUM_SELECTORS 1

We should also adapt the PLUGIN_NAME variable. I'm writing a boilerplate plugin so I'll name it Boilerplate but your plugin should definitely find a better name.

#define PLUGIN_NAME "Boilerplate"

Ok now let's look for the selector that we saw in the comments Right! There's an enum! Let's edit it and add our selector:

typedef enum {
    SWAP_EXACT_ETH_FOR_TOKENS,
} selector_t;

We should also edit the name of the BOILERPLATE_SELECTORS name to match the one declared in main.c.

Now let's go back to main.c. We see the app calls dispatch_plugin_calls(). Let's go through each function, one at a time. They should all have a corresponding handle_*.c file.
Recall you can go to the flow chart to make sure you have a proper mental model of what's going on!

Init contract

The first one is handle_init_contract.c. In this file, we will need to set selectorIndex to be the first parameter we expect to parse. Let me dive a litle bit deeper into parameters.

See, a method in a smart-contract takes some parameters (or arguments). For example, swapExactEthforToken has this signature:

swapExactETHForTokens(uint256 amountOutMin,
                        address[] path,
                        address to,
                        uint256 deadline)

So the parameters for this function are:

  1. uint256 amountOutMin: the minimum amount of tokens the user is going to get back from his swap.
  2. address[] path: an array of paths. If we look at the source code of the contract (you can see it on Etherscan), we can see that the only paths used are path[0] and path[1]. path[0] is corresponding to the OUTGOING token, and path[1] corresponds to the INCOMING token.
  3. address to: the address to which the tokens will be sent back.
  4. uint256 deadline: the deadline after which the swap should not occur.

Those parameters will be passed to your plugin, in the exact same order than the function signature. Parameters always come in 32-bytes chunks.

The end goal of this plugin is to be able to display on screen the number of ETH going out of the wallet, which currency he will get back in return, and the address to which the tokens will be sent.

Hence, we will need to parse amountOutMin, path[1], and to. We won't really care about deadline, since we are not going to display this information. We also don't need to parse path[0] since we know we are sending ETH.

The parsing of these fields will be done in the next call, in handle_provide_parameter.c. Right now, what we need to do, is to set the first parameter we expect to parse. In our case, it is amountOutMin.

switch (context->selectorIndex) {
    case SWAP_EXACT_ETH_FOR_TOKENS:
        context->next_param = AMOUNT_OUT_MIN;
        break;
    default:
        PRINTF("Missing selectorIndex: %d\n", context->selectorIndex);
        msg->result = ETH_PLUGIN_RESULT_ERROR;
        return;
}

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
If there's an error in your plugin, simply set msg->result to ETH_PLUGIN_RESULT_ERROR.

Now let's not forget to add this parameter name in the parameter enum located in boilerplate_plugin.h.

typedef enum {
    MIN_AMOUNT_RECEIVED, // min amount of tokens the user is going to get
    TOKEN_RECEIVED, // Address of the token the user is swapping to
    BENEFICIARY, // Address to which the tokens will be sent
} parameter;

Ok let's jump into handle_provide_parameter.c then!

Provide parameter

The function handle_provider_parameter dispatches the msg depending on what selectorIndex is. Let's add our selector:

switch (context->selectorIndex) {
    case SWAP_EXACT_ETH_FOR_TOKENS:
        handle_swap_exact_eth_for_tokens(msg, context);
        break;
    default:
        PRINTF("Selector Index %d not supported\n", context->selectorIndex);
        msg->result = ETH_PLUGIN_RESULT_ERROR;
        break;
}

Now let's actually write handle_swap_exact_eth_for_tokens()!
The parsing logic for those handlers is simple: look at the next_param we are expecting to parse, and copy the data we need into our context.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
You will be seing the variable context being used frequently throughout the code. It's a structure defined in the boilerplate.h header file.
Since the code goes back in forth between the Ethereum App and the Plugin, the plugin needs somewhere to store the data (stuff like the contract address, the number of tokens swapped, etc etc). This is all stored in the context. Make sure you edit the context definition in boilerplate.h to fit to your plugin!

We set next_param to be MIN_AMOUNT_RECEIVED in handle_init_contract because it is the first parameter we're expecting to parse. Let's write the logic to handle this bad boy!

We'd like to store amountOutMin somewhere, so that we can use it later on when displaying stuff. This is what the context is for: we can just copy amountOutMin to our context. Since amountOutMin is an amount, it's a uint256, so it's 32 bytes long. This is exactly the size of PARAMETER_LENGTH! We use the function copy_parameter, which is designed exactly for this purpose!

switch (context->next_param) {
    case MIN_AMOUNT_RECEIVED:  // amountOutMin
        copy_parameter(context->amount_received,
                       sizeof(context->amount_received),
                       msg->parameter);
        context->next_param = PATH_OFFSET;
        break;

Notice we also need to set the next parameter we expect to parse. We set it to PATH_OFFSET. "But why PATH_OFFSET" you ask. Good question!

Arrays (and structs) are special kinds of parameters. They are "dynamic": meaning their size is not known in advance. path could have 1 or 1000 elements. So it's encoded differently: when parsing a dynamic parameter, the actual value we get is an offset to the actual array!

Let's work with an example. Here's the data for the an actual swapExactEthForTokens

[0]:  0000000000000000000000000000000000000000000000000000000001ced03f
[1]:  0000000000000000000000000000000000000000000000000000000000000080
[2]:  000000000000000000000000871732ce9ae62f065c6c787b600c1c44ceb873b8
[3]:  00000000000000000000000000000000000000000000000000000000611e293d
[4]:  0000000000000000000000000000000000000000000000000000000000000002
[5]:  000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
[6]:  000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7

As we said:

  1. [0] is amountOutMin
  2. [1] is the path offset (more later)
  3. [2] is to
  4. [3] is deadline

But what are [4], [5], [6]?
It's easy actually: [4] is the number of elements in the array, and [5] and [6] are the actual elements! Indeed you can see [4] is 2 (because there are two elements in the array). path[0] would be [5] and path[1] would be [6].

Indeed if we take the address located at [5], we get the WETH token, and if we look for the address located at [6], we get the USDT token. Remember the initial transaction was swapping ETH to USDT (going through WETH), so this all makes perfect sense!

And how did we know that [4] would be the starting point of the array? Well, this information is held in [1] (the offset). See, [1] holds the value 0x80 (it's hexadecimal), i.e 128 in decimal. Since each parameter is 32 bytes long, let's have a look at the offsets of this data:

  • [0] is at offset 0
  • [1] is at offset 32
  • [2] is at 64
  • [3] at 96
  • [4] at 128
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

There we have it! The array starts at 0x80, which is exactly the offset given by [1]!

Now that we have this in mind, let's keep on writing our parsing logic by adding a case that will store the offset in our context.
First let's add a parameter offset to our context (in boilerplate_plugin.h):`

typedef struct context_t {
    // previous stuff..
    uint16_t offset;
}

We use a uint16_t because I'm not expecting the offset to ever be bigger than 65535 (in fact I could probably use a uint8_t). Maybe your plugin will need to use a uint32_t if you want to handle bigger offsets!

Now let's add the switch case in our parsing method:

case PATH_OFFSET:
    context->offset = U2BE(msg->parameter,
                            PARAMETER_LENGTH - sizeof(context->offset));
    context->next_param = BENEFICIARY;
    break;

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
We're using PATH_OFFSET even though it's not declared. We'll add it at the end of the section in the enum parameter declared in boilerplate_plugin.h.

We're using U2BE which stands for Unsigned 2-Bytes Big Endian. Indeed offset is a uint16_t, so two bytes long. We want it to parse the two last bytes, so we pass it the offset 30 (PARAMETER_LENGTH - 2).

We've stored the offset in our context, but the next parameter we expect to parse is to (which we named BENEFICIARY).
Let's write the case for BENEFICIARY:

// We copy the `to` address to `context->beneficiary` using `copy_address()`.
// Indeed, the beneficiary will be an ethereum address, so we use `copy_address()`
// that will only copy 20 bytes instead of `copy_parameter()` which
// would've copied 32 bytes.
case BENEFICIARY:
    copy_address(context->beneficiary,
                sizeof(context->beneficiary),
                msg->parameter);
    context->next_param = PATH_LENGTH; // See comments below
    context->go_to_offset = true; // See comments below
    break;

The next parameter we would've expected to parse would've been deadline, but we said earlier that we would not be displaying it. So our next stop would have to be at context->offset, which will be where PATH_LENGTH will be at. One way to do this would be to use a boolean go_to_offset in our context, and modify our function to:

  1. Check if go_to_offset is true
  2. If it is, check whether we have reached context->offset.
  3. If we have not -> early return (so we continue parsing)
  4. If we have, proceed to switch case.

Ok let's do that!
First modify our context structure (boilerplate_plugin.h):

typedef struct context {
    // previous stuff...
    bool go_to_offset;
}

And then modify the parsing logic to add a check before the switch:

// Hint: We're adding `SELECTOR_SIZE` because `msg->parameterOffset` also holds
// the `SELECTOR` (4 bytes ID of the method) data.
if (context->go_to_offset) {
    if (msg->parameterOffset != context->offset + SELECTOR_SIZE) {
        // We still havn't reached the offset...
        return;
    }
    context->go_to_offset = false;
}
switch (context->next_param) {
    // previous code...
}

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
msg->parameterOffset corresponds to the offset in the data of the transaction. It is updated by the Ethereum App everytime it parses more data. It's useful for parsing purposes!

The next case we need to write is the one for PATH_LENGTH:

// We are now at the parameter `PATH_LENGTH` (`2` in our example).
// Recall that we wish to access `path[1]`,  which is located two parameters 
// away from the length. Hence we set `context->offset` to be the current 
// offset (*minus SELECTOR_SIZE because remember it holds the selector in it*)
// plus two chunks (`PARAMETER_LENGTH * 2`).
case PATH_LENGTH:
    context->offset = msg->parameterOffset - SELECTOR_SIZE
                        + PARAMETER_LENGTH * 2;
    context->go_to_offset = true;
    context->next_param = TOKEN_RECEIVED;

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
We could've computed this offset directly in the switch case of PATH_OFFSET since we are not using the path length at all. However we decided to showcase how one could access the path length :)

And we finally end up parsing the last chunk of data, which should contain the address of the token the user will be receiving:

// We simply copy the address in `context->token_received`!
// Notice we've set `context->next_param` to `UNEXPECTED_PARAMETER`.
// This is because we don't expect any additional parameters.
// To account for that, let's not forget the default case,
// which should set `msg->result` to `ETH_PLUGIN_RESULT_ERROR` to
// let the ethereum app that something went wrong.
`
case TOKEN_RECEIVED:
    copy_address(context->token_received,
                sizeof(context->token_received),
                msg->parameter);
    context->next_param = UNEXPECTED_PARAMETER;
    break;
default:
    PRINTF("Unexpected parameter: %d\n", context->next_param);
    msg->result = ETH_PLUGIN_RESULT_ERROR;
    break

To summarize, the complete code should look something like that:

static void handle_swap_exact_eth_for_tokens(ethPluginProvideParameter_t *msg,
                                                context_t *context) {
    if (context->go_to_offset) {
        if (msg->parameterOffset != context->offset + SELECTOR_SIZE) {
            return;
        }
        context->go_to_offset = false;
    }
    switch (context->next_param) {
        case MIN_AMOUNT_RECEIVED:  // amountOutMin
            copy_parameter(context->amount_received,
                           sizeof(context->amount_received),
                           msg->parameter);
            context->next_param = PATH_OFFSET;
            break;
        case PATH_OFFSET:  // path
            context->offset = U2BE(msg->parameter, PARAMETER_LENGTH - 2);
            context->next_param = BENEFICIARY;
            break;
        case BENEFICIARY:  // to
            copy_address(context->beneficiary,
                        sizeof(context->beneficiary),
                        msg->parameter);
            context->next_param = PATH_LENGTH;
            context->go_to_offset = true;
            break;
        case PATH_LENGTH:
            context->offset = msg->parameterOffset - SELECTOR_SIZE + PARAMETER_LENGTH * 2;
            context->go_to_offset = true;
            context->next_param = TOKEN_RECEIVED;
            break;
        case TOKEN_RECEIVED:  // path[1] -> contract address of token received
            copy_address(context->token_received,
                        sizeof(context->token_received),
                        msg->parameter);
            context->next_param = UNEXPECTED_PARAMETER;
            break;
        default:
            PRINTF("Param not supported: %d\n", context->next_param);
            msg->result = ETH_PLUGIN_RESULT_ERROR;
            break;
    }
}

Ok we've added a couple of enums variants, so let's add them to our enum definition in boilerplate_plugin.h:

typedef enum {
    // previous stuff...
    PATH_OFFSET,
    PATH_LENGTH,
    UNEXPECTED_PARAMTER,
} parameter;

Congrats! We've juste written a parser for our first method!
Let's now move on to the next step: handle_finalize!

Finalize

This step can be decomposed in two easy steps:

  1. Deciding how many screens we will wish to display.
  2. Letting the Eth App know whether we need information regarding specific tokens or not.

To address 1, let's think about what exactly we'd like to display.
Our user is simply swapping tokens, so the useful for information would be: the number of ether sent, the number of tokens received, and the beneficiary. This would look something like that:

| Swap    | Receive Min  | Beneficiary |
| ETH xx  | USDT xx      | 0x...       |

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
The Eth App will add additional screens to "Accept" or "Reject" the transaction.

So the answer to 1 is 3!

Now to answer 2: we will need information regarding the token that the user will receive. We already have the information for ETH: ticker is "ETH", number of decimals: 18.

So the code you need to edit for handle_finalize would look something like this:

msg->numScreens = 2;
msg->tokenLookup1 = context->token_received;

You can only request the information of two erc20 tokens, via tokenLookup1 and tokenLookup2. There is no tokenLookup3!

And this part is done! Next part is the part where the Ethereum App returns the requested information regarding the tokens.

Provide Token

If the Ethereum App found some info on the requested tokens, it should be found in msg->token1 and msg->token2.

In our plugin, we need to make sure we prepare for the case where the token is NOT found by the Ethereum App.

Here's the code for our plugin, with comments:

    if (msg->token1) {
        // The Ethereum App found the information for the requested token!
        // Store its decimals.
        context->decimals = msg->token1->decimals;
        // Store its ticker.
        strlcpy(context->ticker,
                (char *) msg->token1->ticker,
                sizeof(context->ticker));

        // Keep track that we found the token.
        context->token_found = true;
    } else {
        // The Ethereum App did not manage to find 
        // the info for the requested token.
        context->token_found = false;

        // Default to ETH's decimals (for wei).
        context->decimals = 18;
        // Default to "???" when info was not found.
        strlcpy(context->ticker, "???", sizeof(context->ticker));
        
        // If we wanted to add a screen, say a warning screen for example,
        // we could instruct the ethereum app to add an additional screen
        // by setting `msg->additionalScreens` here, just like so:
        // msg->additionalScreens = 1;
    }

The code is pretty straigthforward. One thing to note though is the msg->additionalScreens which will tell the Ethereum App "Hey actually, I will need X additional screens!"

Next step is going to be about choosing the wording for your first screen!

Query Contract ID

The Ethereum App is asking for some information to display. For the moment, it just wants a sort of "high-overview" of what this method is about (in our case, a swap). Let's look at what needs to be done.

The device screen has two lines. For example:

|  Ledger  |    <-- Upper line
|  Rocks   |    <-- Bottom line

In this call, the upper line is represented by msg->name. It is meant to hold the Plugin name.
The bottom line is represented by msg->version (legacy name). It is meant to hold the "overview" of the action the user is taking. For example "Stake", "Vote", "Buy" In our example, the user is swapping, so we'll use "Swap"!

The boilerplate repo already contains the code which copies the PLUGIN_NAME to msg->name, so we just need to take care of the switch case:

switch (context->selectorIndex) {
    case SWAP_EXACTH_ETH_FOR_TOKENS:
    	strlcpy(msg->version, "Swap", msg->versionLength);
    	break;
}

This will result in the following screeen:

| Uniswap |
|  Swap   |

That's it! Now we can proceed to the last section!

Query Contract UI

The previous section was about displaying a single screen. This section is about displaying the rest of the screens. Similarly to the section juste above, the display itself is handled by the Eth App. The only thing that the plugin needs to do is determine what string needs to be displayed.

Each screen has a screen number (also called screenIndex). When the Eth App needs to display a screen, it just calls the plugin and gives it the screenIndex. The plugin fills msg->title and msg->msg accordingly. The Eth App then displays the strings. If the user presses the right button or the left button, the Eth App increments or decrements screenIndex and calls the plugin with the updated screenIndex. Here's visualization of the flow:

Eth App -> Plugin: We're on screen 1
Note right of Plugin: Determine what needs to be displayed
Plugin --> Eth App: 
Note left of Eth App: User sees screen
Note left of Eth App: User presses right button
Eth App -> Plugin: We're on screen 2
Note right of Plugin: Determine what needs to be displayed
Plugin --> Eth App:
Note left of Eth App: User sees screen
Note left of Eth App: User presses *left* screen
Eth App -> Plugin: We're on screen *1*
Note right of Plugin: Determine what needs to be displayed
Plugin --> Eth App:
Note left of Eth App: etc ...

We will be using msg->title to fill the upper line, and msg->msg to fill bottom line.

| Ledger |  <- msg->title
| Rocks  |  <- msg->msg

In our boilerplate example, this is what we'd like to display when performing a swap:

    0            1                2                <- screenIndex
| Send     | Receive Min. | Beneficiary |
| ETH 0.1  | USDT 300.12  | 0x37abc...  |

So let's dive right into it! In handle_query_contract_ui.c:

// Switch on `screenIndex`, and call some (yet-to-be-written) function
// to set the UI accordingly.
switch (msg->screenIndex) {
    case 0:
        set_send_ui(msg, context);
        break;
    case 1:
        set_receive_ui(msg, context);
        break;
    case 2:
        set_beneficiary_ui(msg, context);
        break;
    default:
        PRINTF("Received an invalid screenIndex\n");
        msg->result = ETH_PLUGIN_RESULT_ERROR;
        return;
}

Let's start by writing the set_send_ui function.

static void set_send_ui(ethQueryContractUI_t *msg, context_t *context) {
    // Copy the "Send" in the upper line.
    strlcpy(msg->title, "Send", msg->titleLength);
    
    // The number of ETH associated with this transaction is
    // located in `msg->pluginSharedRO->txContent->value.
    uint8_t *eth_amount = msg->pluginSharedRO->txContent->value.value;
    uint8_t eth_amount_size = msg->pluginSharedRO->txContent->value.length;

    // `amountToString` is a utility function that converts a `uin256_t` to
    //  a string.
    // `18` and `ETH ` refer to the decimals and the ticker.
    amountToString(eth_amount,
                eth_amount_size,
                18,
                "ETH",
                msg->msg,
                msg->msgLength);
}

We can write a similar function for set_receive_ui:

static void set_receive_ui(ethQueryContractUI_t *msg, context_t *context) {
    // Set the title
    strlcpy(msg->title, "Receive Min.", msg->titleLength);

    // This time use amountToString with data that we stored in our context!
    amountToString(context->amount_received,
                   sizeof(context->amount_received),
                   context->decimals,
                   context->ticker,
                   msg->msg,
                   msg->msgLength);
}

And finally we will need to write set_beneficiary_ui. Remember, this screen will only get shown if the address of the beneficiary doesn't match the user's address. This was done in handle_plugin_finalize, where we increased numScreens by one if the addresses were different!

static void set_beneficiary_ui(ethQueryContractUI_t *msg, context_t *context) {
    // Set the upper line
    strlcpy(msg->title, "Beneficiary", msg->titleLength);

    // Prefix the address with `0x`.
    msg->msg[0] = '0';
    msg->msg[1] = 'x';


    // We need a random chainID for legacy reasons with `getEthAddressStringFromBinary`.
    // Setting it to `0` will make it work with every chainID :)
    uint64_t chainid = 0;

    // Get the string representation of the address stored in `context->beneficiary`. Put it in
    // `msg->msg`.
    getEthAddressStringFromBinary(
        context->beneficiary,
        (uint8_t *) msg->msg + 2,  // +2 because we've already prefixed with '0x'.
        msg->pluginSharedRW->sha3, // Used by the function to compute the hash
        chainid);
}

And that's it for your plugin! You have all the logic, now it's time to test!

Testing

To test our code we are going to use Zondax's Zemu testing framework. It allows to quickly add tests, run them using the emulator speculos and take / compare snapshots to make sure that everything was correctly displayed.

We won't be using a docker image for this one: we will install yarn and the dependencies on your machine. So let's fire up a new terminal window!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Make sure you're not running / installing this from the docker container previously launched.

You will need:

  1. NodeJS and npm: instructions here
  2. Yarn: npm install -g yarn

Instead of writing everything from scratch, we'll start by copying the tests folder of the boilerplate plugin repo.

Ok now for an overview of what needs to be done:

  1. Install the dependencies
  2. Create a folder that will hold information about our contracts
  3. Build our plugin and the ethereum app
  4. Write the tests

Installing the dependencies

This part is easy, simply run:

cd tests && yarn install

This should install all the dependencies required for the tests to run properly.

Providing information regarding contracts

If you look carefully, you'll notice a folder named boilerplate inside the tests folder. This file contains two items.

The abis folder

The abis folder will contain the abis of the contracts you wish to support. In our example, Uniswapv2's contract address is this one: 0x7a250d5630b4cf539739df2c5dacb4c659f2488d. Its abi is obtainable via etherscan. We simply need to take the abi, put it in a file, and name the file with the contract address + ".json".
So 0x7a250d5630b4cf539739df2c5dacb4c659f2488d.json is the name of the file, and its content is the abi for this contract.
You can add here as much contracts as you'd like, as long as they are properly named (ending with .json and all lower case), and they contain a valid ABI.

The b2c.json file

The b2c.json file holds information regarding which contracts and which selectors the plugin is going to support.

{
    "chainId": 1, // chainID, no need to edit
    "contracts": [
        {
            "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", // address of the contract address we wish to interact with
            "contractName": "UniswapV2", // contract name, this field is not used so feel free to use whatever your name you wish to use.
            "selectors": { // list of selectors 
                "0x7ff36ab5": { // bytes of the selector
                    "erc20OfInterest": [ // more info down below
                        "path.1"
                    ],
                    "method": "swapExactETHForTokens", // method name: feel free to user whichever name you would like to use
                    "plugin": "Boilerplate" // plugin name
		        }
	        }
	    }
    ],
    "name": "Boilerplate" // plugin name
}

Fields are pretty explicit here. Keep in mind you can add as many contracts as you want (contracts is an array!) an as many selectors per contracts as you want.

The only field that is a bit tricky is the erc20OfInterest that selectors have.

In our example, we're trading ETH for erc20 (tokens). In order to display the token tickers (like DAI, UBI etc), the js library needs to know which field in the transaction will hold the token address.

For the swapExactETHForTokens method, the token we are going to swap is located in the element path.1, meaning the second element of path (think of it as path[1] in most languages). If the token address was located in to, then we'd simply put to.

The js library will then use the abi provided in abis/, parse the transaction data, and extract the address of the erc20. It will then look into its database, find the erc20 ticker (DAI for example), and send it to the device for later use.

Note that only TWO erc20OfInterest can be added! This is a limitiation due to the memory constraints on Nano Ledger S.

Build the plugin and the ethereum app

To build the plugin, you will need to go back to your docker setup. Fire up a new terminal window, go to your plugin_dev folder, and run ./start.sh.

In your container, go to the plugin repo, and in the tests folder.

cd app-plugin-boilerplate/tests`

If you've followed this tutorial, you simply need to run ./build_local_test_elfs.sh. If you have your own setup (without using docker), then open this file and edit the NANOS_SDK, NANOX_SDK and ETHEREUM_APP variables.

Writing the tests

We will be using Zondax's Zemu framework for testing. This will allow us to:

  • Create transactions either from a handcrafter transaction, or a raw transaction hash from Etherscan
  • Run the transaction using Speculos, our official emulator
  • Navigate through the transaction and accept it
  • Compare the displayed content with some screenshots of EXPECTED content (useful for regression!)

Your tests folder should contain a couple of subfolders:

  • elfs: contains the elfs generated by the build_local_test_elfs.sh script.
  • snapshots: will hold the snapshots of the expected display content.
  • src: the source code for our tests!

You guessed it: we are mostly going to work in the src/ folder.

First thing to do is edit src/generate_plugin_config.js. Line 5:

const pluginFolder = "boilerplate";

Edit boilerplate and put it in the name of the folder you created earlier on which hosts the abis/ and b2c.json files.

Next, edit test.fixture.js:

const NANOS_PLUGIN = { "Boilerplate": NANOS_PLUGIN_PATH };
const NANOX_PLUGIN = { "Boilerplate": NANOX_PLUGIN_PATH };

Once again, edit Boilerplate and put it the name of your plugin (the one you added to the Makefile at the beginning of this guide!).

Now for the actual testing:

Tests need to be located in the src/ folder and have the .test.js suffix. We recommend having one test file (or even more) per selector.
We also recommend having duplicating tests: having one for Nano S, and one for Nano X (in the same file).

There are two main ways of creating a test transaction:

  1. You can build your own transaction using your contract ABI and populateTransaction: see the first test for NanoS in swap_exact_eth_for_tokens.test.js.
  2. Replay a transaction from etherscan: see the NanoX test in swap_exact_eth_for_tokens.test.js.

Option number two is the fastest and easiest option: however option number one will give you more control and flexibility around the transactions you wish to test.

If you go with option number two, and you're wondering "but how do I get a raw transaction hex?", the answer is simple: take your etherscan tx, click in on the three dots in the upper right corner and click Get Raw Tx Hex. This should open up a page similar to this one. Voila!

Rename swap_exact_eth_for_tokens.test.js, open it and start editing!
Comments have been written throughout the code to help you along the way.

Most importantly, you need to:

  1. Edit the contractAddr variable
  2. Edit the pluginName variable
  3. Edit the name of the test
  4. Decide whether you wish to create your own transaction (and use the populateTransaction call just like the first test) or use a raw transacton from Etherscan (like in the second test).

The string passed in as the second argument to the function navigateAndCompareSnapshots represents the folder to which the snapshots will be compared to. The framework will make the screenshots for the current test and store them in snapshots-tmp/test_name. It will then compare those screenshots to the expected screenshots that should be in the snapshots/test_name folder. test_name here corresponds to the second string you pass in to navigateAndcompareSnapshots. When you create a new test, simply create a new folder in snapshots with the appropriate name. You won't have any snapshots in it, but it's ok, tests will still run (and fail, but we'll get to that later on).

To start the test, simply run yarn test (from your computer, not the docker container). This should print a lot of debugging info on your terminal, and open up a speculos window. If you're lucky and your plugin is working correctly / tests are setup properly, the speculos screen should display the transaction and automatically press the right buttons for you.

The first time you'll run those tests, you will probably get an error about images not being equal. Indeed, this is expected: the framework is taking screenshots while it's running, and comparing them to well either the screenshots of the boilerplate plugin if you haven't removed them, or non-existant screenshots!

So one way to fix that is to take the screenshots generated in the snapshots-tmp/test_name, and copy the to the snapshots/test_name folder! Just make sure the screenshots are correct and are what you expect them to be for this particular test!

What's next

For the next steps, simply follow this link! // ASK THOMAS OR NAFI WHAT LINK DEV SHOULD FOLLOW

Flow

This long sequence is a visualisation of all the interactions between the Ethereum App and the plugin. If you ever get lost, feel free to come back to it!

Note left of Eth App: Receives transaction
Eth App -> Plugin: ETH_PLUGIN_CHECK_PRESENCE
Plugin --> Eth App: 
Note left of Eth App: Plugin exists so initialize it
Eth App -> Plugin: ETH_PLUGIN_INITIALIZE
Note right of Plugin: Initializes context
Plugin --> Eth App:
Note left of Eth App: Splits contract data in chunks
Note left of Eth App: Sends said chunks to plugin
Eth App -> Plugin: ETH_PLUGIN_PROVIDE_PARAMETER
Note right of Plugin: Parses the chunk
Plugin --> Eth App:
Note left of Eth App: Repeats call for every chunk
Eth App -> Plugin: ...
Plugin --> Eth App: ...
Note left of Eth App: Finishes parsing tx
Eth App -> Plugin: ETH_PLUGIN_FINALIZE
Note right of Plugin: Notes which erc20 are needed
Plugin --> Eth App: Asks for info on erc20
Note left of Eth App: Gets info on erc20
Note left of Eth App: Send it back to plugin
Eth App -> Plugin: ETH_PLUGIN_PROVIDE_TOKEN
Note right of Plugin: Stores erc20 info
Plugin --> Eth App:
Eth App -> Plugin: ETH_PLUGIN_QUERY_CONTRACT_ID
Note right of Plugin: Prepares screen to display
Plugin --> Eth App:
Note left of Eth App: Display contract ID screen
Eth App -> Plugin: ETH_PLUGIN_QUERY_CONTRACT_UI
Note right of Plugin: Prepares screen to display
Plugin --> Eth App:
Note left of Eth App: Repeats call for every screen
Eth App -> Plugin: ...
Plugin --> Eth App: ...
Note left of Eth App: Sign / reject transaction
Select a repo