Creating an NPM package written in Rust
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.
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
$ 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
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.
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_abi as dependencies (built with the
non-blocking feature) and wrap the code snippets above into a
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
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
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.
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.