or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
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:
- 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:
Here's a rough overview of what the flow looks like for plugins:
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):
- 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 →Let's create a new directory, which will contain everything plugin related.
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 adocker-compose
file that will do all the magic for you. To start it, simply runstart.sh
.If you're getting this error:
- 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 →docker
.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 thevolume
in the docker-compose).To compile the ethereum app, simply run
cd
to it and runmake
!During the first compilation, a couple of things might come as a surprise:
BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH
. This is expected, don't panic.yes
.If everything goes well, you once compilation is done, you should have
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: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 thisMakefile
and change theAPPNAME
variable to your own plugin name! (e.gParaswap
,1inch
,Lido
…).Main
Let's start by tackling the first
EDIT THIS
comments inmain.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:
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):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…We should also adapt the
PLUGIN_NAME
variable. I'm writing a boilerplate plugin so I'll name itBoilerplate
but your plugin should definitely find a better name.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:
We should also edit the name of the
BOILERPLATE_SELECTORS
name to match the one declared inmain.c
.Now let's go back to
main.c
. We see the app callsdispatch_plugin_calls()
. Let's go through each function, one at a time. They should all have a correspondinghandle_*.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 setselectorIndex
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:So the parameters for this function are:
uint256 amountOutMin
: the minimum amount of tokens the user is going to get back from his swap.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 arepath[0]
andpath[1]
.path[0]
is corresponding to the OUTGOING token, andpath[1]
corresponds to the INCOMING token.address to
: the address to which the tokens will be sent back.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]
, andto
. We won't really care aboutdeadline
, since we are not going to display this information. We also don't need to parsepath[0]
since we know we are sendingETH
.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 isamountOutMin
.- 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->result
toETH_PLUGIN_RESULT_ERROR
.Now let's not forget to add this parameter name in the
parameter
enum located inboilerplate_plugin.h
.Ok let's jump into
handle_provide_parameter.c
then!Provide parameter
The function
handle_provider_parameter
dispatches the msg depending on whatselectorIndex
is. Let's add our selector: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.- 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 →context
being used frequently throughout the code. It's a structure defined in theboilerplate.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 thecontext
definition inboilerplate.h
to fit to your plugin!We set
next_param
to beMIN_AMOUNT_RECEIVED
inhandle_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 thecontext
is for: we can just copyamountOutMin
to our context. SinceamountOutMin
is an amount, it's auint256
, so it's 32 bytes long. This is exactly the size ofPARAMETER_LENGTH
! We use the functioncopy_parameter
, which is designed exactly for this purpose!Notice we also need to set the next parameter we expect to parse. We set it to
PATH_OFFSET
. "But whyPATH_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
As we said:
[0]
isamountOutMin
[1]
is the path offset (more later)[2]
isto
[3]
isdeadline
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]
is2
(because there are two elements in the array).path[0]
would be[5]
andpath[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 value0x80
(it's hexadecimal), i.e128
in decimal. Since each parameter is32
bytes long, let's have a look at the offsets of this data:[0]
is at offset0
[1]
is at offset32
[2]
is at64
[3]
at96
[4]
at128
- 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 ourcontext
(inboilerplate_plugin.h
):`We use a
uint16_t
because I'm not expecting the offset to ever be bigger than 65535 (in fact I could probably use auint8_t
). Maybe your plugin will need to use auint32_t
if you want to handle bigger offsets!Now let's add the switch case in our parsing method:
- 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 →PATH_OFFSET
even though it's not declared. We'll add it at the end of the section in the enumparameter
declared inboilerplate_plugin.h
.We're using
U2BE
which stands forUnsigned 2-Bytes Big Endian
. Indeedoffset
is auint16_t
, so two bytes long. We want it to parse the two last bytes, so we pass it the offset30
(PARAMETER_LENGTH - 2
).We've stored the offset in our context, but the next parameter we expect to parse is
to
(which we namedBENEFICIARY
).Let's write the case for
BENEFICIARY
: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 atcontext->offset
, which will be wherePATH_LENGTH
will be at. One way to do this would be to use a booleango_to_offset
in our context, and modify our function to:go_to_offset
is truecontext->offset
.Ok let's do that!
First modify our
context
structure (boilerplate_plugin.h
):And then modify the parsing logic to add a check before the
switch
:- 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
:- 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 →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:
To summarize, the complete code should look something like that:
Ok we've added a couple of enums variants, so let's add them to our enum definition in
boilerplate_plugin.h
: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:
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:
- 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 →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:You can only request the information of two erc20 tokens, via
tokenLookup1
andtokenLookup2
. There is notokenLookup3
!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
andmsg->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:
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:
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
tomsg->name
, so we just need to take care of theswitch
case:This will result in the following screeen:
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 thescreenIndex
. The plugin fillsmsg->title
andmsg->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 decrementsscreenIndex
and calls the plugin with the updatedscreenIndex
. Here's visualization of the flow:We will be using
msg->title
to fill the upper line, andmsg->msg
to fill bottom line.In our boilerplate example, this is what we'd like to display when performing a swap:
So let's dive right into it! In
handle_query_contract_ui.c
:Let's start by writing the
set_send_ui
function.We can write a similar function for
set_receive_ui
: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 inhandle_plugin_finalize
, where we increasednumScreens
by one if the addresses were different!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!- 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 need:
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:
Installing the dependencies
This part is easy, simply run:
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 thetests
folder. This file contains two items.The
abis
folderThe
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
fileThe b2c.json file holds information regarding which contracts and which selectors the plugin is going to support.
Fields are pretty explicit here. Keep in mind you can add as many contracts as you want (
contracts
is an array!) an as manyselectors
per contracts as you want.The only field that is a bit tricky is the
erc20OfInterest
thatselectors
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 elementpath.1
, meaning the second element ofpath
(think of it aspath[1]
in most languages). If the token address was located into
, then we'd simply putto
.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.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 theNANOS_SDK
,NANOX_SDK
andETHEREUM_APP
variables.Writing the tests
We will be using Zondax's Zemu framework for testing. This will allow us to:
Your
tests
folder should contain a couple of subfolders:elfs
: contains the elfs generated by thebuild_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:Edit
boilerplate
and put it in the name of the folder you created earlier on which hosts theabis/
andb2c.json
files.Next, edit
test.fixture.js
: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:
populateTransaction
: see the first test for NanoS inswap_exact_eth_for_tokens.test.js
.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:
contractAddr
variablepluginName
variableThe 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 insnapshots-tmp/test_name
. It will then compare those screenshots to the expected screenshots that should be in thesnapshots/test_name
folder.test_name
here corresponds to the second string you pass in tonavigateAndcompareSnapshots
. When you create a new test, simply create a new folder insnapshots
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 thesnapshots/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!