Creating an NPM package written in Rust
Ethane
Ethane is an open source Web3
client written in Rust with simplicity in mind. Essentially, it opens a doorway to the Ethereum blockchain, thus enabling the user to call smart contracts deployed there. Originally, it was maintained by th4s, and since our company, ZGEN, needed a tool like this, we started adding more and more features to it. This post is about Ethane, and how it was ported to a Wasm NPM package in order to communicate with the Ethereum network from NodeJS, with Rust running under the hood.
Wasm
Allow us to have a little intermezzo here to quickly introduce Wasm. WebAssembly (Wasm) is a lightweight virtual machine that aims to be fast and portable, making it an ideal choice to deploy in web applications. Thanks to the wasm-bindgen
package, Rust can be compiled to Wasm, resulting in a small and blazingly fast application. Not to mention the fact, that it can be deployed to NPM to be reused by NodeJS developers, which happened to be our goal as well. If you are interested in porting your Rust application to Wasm, be sure to check out the Rust and WebAssembly tutorial where you will be guided through the ins and outs of wasm-bindgen
, the crate that glues Rust and Wasm together.
Ethane to NPM
As I hinted before we wanted to create an internal tool to call smart contracts on the Ethereum blockchain in Rust, which can be published as an NPM package to be used by our NodeJS devs. However, Ethane itself was not written with Wasm-compatibility in mind, therefore, some wrapper functions were implemented to interface Ethane to Wasm and JavaScript. This was a challenging task since Rust is a strongly typed language contrary to JavaScript. Thus, the interface was written in a way that a JavaScript user can call it with basic types such as a String
, while the underlying Rust logic is running behind closed curtains. Sounds straightforward, however we met some bumps along the way. The main issue we faced occurred after the Rust code was successfully built to the Wasm target and we tried to run a little JavaScript program using our Wasm package. For some reason, we were greeted with this — slightly cryptic — error message:
$ ERROR in ../pkg/ethane_wasm_bg.wasm
$ module not found: Error: Cannot resolve 'env' in 'ethane-wasm/pkg'
Looks like there is a module called env
invoked in the Wasm binary, even though it was not explicitly imported in our JavaScript code. After a bit of searching around we found some issues similar to our problem, for example this one posted under wasm-bindgen
and another posted under wasm-pack
. Both issues were tracing the env
import back to a rust dependency that needed to be updated. However, none of those dependencies were directly imported in our Cargo.toml
so we thought they must be dependencies of our dependencies. Thus, we pinned the respective dependencies to the suggested version in our Cargo.toml
to overwrite the dependency tree. Nevertheless, the env
issue persisted.
It seemed that there’s no easy fix to our problem, so we needed to dig deep in the code. Since the error occurred in ethane_wasm_bg.wasm
, which is a binary file, it had to be converted to a human-readable form first. A command line tool called wasm2wat
serves just this purpose, that is, it converts the binary Wasm to WebAssembly Text Format (Wat). Just type the following in the terminal:
$ wasm2wat ethane_wasm_bg.wasm -o ethane_wasm_bg.wat
Opening the generated .wat
file showed a lot of imports from env
such as
which gave some pointers where to look next, quickly leading to this open issue under the ring
Rust crate. It listed exactly the same env
imports we were facing, so we were onto something. It seemed that Wasm imported some bigint C
functions from ring
that are not supported by Wasm. Our immediate thought was that tiny-keccak
— a cryprographic library used by Ethane — could be responsible for these bigint
functions because ring
wasn’t a direct dependency of Ethane. However, after some more digging, we found that the culprit was the ureq
crate, which is a simple, blocking HTTP library. ureq
uses ring
for TLS to encrypt data which is an optional feature of ureq
, so it can be easily disabled if you don’t need TLS. Nevertheless, we decided to use reqwest
instead that solved the problem, and we were able to use the NPM package without any issues.
Ethane to Wasm example
Okay, so we managed to publish our NPM package, but how does the actual Rust code look like? In this section, I’ll walk you through a small example to showcase how Ethane could be ported to Wasm. We’ll take the lazy and quick way by simply wrapping smart contract calls into a function that takes arguments supported by Wasm. This is probably not the best and most elegant way to go, however, having a relatively complex codebase, that uses generic types and lifetimes (currently not explicitly supported by wasm-bindgen
), it is a good way to have something up and running quickly, and optimize later.
Let’s say we want to call an Ethereum smart contract and use the result in NodeJS. First, we need a connection to an Ethereum node that is our doorway to the blockchain. Since there’s no blocking I/O in Wasm, an asynchronous client will be used. Actually, you wouldn’t even be able to compile something to Wasm that uses, for example, reqwest::blocking::Client
for HTTP under the hood.
Now that we have a connection, a smart contract caller can be set up. We need an ABI that describes the functions present in the contract as well. You can read more about how an ABI works here. For now, suppose we have our ABI file on a path accessible by our program so that we can create our smart contract caller as
Here, we see that an Ethereum address is also needed to be provided for the caller to know where to look for the smart contract. Note that I used unwrap
on a Result
type which should be avoided, since calling it on an Err
will cause our code to panic, and panics are masked by Wasm. This means that even though your code has panicked, you won’t see any indication of it when you run the Wasm executable. Trust me, I’ve been there. For the sake of this example, however, we can assume, that the given &str
is valid, so try_from_str
will not panic.
Suppose that our smart contract has a function called addTheseNumbers
that simply takes two 256 bit unsigned integers (Uint256
), adds them while doing some blockchain magic, and returns their sum. Assume that this function and its parameters are described properly in the contract.abi
file, so we can call this function and process it’s result.
Alright, so we have our result that can be used for further computations, at least in Rust. But what if our NodeJS devs would want to use this code? How can we convert this into Wasm quickly, without modifying Ethane’s source code? Well, let’s create another crate that uses ethane
and ethane_abi
as dependencies (built with the non-blocking
feature) and wrap the code snippets above into a wasm_bindgen
function.
There’s a lot happening in this code snippet, so let’s elaborate on what we see here. First, we are adding the attribute-like macro #[wasm_bindgen]
to our function in order to glue it into Wasm and make it callable from JavaScript. Our function takes parameters with types supported by Wasm and returns a Result that can be handled in JavaScript. JsValue
is a catch-all type that is inherent to JavaScript, e.g. a String
, or an u32
. We can call this function from NodeJS by specifying the Ethereum endpoint’s HTTP address, and the two numbers we want to add in our smart contract. The underlying logic didn’t change, however, we got rid of the panic
statements, and we return an error instead. This way, Wasm won’t mask the panics, and we get an informative error message in JavaScript about what went wrong.
Basically, this was it: a simple example to show how to quickly compile an existing Rust package to Wasm, without modifying the existing codebase. Of course, there are better and more elegant ways to build something in Rust that ports to Wasm but that usually requires unsafe code with low level pointers and/or some design trade-offs that sacrifice Rust’s type safety for Wasm-compatibility. So, this lazy, high level solution might be the first simple implementation that someone with limited Rust-Wasm experience, like me, could concoct.
Conclusions
In this post I introduced our smart contract calling tool and the process of porting it to Wasm. wasm-bindgen
is an awesome crate to glue our Rust to Wasm, however, as we have seen, sometimes there are hidden challenges emerging from unexpected sources while using it.
Nevertheless, after a few days of slightly painful debugging and code restructuring, we had our tool up an running as an NPM package that was generated from Rust via Wasm. It is worth noting, that the issue with ring
referenced above is still open and Wasm support is being actively worked on. Regardless, we managed to develop something unique, and it is fun to consider that, under all that Wasm binary, it is actually Ethane calling smart contracts deployed on the Ethereum blockchain. Finally, if Ethane caught your attention, consider supporting it on stakes.social.