Support for browser based wallets with truffle-box

UPDATE: In July 2017, the truffle team at ConsenSys released a set of official boxes http://truffleframework.com/boxes/, the react-auth and react-uport boxes support redux!

A reducer to pass web3 to components as a prop from the Redux store. repo can be found here, truffle-box-browser-wallet.

Truffle-box has recently published a set of truffle-webpack-react boilerplates that make it easy to create a smart contract application with a React user interface.

The boiler plates are fantastic and I am particularly excited about truffle-box-uport.

Engaging with smart contracts through your smart phone is going to be a reality soon.

Authorising transactions with your finger print from your phone is so future.

While exploring these boilerplates, I noticed that the react components weren’t aware of my Metamask browser based wallet. The boilerplates ship with setting the provider to the truffle contracts to `localhost:8545`.

Adding a few lines of java script to the truffle-box will make components aware of the Metamask browser based wallet.

import Web3 from 'web3'
import truffleConfig from './../../truffle-config.js'
var web3Location = `http://${truffleConfig.networks.development.host}:${truffleConfig.networks.development.port}`
var web3Provided;
if (typeof web3 !== 'undefined') {
// Use the browser based wallet provider.
// DEVELOPER NOTE: removing the next commented line will break the app
// eslint-disable-next-line
web3Provided = new Web3(web3.currentProvider)
} else {
// DEVELOPER NOTE: What happens if the
// user does not have a browser based wallet? What happens
// if the Web3 object cannot be initialised with the httpProvider
// given from the location in the truffle-config file?
web3Provided = new Web3(new Web3.providers.HttpProvider(web3Location))
}

This implementation works well with the truffle-box-react boilerplate and web3Provided can be passed in as a prop to the App component. This implementation becomes cumbersome when the project manages state using Redux because the developer would have to insert this script in every component that wants to use the MetamaskInpageProvider for the smart contract instance.

Pass the web3 object as a prop to the component through the Redux store

For folks that are new to Redux, the egghead video series are excellent.

If we want a friendly way to make all of our components aware of the same web3 object we need to get web3 from the Redux store. In order for us to do that we’ll need a web3 reducer that will handle actions appropriately.

web3Reducer.js →reducer

import Web3 from 'web3'
const provider = new Web3.providers.HttpProvider('http://localhost:8545')
const web3 = new Web3(provider)
const initialState = {
web3: web3
}
const web3Reducer = (state = initialState, action) => {
if (action.type === "WEB_3_INITIALIZE")
{
return Object.assign({}, state, {
web3: action.payload
})
}
return state
}
export default web3Reducer

web3Init.js →presentation component

import React, { Component } from 'react'
class Web3Init extends Component {
componentDidMount(){
this.props.onWeb3ComponentLoad()
}
render() {
return(
<li className="pure-menu-item">
</li>
)
}
}
export default Web3Init

Web3InitContainer.js →container component

import { connect } from 'react-redux'
import Web3Init from './Web3Init'
import { web3Initialize } from './Web3InitActions'
const mapStateToProps = (state, ownProps) => {
return {
web3:state.web3
}
}
const mapDispatchToProps = (dispatch) => {
return {
onWeb3ComponentLoad: (web3) => {
dispatch(web3Initialize(web3))
}
}
}
const Web3InitContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Web3Init)
export default Web3InitContainer

Web3InitActions.js →actions

import Web3 from 'web3'
import truffleConfig from './../../truffle-config.js'
var web3Location = `http://${truffleConfig.networks.development.host}:${truffleConfig.networks.development.port}`
export const WEB_3_INITIALIZE = 'WEB_3_INITIALIZE'
function web3Init(web3) {
return {
type:WEB_3_INITIALIZE,
payload:web3
}
}
export function web3Initialize() {
return function(dispatch) {
var web3Provided;
if (typeof web3 !== 'undefined') {
// Use the browser based wallet provider.
// DEVELOPER NOTE: removing the next commented line will break the app
// eslint-disable-next-line
web3Provided = new Web3(web3.currentProvider)
} else {
// DEVELOPER NOTE: What happens if the
// user does not have a browser based wallet? What happens
// if the Web3 object cannot be initialised with the httpProvider
// given from the location in the truffle-config file?
web3Provided = new Web3(new Web3.providers.HttpProvider(web3Location))
}
dispatch(web3Init(web3Provided))
}
}

Now that we have a reducer for web3, when you want to pass in web3 as a prop to a new component:

  1. Define web3 in your mapStateToProps function in your container component.
const mapStateToProps = (state, ownProps) => {
return {
web3: state.web3
}
}

2. In your mapDispatchToProps of your container component, pass web3 into the function that you pass as a prop to your presentation component

const mapDispatchToProps = (dispatch) => {
return {
onSignUpFormSubmit: (name, web3) => {
event.preventDefault();
    dispatch(signUpUser(name, web3))
}
}
}

3. In the presentation component, add web3 to the state within the constructor function.

4. Finally in your actions file use the web3 to set the provider

const authentication = contract(AuthenticationContract)
authentication.setProvider(web3.currentProvider)

Some things that I noticed after creating the reducer:

web3 does not dispatch to the store immediately after the window loads, it has to wait until the web3Init presentation component loads.

This error came up when Metamask is locked and I have not migrated the contracts to the blockchain. I was not able to replicate this again.

Uncaught Error: Invalid JSON RPC response: {“id”:1,”jsonrpc”:”2.0

This warning comes up

warning.js:36 Warning: setState(…): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the Connect(HiddenOnlyAuth(Component)) component.

I believe this is coming up now because web3 is being passed into the unmounted components that are toggled when the user signed up for the first time.

There is some buggy behavior:
When updating the user name and rejecting the transaction through metamask the ui updates the username, to the user it looks like their transaction went through. If you logout and log back in, you can see that no change was made to the contract.

The first time that the user signs up and rejects the transaction has app has the appropriate behavior, if the user has signed up already it allows them to sign up again.

After the first time the user signed up, if they try to sign up again and reject the transaction it looks like they’ve signed up again. The user name is the original name from the first signup.

This is the first reducer that I’ve created, so I’m very open to edits, feedback and suggestions. :D Thanks!