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.
Here’s a list of requirements that I think a good crypto wallet should have, in no particular order:
hardware wallet support: this one is a no-brainer, in some cases this isn’t possible (a lightning node needs some hot privkeys)
core portability: the internal heavy lifting should compile on any platform, to native code
remote control: I should be able to use nodes embedded into the program or remotely, or I should be able to use it the interface as a remote control of a “wallet node” living elsewhere, maybe we should even be able to communicate over SSH tunnels
no web bullshit: related to the above, see below for details (// TODO extend this more)
extensible: you should be able to use the internal libraries to build tools for whatever protocol you’re building on top of a network
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:
wibbly wobbly types make it impossible to trust your program
wibbly wobbly types put a lot more emphasis on using tests to ensure correctness, which is bad beacuse
tests can’t cover all cases, cases that a rich type system would handle trivially
programmers don’t like writing tests, so they inevitably skip writing them
linters can’t handle every mistake
the lack of a sufficient standard library makes heavy reliance on upstream packages for most functionality a necessity
the low barrier to entry means that NPM is filled with low quality and under-maintained packages
the DOM is for documents, its layout engines are not as well-suited interactive applications
the lack of types make runtimes have to do much more work to make programs run at reasonable speeds
the extra work from runtimes means programs use much more memory, and are still slower and less responsive
the dominant web frameworks add more layers of abstractions to solve problems and introduce new ones, which exacerbates the above points
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.
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.
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.
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:
easy to understand, according to the conventions of the platforms they’re on
cheap, relying on secure L2s where possible
fast and efficient, using languages that compile to native code to do heavy lifting
safe, not relying on centralized third parties and avoiding information leakage
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.
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.
multisigs, obviously
timelocks, vaults, etc.
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.
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.
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?
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.
component: some library or external connection that provides one or more services, which may be constructed in terms of other services
services: standardized piece of functionality that can be provided by different components to accomplish some kind of behavior, to share intent logic across different kinds of systems
intent: an action taken by the user effecting the outside world, interacting with services and optionally depending on some resources
resources: provided by services, something that exists conceptually outside of the wallet
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:
“send 2 Bitcoins to address bc1p...
”, completing when the transaction is confirmed/buried
“generate an address”, completing immediately (or an invoice, etc.)
“fulfill invoice lnbc1...
”, completing when we receive the preimage or the payment expires
“bridge 100 DAI from ETH mainnet to zkSync”, completing when the funds are available on the L2
“open a channel with peer 03cafebabe
”, completing when the channel is opened (and also closing, splicing, etc)
“upload a file to a Sia host”, completing when, yk, it’s uploaded
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.
// TODO basically mist but with flatpak/wasm/etc and more restricted access to UIs