Creating an NPM package written in Rust

PopcornPaws
7 min readJun 4, 2021

--

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.

--

--