Lessons Learned While Building a Production Ready Dapp

This is the second in a series of posts that detail how we went about creating and deploying our Interactive Coin Offering. In the first post, we discussed the reasoning behind the smart contract architecture. In this post, we will share the lessons we learned and some useful patterns that arose from getting the UI ready for production. The code we’ll be talking about is in this repo.

General Architecture and Tech Stack

We used our dapper boilerplate as the base for the dapp. Dapper itself is built on top of Facebook’s create-react-app, but comes with a lot more out of the box. It uses our lessdux redux architecture, our create-redux-form for form management, and our kleros-scripts for formatting and linting.

The folder structure follows a very structured pattern that has proven to scale very well. Let’s break down the src folder which is where most of the magic lies:

  • actions: 1 file with action constants and creators, per resource.
  • bootstrap: General dapp bootstrap logic and routing set up.
  • components: Presentational components used throughout the dapp.
  • constants: Constants and enums.
  • containers: 1 connected component per route with an optional child components folder that houses container specific presentational components.
  • reducers: 1 file with a reducer and selectors, per resource.
  • sagas: 1 file with a root and child sagas, per resource.
  • styles: Global dapp styles and variables like colors and typography.
  • utils: Global dapp utilities.

We also follow a myriad of Javascript and React best practices and patterns, but since those are not dapp specific, we’ll leave them for another post and focus on the patterns and components we developed around Web3.

The Dapp API

Dapps need an Ethereum Web3 provider to function. This can come in many forms. It can be a local web server, an HTTP, Websocket, or IPC URL, or a global object provided by a Web3 enabled browser like Chrome + Metamask, Mist, or Parity.

In a test environment you’ll probably want to use a local web server, while in production, you’ll want to use the browser’s global provider with a fallback URL. We do all of this in one file that we call dapp-api.js. Additionally, we export the current network and some useful Ethereum specific RegExps that always come in handy. This is also a good place to set up contract factories like we did in our dapp.

/src/bootstrap/dapp-api.js

Your file will probably look a bit different depending on your specific needs, but this is a great starting point for centralizing (in a good way ;) your Web3 set up.

The Initializer

Sometimes, people will access your dapp without a Web3 enabled browser, and your fallback URL might be unavailable or you might not have one. In these cases, you’ll want to cancel the rendering of your dapp or at least disable the Ethereum dependent features, and show a message to the user explaining what the problem is. This logic can be abstracted away into a wrapper component that we called Initializer. This is also a good place for enforcing checksummed addresses in URL paths, a la Etherscan.

/src/bootstrap/app.js

The process for checking if a provider is available is simple. First you check if Web3 from dapp-api.js initialized properly by fetching accounts, if it throws an error, you can catch it and render what you think is appropriate for your users. If it returns 0 accounts, the wallet (e.g. Metamask) either has 0 accounts or is locked, and you should explain that to your users too. Otherwise, if it returned a valid account, all is fine and you can continue to render the child components.

Enforcing checksummed addresses is a bit more complex, but the following code should cover most use cases.

/src/bootstrap/initializer.js

Sending Transactions

Depending on which Web3 library you are using, the resolve value of a function that sends a transaction might be the transaction hash and not the receipt or return value. This means that your code will finish executing before the transaction is actually mined and added to the chain. We came up with a simple solution for this, in the form of a utility saga.

/src/utils/saga.js

It basically waits for the receipt to be available. You could also implement it using a filter if your provider and library support it. This is also a good place to implement common error/success handling, like we did with the toast message and updating the ETH balance.

Displaying Addresses

Ethereum addresses are long, 42 characters to be exact. When you don’t want to show an entire address, showing the first and last 4 characters has become somewhat of a standard. We made a component that optionally does this and shows the full address in a tooltip. We also added a network specific Etherscan link for the user’s convenience.

/src/components/chain-hash/index.js

Feel free to extend this to suit your needs.

Integration and E2E Testing

It’s usually more than sufficient to test presentational components with something like storyshots, but container components require a bit more effort. We prioritize integration tests over unit tests because we believe they give you a lot more bang for your buck in terms of real life bug surface area coverage.

However, conducting full blown React integrations tests that rely on Ethereum is not as straightforward as one might initially think. We abstracted away the set up into a bootstrap file called set-up-integration-test.js. Let’s go over the functions that make it up now.

Set Up

The first step is deploying your contracts to your local test net. We already took care of setting the test net up in “The Dapp API”. Whenever process.env.NODE_ENV === ‘test’, which it should if you are running tests, ganache will start up and become the provider to your Web3 instance. This function will be very different based on what contracts you are testing, but the general idea is to deploy and set up your contracts and then return the instances for use in tests.

/src/bootstrap/set-up-integration-test.js

Then comes the part where you initialize the application and create a function that mounts your entire app in preparation for an integration test. This function takes care of setting up your redux store with a dispatch spy, creating the browser history object, optionally injecting a test element, and calling the contract set up function from before.

/src/bootstrap/set-up-integration-test.js

It should return everything you need to run a test and can be conveniently called from a test file’s beforeEach block.

Necessary Utilities

There are two more functions that you’ll need to run your tests. You need a way to wait for all asynchronous side effects to finish executing after mounting or triggering an event like a mouse click. You also need a way to ignore the browser history API’s random route keys, if you plan to go through route transitions in your tests and use snapshot testing. Here are two functions that do this.

/src/bootstrap/set-up-integration-test.js

enzymeToJSON is a good place to normalize any other sort of entropy in your dapp that can stop you from successfully implementing snapshot tests.

Tying It All Together

This is how it looks like in practice.

/src/containers/home/home.test.js

This test mounts the home page, enters the deployed contract’s address into the search input field, submits it, and verifies it loaded the contract data correctly.

Closing Notes

A lot of these ideas and patterns will eventually be built into our chainstrap framework so that anyone can easily take advantage of them. Dapp development is still in a very early stage and it will be interesting to see what other patterns we and the community come up with. Comment what you think about this below and let us know if you have any suggestions or ideas!

Learn More

Join the community chat on Telegram.

Visit our website.

Follow us on Twitter.

Join our Slack for developer conversations.

Contribute on Github.