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
componentsfolder that houses
containerspecific 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.
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.
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.
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.
The process for checking if a provider is available is simple. First you check if
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.
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.
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.
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.
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.
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.
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.
It should return everything you need to run a test and can be conveniently called from a test file’s
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.
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.
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.
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!
Join the community chat on Telegram.
Visit our website.
Follow us on Twitter.
Join our Slack for developer conversations.
Contribute on Github.