Towards a better wallet framework

Revised 2021-02-23.

Minor revision on 2024-07-07.

Cryptocurrencies have a problem. I mean, there’s many problems, some of them are really substantial.

But one that I think is a pretty big blocker to ecosystem health is the lack of good wallet infrastructure. In some places this is less of a problem than others, but I’ll get into that later.

Requirements

Here’s a list of requirements that I think a good crypto wallet should have, in no particular order:

Objection to “the web”

Certain crypto ecosystems have a reliance on web technology.

Web browsers are bad. Google essentially controls the standardization process and Mozilla, while not as evil as Google, is woefully mismanaged and does Firefox does not have the market share to challenge Google’s near-hegemony. Google has an incentive to prevent adoption of decentralized technology, and if they become threatened by it they will absolutely take steps to disrupt it. Reliance on web is a liability, even ignoring the security risks.

Javascript (and HTML) is a bad language/environment for many reasons:

More generally, the web is a bad platform for distributing applications used in decentralized networks because browsers are fundamentally designed around a model of “clients that request pages from servers”. Breaking out of this model while using the same technology, as some do try, is difficult. When it goes wrong we see disasters like Mist.

The sad part is Mist was actually a neat idea. Instead of shipping the wallet as an extension that injects functionality into pages, the idea is that we’d distribute dapps as the pages themselves and they’d run in a kind of dapp browser. But the execution of this idea was flawed and insecure, as relying on Electron and the web stack is what doomed it.

Distribution

The narrative that it’s a good platform for due to its native sandboxing and ease of distribution is flawed in this space. Sure, we have hardware wallets, but with more complexity in the applications we interact with it’s hard to ensure that the transactions you’re signing are actually doing as they behave. So we have to do some level of auditing on the software, making the need for sandboxing it less strong. But regardless, once we audit what we get from the site we’re using it’s trivial for the maintainer to push a new version without our knowledge. There’s ways around this (hosting on Skynet/IPFS/etc.), but these have their own limitations, don’t solve all the issues with the web, and aren’t as well-understood by users as desktop applications.

So if you’re afraid of your application being malicious your only choice is to audit the code and then build it from source before you use it. And then all the arguments about the web being an ideal distribution platform are moot! Obviously most people don’t build their software from soruce. On Windows it’s even a pretty big pain in the ass. But most platforms at this point are moving to a system with package managers and a more sensible distribution model. System package repository maintainers ideally are a lot more trustworthy, and third parties to manage downstream patches make it a lot more difficult for the authors to sneak in a malicious update without it being noticed.

So yeah, the web is great for application distibution if you want to blindly trust things. Which most users do, even if that’s bad, which leads us to our next point.

Decentralization

The strict client-server model is also bad because it doesn’t map into how distributed protocols are usually architected. You can try, there’s WebTorrent and such, but it’s a ton of work and involving web protocols under the hood infects the architecture of the program and increase the work you have to do.

Just using the web prompts developers to just rely on centralized third party services like Infura to provide data about what’s happening on the chain.

If the way most users interact with the network isn’t in a decentralized fashion, then what’s the point of using cryptocurrencies in general? If we can make running nodes that improve the health of the network as easier to do, then it’s a lot easier to justify the sensible default being that every user runs a node without them having to worry about it.

Responsibility to users

As developers, we have a set of skills that most don’t. We have an understanding of technology that most users shouldn’t have to learn. We can’t just say “users will choose the wallet that suits their needs”, since most users will just do the thing that’s easiest and cheapest for them. We have to build good wallets that are:

We have a responsibility to build good wallets and put the needs of users above our own, as users are dependent on us to do due diligence and not take shortcuts when developing software just because it makes our lives easier.

This doctrine applies to user-facing software more generally, not just wallets.

UX and functionality

This document isn’t really supposed to be concerned with what the wallets do specifically, more about how they do it. But there’s a few higher level features that I think should be mentioned.

Not every wallet needs to support these features, but they’re important considerations to have. Privacy features especially are important because often it’s the case that the more people that use them, the more effective they are.

Issues with major wallets

This article is primarily aimed at Ethereum wallets since it’s a major network and most of the wallets really suck.

Metamask is really holding the industry back. It kinda sets the standard in the Ethereum ecosystem for what dapp developers are capable of through its limited set of capabilities and services that it provides. It’s also hopelessly dependent on Infura and related services for everything the user does, selling user data in the process as an income stream. It’s also not free software, which is a red flag on its own. They kinda seem to want to go in the direction of more programability with their new “snaps” system, but that’s hardly even a quarter-measure in the right direction. You’re extremely limited in what you can build with it and it doesn’t change the basic interface of what dapp developers use.

Another example is Coinbase Wallet, which is primarily an Ethereum wallet. Just today I helped someone who wanted to send some small amount of USDT to someone on Ethereum, but didn’t have any ETH on Ethereum to pay the gas fees. They did have enough ETH on Coinbase’s Base rollup to pay the fees in theory, but they did not understand the difference. The wallet provided no help in rectifying the situation, which is why they were there, for us to explain that the little blue circle icon meant that the ETH was on Base and they had to swap it for native ETH on Ethereum. Some people would argue “oh this is a nonissue with account abstraction and based rollups and shared sequencing and gas abstraction and and and…!”. This argument misses the forest for the trees. It’s a UX issue, not an infrastructure one, but it’s a lot easier to sell those infrastructure to VCs who will fund them than it is to fund wallet UX improvements.

Functionally they do a lot, and a lot of people get a lot of benefit from them. Which is great. But we, as an industry, can do a lot better.

Most Bitcoin wallets are really great, and many of the more recently-developed ones do support Lightning, but have slight issues. I haven’t used any of the wallets designed for what are being called “federated mints” like Fedimint and eCash, but people seem to like them.

But I wish wallets like Phoenix gave you the option to run the node separately from the phone and remote control it. That way recovery can be easier if you lose your phone. But that use case is reasonably well-suited by Zap at the moment.

In a prior version of this article I had listed several headings aimed at specific wallets and had intended to do more thorough analyses of the issues they have, but that’s not really what this article is supposed to be about. It’s not really constructive to deeply tear into these projects, so I simplified this section and added some more recent reflections.

Solutions

So what do we do about this situation?

Just build a wallet that checks all the boxes in that list at the top. But how?

First of all, Rust is the only really suitable choice to build this. You could argue that you can use Go, since you can kinda use Go on every platform you could want to. But there’s extra effort required in calling into Go from higher level languages. But also Go sucks, don’t use it. C/C++ might also be suitable but Rust just has better tooling for this kinda stuff and is just nicer and more productive overall.

I can’t think of any other language that would be suitable. Something that’s necessary is that it must be reasonably easy to call into it from any other language and not have strong runtime requirements (like a GC or something), and only languages that I know of that provide this that are popular are C, C++, and Rust. There’s also things like D and Zig that you might be able to get away with but like who knows those?

Architecture

I haven’t completely sorted out the nomenclature but there’s a general idea.

Our wallet library is constructed out of various, fairly general, pieces.

An example of a component would be an RPC connection to a bitcoind or geth node. It has a certain amount of configuration to initialize and some properties about its state that it exposes (like if it’s running, booting, shutting down, etc.). Components can expose services. Services are more standardized and give us access to more general behaviors. A goal is that we can assemble (“virtual”?) components that rely on some services, which we would use to expose services to higher-layer protocols (such as uniswap).

The high level wallet UI will be designed in terms of user intents. Some applications are already moving to this model, but it isn’t very popular yet. Android technically already has a concept similar to this, which is designed for inter-app communication.

Some intents may resolve immediately, and some may take time to resolve. But ongoing intents should be made available to the user, possibly allowing the user to abort it.

Examples of intents:

So note that number 3 involved two networks. This is where resources come in. Intents may require resources to operate on. Some of these are pretty broad like “a bitcoind’s embedded wallet”, “x amount of funds on Foo ledger”, etc. I haven’t completely sorted out how intents are supposed to describe the resources they require (especially when it’s a fungible kind of thing). I also haven’t completely sorted out the relationship of how services should provide and allocate resources to intents.

But what’s relevant here is intents may require exclusive or shared locks on resources. An intent that spends funds would require exclusive ownership of the resource. Then we can schedule execution of intents such that they can’t compete with each other. A coinjoin or an atomic swap intent might require locks on specific utxos, whereas simple sends can be more coarse.

What this gives us is it lets us reason about future intents that we haven’t tried to begin yet. If we bridge funds from one network to another, we can expect that we’ll have the resource on the destination ledger once that intent completes. If we’re careful about how we design intents, a user should be able to schedule several intents into the future and then leave the wallet to deal with it later.

This sounds like a lot of complexity, which it is, but I’m confident that we can use a design language like this to build a very powerful wallet infrastructure and expose a subset of it to build a very well-polished series of wallets.

The long game

// TODO basically mist but with flatpak/wasm/etc and more restricted access to UIs


Articles Index