1 unstable release

0.1.0 May 24, 2024

#1670 in Magic Beans

Download history 106/week @ 2024-05-21

106 downloads per month

MIT license

2MB
9K SLoC

Rust 4.5K SLoC // 0.1% comments JavaScript 4K SLoC // 0.0% comments

WASM Abstraction for WalletConnect Modal Bindings

This is an abstraction for bindings created for WalletConnect's Modal. The cargo doc --target wasm32-unknown-unknown --open provides information on using this library.

Usage

Why does this exist?

It seemed complicated to use WalletConnect with a Rust WASM frontend and I wanted the bindings for my own projects.

The goal of this abstraction is just to make it easy to use WalletConnect's Modal with Rust based Ethereum/blockchain providers. (ethers-rs, alloy-rs, web3-rs, etc) Though, currently this crate only supports ethers-rs. This was also written a while ago, so the packages from NPM are not new. (I also have not searched for other libraries that might serve the same purpose as this one for a while)

Why was it written this way?

I used writing this library to experiment with Rust in general.

One of the goals in writing it was to simplify the bridge from the dynamic typing in JS to something solid and reliable in Rust. I think it's overcomplicated internally, my original attempts to wrap JsValue's are hard to follow.

A struct with internal JsValues values cannot directly interact with the trait requirements for ethers, alloy, and other providers because they require Send + Sync restrictions. Without an existing runtime, a safe Rust solution is to use message sending with a library like async_channel and a runtime/server to listen for these messages. This is what this crate does, it acts a runtime/server, and providers requests send messages to pass the trait restrictions from the Rust based providers.

The current version to perform a JSON request against the JS binded RPC provider looks something like the following

let request = RequestFromTransport {
    full_rpc_request: json_request,
    queue_type: queue_type,
    send_back_to: send,
    started: false,
    start_time: None,
    queue_time: Utc::now(), 
};

if let Err(_e) = server_cmd_cli
    .send(ClientCommands::RpcRequest(request))
    .await
{
    return Err(WalTransportFailure {
        msg: "Channel closed. This should never happen. Open an issue: {_e:?}".into(),
        failure: WalletConnectTransportFailure::Json(JsonFailure::JsDe),
    });
}

if let Ok(result) = receieve.recv().await {
    result
} else {
    return Err(WalTransportFailure {
        msg: format!("Channel closed. This should never happen. Open an issue"),
        failure: WalletConnectTransportFailure::Json(JsonFailure::JsDe),
    });
}

Which has the associated runtime/server that is spawned to run as a promise in the background

loop {
        let next_cmd = self.command_receiver_channel.recv().await.unwrap();

        match next_cmd {
            ClientCommands::RpcRequest(req) => {
                if self.rpc_runner.has_active_request() {
                    match req.queue_type {
                        QueueTypes::UserKillRequest => {
                            let _ = self.rpc_runner.send_request_kill_signal().await;

                            // User requests can quickly change before being finished. If a user changes
                            // their request, then other user requests should just be cancelled.
                            if let Some(next_kill_req) = &self.active_kill_request_next {
                                let _ = next_kill_req.kill().await;
                            }

                            self.active_kill_request_next = Some(req);
                        }
                        QueueTypes::SystemQueueRequest => {
                            self.rpc_request_queue.push_back(req);
                        }
                    }
                } else {
                    let _ = self.rpc_runner.send_request(req).await;
                }
            }
        /* The rest of the matched types are also included here, but removed in this description */
        }
}

After writing all of this, I realized this could have been simplified with a wrapping type that does all of this with closures. So, I took the runtime/server related stuff, and made a custom type, JsArc, that I think would help to reduce the complexity. In the example of RPC requests, the above could have been something like the following

let (s, r) = async_channel::unbounded();
let request: String = request;

self.jswallet.with_self_async(|jswallet| async move {
	let result = jswallet.request(request).await;
	s.send(result.to_string().unwrap()).await;
	
    jswallet
})
.await;

let result: String = r.recv().await;

Though, I didn't fix the bindings abstraction because I had already written message sending and receiving for everything. If I were to write it again, I think I would just use that wrapped JsValue type instead of message passing to a custom runtime/server.

The rest of the complexity is mostly associated with experimentation

  • I tried writing macros because providers had to manually be cloned when moving into custom futures
  • I tried creating queues as I felt like it might be a good step for a future library to use to know when to cancel requests to wallets
  • I tried creating a shared format for rust providers errors
  • I wanted downstream users to be able to opt out of manually managing NPM dependencies, so this crate uses a pre-bundled version. (But users also have the option to use npm with feature flag npm)
    • The change from web linked modules to dynamic modules caused the original bindings to not match. (And parts of the old bindings were left)

Building

Using the example

wasm-pack build --target web
cargo install miniserve
miniserve .

Miniserve starts a webserver in current directory. It will display a address to visit, then load the index.html file at that address.

Building a new web3_bindings.bundle.js

cd js
npm install
cd bindings
npx webpack

You will need to have NPM and Webpack installed for the above to work.

Dependencies

~35–52MB
~1M SLoC