The handle_provider_parameter
function dispatches msg
according to the value of selectorIndex
.
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 write handle_swap_exact_eth_for_tokens()
.
Parsing these handlers is simple: just look at next_param
to be parsed and copy the data to your context.
Tip
You will see context
used frequently throughout the code. It is defined in the boilerplate.h
header file.
Since the execution toggles between the Ethereum App and the Plugin, the plugin needs somewhere to store the data (such as the contract address or the amount of tokens swapped) as it goes along. This is done in context
. Make sure you change the definition of context
in boilerplate.h
to suit your plugin.
Set next_param
to be MIN_AMOUNT_RECEIVED
in handle_init_contract
because it is the first parameter to parse. Let’s write the code to handle it.
We want to store amountOutMin
to be displayed later. This is what context
is for: just copy amountOutMin
to our context. Since amountOutMin
is an amount, it is a uint256
which are 32 bytes long. This is the size of PARAMETER_LENGTH
. Use copy_parameter
, which is designed 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 to parse. We set it to PATH_OFFSET
. Here is why.
Tip
Arrays (and structs) are special kinds of parameters. They are "dynamic", meaning their size is fixed because path
has any number of elements. Dynamic parameters are parsed using an offset to the array, and by looking up the array size.
Let's work with an example. Here is the data for an actual swapExactEthForTokens
[0]: 0000000000000000000000000000000000000000000000000000000001ced03f
[1]: 0000000000000000000000000000000000000000000000000000000000000080
[2]: 000000000000000000000000871732ce9ae62f065c6c787b600c1c44ceb873b8
[3]: 00000000000000000000000000000000000000000000000000000000611e293d
[4]: 0000000000000000000000000000000000000000000000000000000000000002
[5]: 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
[6]: 000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7
As we said:
- [0]
is amountOutMin
- [1]
is the path offset (more later)
- [2]
is to
- [3]
is deadline
But what are [4]
, [5]
, [6]
?
It is easy: [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]
is [5]
and path[1]
is [6]
.
If we read the address at [5]
, we see the WETH token, and if we look for the address at [6]
, we get the USDT token ). Remember the initial transaction swapped ETH to USDT (via WETH), so it all makes sense.
And how do we know that [4]
is the start of the array? This information is held in [1]
(the offset). See, [1]
contains 0x80
in hexadecimal or 128
in decimal. Since each parameter is 32
bytes long, let's have a look at the offsets of the data:
- [0]
is at offset 0
- [1]
is at offset 32
- [2]
is at 64
- [3]
at 96
- [4]
at 128
<===
There we have it. The array starts at 0x80
, which is exactly the offset given by [1]
.
With this in mind, let’s continue writing the parser.
But first let’s add a parameter offset
to our context
(in boilerplate_plugin.h
):
typedef struct context_t {
// other code...
uint16_t offset;
}
We use uint16_t
because we don’t expect the offset to be bigger than 65535, but we could probably use uint8_t
). Maybe your plugin needs uint32_t
if you need to handle bigger offsets.
Now let’s add a case condition to store the offset:
case PATH_OFFSET:
context->offset = U2BE(msg->parameter,
PARAMETER_LENGTH - sizeof(context->offset));
context->next_param = BENEFICIARY;
break;
Tip
We are using PATH_OFFSET
even though it is not declared. We will add it at the end of the section, in the enum parameter
declared in boilerplate_plugin.h
.
We use U2BE
, which stands for Unsigned 2-Bytes Big Endian
. Indeed offset
is a uint16_t
, so it is two bytes long. We need it to parse the two last bytes, so we pass the offset 30
(PARAMETER_LENGTH - 2
) to it.
We have stored the offset in our context, but the next parameter to parse is to
(which we named BENEFICIARY
).
Let’s write the case condition for BENEFICIARY
:
// The `to` address is copied to `context->beneficiary`, using `copy_address()`.
// Indeed, since the beneficiary is an Ethereum address, we use `copy_address()`
// which only copies 20 bytes compared to `copy_parameter()` which
// would have 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 to parse would be deadline
, but we said earlier that we won’t show this. Next, we have context->offset
, which is where PATH_LENGTH
is. One way to do this is to use a boolean go_to_offset
, and write the function to:
- Check if
go_to_offset
is true
- If it is, check whether we have reached
context->offset
.
- If we have not -> early return (so we continue parsing)
- If we have, proceed to case condition.
So first modify the context
structure (boilerplate_plugin.h
):
typedef struct context {
// other code...
bool go_to_offset;
}
Then add a condition before the switch
in the parsing code:
// Hint: We add `SELECTOR_SIZE` because `msg->parameterOffset` also contains
// the `SELECTOR` (4 bytes ID of the method) data.
if (context->go_to_offset) {
if (msg->parameterOffset != context->offset + SELECTOR_SIZE) {
// We still haven't reached the offset...
return;
}
context->go_to_offset = false;
}
switch (context->next_param) {
// previous code...
}
Tip
msg->parameterOffset
is the offset of the transaction data. It is updated by the Ethereum App every time it parses data. It is useful for parsing.
The next case condition is for PATH_LENGTH
:
// We are now at the parameter `PATH_LENGTH` (`2` in our example).
// Remember we want 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 it contains the selector*)
// 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;
Tip
We could calculate the offset in the case condition of PATH_OFFSET
since we don't use the path length. However, see above to see how to get the path length.
And then we parse the last chunk of data which contains the token address the user receives:
// We simply copy the address in `context->token_received`.
// Notice we set `context->next_param` to `UNEXPECTED_PARAMETER`.
// This is because we don't expect any other parameters.
// The default must set `msg->result`
// to `ETH_PLUGIN_RESULT_ERROR` to
// tell the Ethereum app if something goes 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 looks something like this:
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;
}
}
We have added a few enums, so let’s add them to the enum definition in boilerplate_plugin.h
:
typedef enum {
// other code...
PATH_OFFSET,
PATH_LENGTH,
UNEXPECTED_PARAMETER,
} parameter;
Congratulations, we have just written a parser for our first method!
Now let’s move on to handle_finalize
.