owned this note
owned this note
Published
Linked with GitHub
# Table of contents
[TOC]
# 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!
:::info
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:
![](https://i.imgur.com/4AjL6D2.png)
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:
```sequence
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](https://docs.docker.com/get-docker/)
2. Docker-compose: [instructions here](https://docs.docker.com/compose/install/)
:::info
:bulb: 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`.
```bash
cd plugin-tools
./start.sh
```
:::warning
If you're getting this error:
```
ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?
```
:fire: This means you either:
- Failed to install`docker`.
- 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).
```bash
root@997ccff31349:/plugin_dev$ ls
app-ethereum plugin-tools
```
To compile the ethereum app, simply run `cd` to it and run `make`!
```bash
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](#flow).
### 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](https://github.com/LedgerHQ/app-plugin-boilerplate) 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](https://developers.ledger.com/docs/nano-app/design-requirements/).
- `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](https://etherscan.io/txs?a=0x7a250d5630b4cf539739df2c5dacb4c659f2488d), we can find a transaction that has the method "SwapExactEthForToken". [Here's an example](https://etherscan.io/tx/0x216bfa6fb8488901d168810cda1b716d1abcb002a87c3224180deaff00c950fc). If you scroll down and click "Click to see more", you will see the data:
![](https://i.imgur.com/30fGlqb.png)
Bingo! The corresponding method ID is: `0x7ff36ab5`.
So now let's edit our code:
```c
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*):
```c
// 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...
```c
#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.
```c
#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:
```c
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](#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`.
```c
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;
}
```
:::info
:bulb: 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`.
```c
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:
```c
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.
:::info
:bulb: 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!
```c
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!
:::info
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](https://etherscan.io/tx/0xdf6ea6b4e45d53416713f72e6ab36f6979ad59839b9c83e0bc64b12b460bfc81) 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](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2), and if we look for the address located at `[6]`, we get [the USDT token](https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7). 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` :bulb:
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`):`
```c
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:
```c
case PATH_OFFSET:
context->offset = U2BE(msg->parameter,
PARAMETER_LENGTH - sizeof(context->offset));
context->next_param = BENEFICIARY;
break;
```
:::info
:bulb: 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`:
```c
// 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`):
```c
typedef struct context {
// previous stuff...
bool go_to_offset;
}
```
And then modify the parsing logic to add a check before the `switch`:
```c
// 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...
}
```
:::info
:bulb: `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`:
```c
// 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;
```
:::info
:bulb: 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:
```c
// 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:
```c
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`:
```c
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... |
```
:::info
:bulb: 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:
```c
msg->numScreens = 2;
msg->tokenLookup1 = context->token_received;
```
:::warning
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:
```c
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:
```c
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:
```sequence
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`:
```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.
```c
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`:
```c
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!
```c
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](https://github.com/Zondax/zemu) testing framework. It allows to quickly add tests, run them using the emulator [speculos](https://github.com/LedgerHQ/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!
:::warning
:fire: Make sure you're not running / installing this from the docker container previously launched.
:::
You will need:
1. NodeJS and npm: [instructions here](https://nodejs.org/en/download/package-manager/)
2. Yarn: `npm install -g yarn`
Instead of writing everything from scratch, we'll start by copying the [`tests`](https://github.com/LedgerHQ/app-plugin-boilerplate/blob/master/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](https://etherscan.io/address/0x7a250d5630b4cf539739df2c5dacb4c659f2488d#code). 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](https://github.com/zondax/zemu) 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](https://etherscan.io/tx/0x0160b3aec12fd08e6be0040616c7c38248efb4413168a3372fc4d2db0e5961bb), 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](https://etherscan.io/getRawTx?tx=0x0160b3aec12fd08e6be0040616c7c38248efb4413168a3372fc4d2db0e5961bb). 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](https://www.youtube.com/watch?v=dQw4w9WgXcQ)! // 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!
```sequence
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