Over the last few weeks I’ve been developing an Ethereum app that uses metamask for user management, just like I said I would. Overall, this has been a painless experience, but I have found a few gotchas that I want to share. Hopefully this will save some developers time in the future.
My first roadblock came from an error that I discovered in the transaction popup window. For some reason, when I hit “accept” to send my transaction, I was greeted by an endlessly spinning orange circle. Upon expanding the popup’s console, I saw:
AssertionError: The field v must have byte length of 1 at FakeTransaction.setter [as v]
Hmmm… what does that mean?
I pinged Kumavis on slack and he told me it was related to EIP155, which was just recently supported. Any older version of testrpc would lack this support, so I upgraded and things worked fine:
npm i -g ethereumjs-testrpc
Another testrpc problem! Once you open up metamask and type in your password, you are connected to that provider. If you switch providers (e.g. from main net to kovan) and refresh the page, it will reset your connection and you will need to enter your password again.
But what happens if you reset testrpc? You should still be listening to localhost:8545
, but you’ll find that once you refresh the page, things won’t look right. In fact, things will look very, very wrong.
This is because metamask is tracking the block number and when you reboot testrpc that number resets to zero. This is a recipe for disaster. Fortunately, there’s a relatively easy way to avoid this: whenever you reboot testrpc, open up metamask and re-select localhost:8545
as your provider. Now refresh your page, click on metamask, and type your password. Voila! You’re connected to the new testrpc instance.
Your app will probably run into a race condition if you don’t properly check for web3. If you try to render your actual app before web3 is injected, everything will break. So I highly recommend something like the following for your app’s landing component (I’m assuming you’re using react):
if (typeof web3 == 'undefined') {return (<NoMetamask/>);} else if (web3.version.network == 'loading') {return (<WrongNetwork/>);} else {return (<Landing/>);}
Note that checking if (web3 !== undefined)
or if (web3)
or anything equivalent will not work. You need to compare the typeof
the injected web3 object.
This is may be more of a developer’s preference than the other ones, but I strongly recommend abstracting all your metamask calls into one file because you will find yourself reusing metamask calls a lot. Rather than collapsing a bunch of copypasta versions of the same function later, why don’t you just start with one metamask file in your util
directory? And don’t worry about not having access to web3
: it’s just a javascript object that you can pass around like a hot potato — it can go anywhere!
// metamask.jsexports.doSomething = function(web3, param) {
// I have access to web3, even though I'm not in the DOM!const user = web3.eth.accounts[0];
// Do stuff}
I can’t believe I still see this in production. As an ecosystem, we’ve gotten better about this, but I want to extinguish it like smallpox. Please, people. Ethereum has a ~15s block time. That may seem fast in the blockchain world, but it is a lifetime in the world of fast, reactive web applications. And if you’re using testrpc for development, you will probably find yourself forgetting how slow blockchains are. Do not forget how slow blockchains are. Say it out loud every day: “Waiting for Ethereum transactions is a bad user experience.”
Please please please don’t make your users sit around twiddling their thumbs after submitting a transaction. It’s so easy to handle a sendTransaction
callback (which is a transaction hash) by dispatching that hash to a queue and periodically checking for a receipt on it. Consider the following:
// metamask.jsexports.doSomething = function(web3, param) {const user = web3.eth.accounts[0];web3.eth.getTransactionCount(owner, (err, nonce) => {// Form your tx object...web3.eth.sendTransaction(obj, (err, res) => {if (err) { cb(err); }else { cb(null, res); }})}
// component.jsxcallMetamask() {metamask.doSomething(web3, 'something', (err, result) => {if (err) { console.log('Error doing something.') }else { dispatch({ type: 'QUEUE_TXHASH', result: result }) }})}
With that hash in your queue, you can display it in the toolbar and the user can happily continue with your app while waiting for the transaction to go through.
If it really is critical that the transaction is processed before advancing your UI (and you can honestly tell me this is the best UX flow you can think of), please give your users something exciting to look at — maybe the Ethereum whitepaper or this cat with a shower cap.
Three weeks of heavy metamask development and these are really the only gripes I can come up with. Really, metamask is marvelous and easy to set up. Generally speaking, you should be good to go if you follow the rules/suggestions outlined above and properly reference the web3 API (remember to always use asynchronous calls — synchronous ones won’t work in metamask).
If you have a question, but it wasn’t answered here, I recommend checking out the Ethereum stack exchange.
—
If you like my articles, follow me on twitter or check out my company, ConsenSys, because we do awesome stuff.
And, as always, there is an open invitation to join the Ethereum communityand help us conquer the world.