Developer Update #1: Gnosis Olympia

Adolfo Panizo
GnosisDAO
Published in
8 min readDec 4, 2017

--

At Gnosis, we want to make it as easy as possible for all users to participate in decentralized prediction markets. To get valuable product feedback, we think it’s essential to involve the entire community in testing our application. That’s why we came up with the idea to organize the prediction-market tournament Olympia, encouraging users to trade in prediction markets and win GNO tokens.

Management Interface Fork

We also took advantage of this opportunity to integrate new features and take several architecture decisions. The first decision we made was to fork our core product, the Gnosis Management Interface and establish a mechanism for back-merging changes, fixes, and features we had implemented in both projects—our Management Interface and Olympia.

We started by reviewing our current software development process and established a clear, structured process to be followed across all Gnosis products, starting with Olympia: Product Managers and Product Owners had to be in constant communication to check, test, and assure that shared tickets are implemented in both the Olympia and the Management interface; back-end engineers had to configure servers according to the project’s network necessities; and front-end developers had to find a way to separate code used for Olympia from the one used in the Management Interface, making it easier to merge processes.

uPort Integration

We also decided to integrate uPort as a wallet provider for managing all transactions when trading in prediction markets, including our play-money token OLY.

Uport will be used in two ways:

1) To log into the interface by scanning a QR code
2) To approve every single transaction by responding to a push notification or by scanning a QR code

During our integration process with uPort, we’ve faced a few challenges:

1. Code Integration of a new Wallet Provider

When integrating uPort, the most difficult thing was to correctly isolate uPort errors from those directly related with our app, test them on a test network (we use the ConsenSys Rinkeby network), and report them to the uPort team.

Coding-wise, we have implemented a Publisher/Subscriber system where each respective Provider can publish their changes to the parent integration class, reducing overall load and serving as a solid framework for future provider integrations.

Providers update interval


class Uport extends BaseIntegration {
constructor() { super() this.watcher = setInterval(() => { this.watch(‘balance’, this.getBalance) this.watch(‘network’, this.getNetwork) }, Uport.watcherInterval)}

Another improvement is the initialization of providers. We have decoupled initialization from a DOM Higher Order Component (HOC) and isolated it as an external middleware service. What this translates to is improved app execution time and higher failure tolerance as we are assured providers are initialized once.

2. Initialization & Session Management

In uPort’s case, we can speak about two initialization processes: the uPort-GnosisJS initialization for simpler and more robust transactions on the app, and the uPort-User initialization for implementing a proper session-management solution.

Initialization of Providers as a middleware

export default store => next => (action) => {  ...  if (type === ‘INIT_PROVIDERS’) {
const providerOptions = {
...
}
Promise.all(map(walletIntegrations,
integration => integration.initialize(providerOptions)))
}
return handledAction
}

Init 1 of 2 [uPort — Gnosisjs registration and initialization]

async initialize(opts) {  super.initialize(opts)  this.runProviderRegister(this, {
priority: Uport.providerPriority,
account: opts.uportDefaultAccount,
})
this.uport = await initUportConnector(Uport.USE_NOTIFICATIONS) this.web3 = await this.uport.getWeb3()
this.provider = await this.uport.getProvider()
this.network = await this.getNetwork()
this.networkId = await this.getNetworkId()
this.account =
opts.uportDefaultAccount || (await this.getAccount())
return this.runProviderUpdate(this, {
available: true,
network: this.network,
networkId: this.networkId,
account: this.account,
})
.then(async () => {
if (!this.account) {
return
}
await opts.initGnosis()
opts.dispatch(fetchOlympiaUserData(this.account))
})
}

Step 2 of 2 [uPort — User, necessary for session management]

In the previous code snippet, I marked in bold:

 this.uport = await initUportConnector(Uport.USE_NOTIFICATIONS)

Note that initUportConnector behaves differently based on uPort’s mode (that is, approving a transaction via QR code or via a push notification). For dealing with the connection itself, we have created a helper class called connector.js. It is within this class that you’ll find the initUportConnector method that was used previously.

// src/integrations/uport/connector.jsconst initUportConnector = async (useNotifications) => {  const provider = useNotifications
? await import(‘./uportNotifications’)
: await import(‘./uportQr’)
return provider.default(
uport,
requestCredentials,
getCredentialsFromLocalStorage)
}

These methods work as a kind of “Factory Pattern” where we load the provider based on the type.

As you can probably imagine, the default method of both providers should contain the logic for knowing if the stored credential is valid or not. If it is not valid, we ask the user to log into the application.

// src/integrations/uport/uportNotifications.jsconst init = async (uport, requestCredentials, getCredential) => {  let credential = getCredential()  if (!isValid(credential)) {
credential = await requestCredentials()
}
if (credential) {
assignSessionProps(uport, credential)
}
return uport}export default init
// src/integrations/uport/connector.jsconst requestCredentials = notifications => async () => {  try {
modifyUportLoginModal(uport.firstReq)
const cred = await uport.requestCredentials({ notifications })
localStorage.setItem(UPORT_OLYMPIA_KEY, JSON.stringify(cred))
return cred
} catch (err) {
localStorage.removeItem(UPORT_OLYMPIA_KEY)

return null
}
}

3) Switch Mode

After a couple of internal tests, we identified that if we used the app with PushNotifications, sometimes notifications were delayed or un-ordered as a result of network problems — causing failing transactions and general confusion.

On the other hand, scanning a QR code to approve every transaction could turn out to be tough if there is heavy usage of the app. That's why we had to find a way to integrate both modes and switch between them at ludicrous speed.

We also had to implement it without breaking the previous code, hence the requirements were:

  • Keep the general code for a wallet provider (app — uPort — gnosisjs) in one module (uport/index.js)
  • Keep the connection code between the app — uPort — user/storage in another module (uport/connector.js)
  • Keep the specifics of each mode in different modules (uport/uportNotifications.js and uport/uportQr.js), respectively

To summarize, the above architecture style allowed us to decouple the three “controller-domains”, usability, and specific technical aspects. The result is a more robust and less error prone code.

Finally, the entry point for switching modes is simply a boolean variable inside uport/index.js.

static USE_NOTIFICATIONS = false

Olympia and GnosisDB

Olympia relies on a middle tier between the blockchain network and the app as a data feed. We had to make some important improvements such as a patch for solving the blockchain’s reorganization issues in order for it to work with Rinkeby.

The major challenge on that part was to inject and use the big amounts of data received from the user ranking in the application.

Olympia ranking

We’ve spent a lot of time and energy to not update the DOM’s node more than absolutely necessary.

  • We did not want to abuse React’s PureComponents because the properties and states to compare are numerous
  • We had to find a way to assure the redux’ state was not mutated
  • We had to find a way to compare and inject the components’ properties super fast
  • As a bonus, we wanted to take this opportunity to implement the new user interfaces following a semantic process and taking advantage of all the great features React has to offer

Reselect, Immutable, and Layout Components

… came to rescue.

With immutable components we assure that all records for the scoreboard table won’t mutate, so whichever calculations and transactions we do, the ranking position will be secure on the redux’ store.

First, we created the User entity as an immutable Record

import { Record } from ‘immutable’const UserRecord = Record({
currentRank: undefined,
diffRank: undefined,
pastRank: undefined,
account: undefined,
score: undefined,
balance: undefined,
predictedProfit: undefined,
predictions: undefined,
}, ‘User’)
export default UserRecord

Then, we created the fetchOlympiaUserData function

export default account => dispatch =>  restFetch(url + account)
.then((response) => {
...
dispatch(addUsers([response]))
})

Adding users to the store using the redux-actions’ createAction function

import { createAction } from ‘redux-actions’export const ADD_USERS = ‘ADD_USERS’export default createAction(ADD_USERS)

And finally pass the date to the store in the reducer

export default handleActions({
[ADD_USERS]: (state, { payload }) =>
state.withMutations((map) => {
payload.forEach(user =>
map.set(user.account, new UserRecord(user)))
}),
}, Map())

So at this point, we have the ranking of users stored as immutable records in the store. The next step is to assure we pass only updates and real changes to the component.

Fetching users from store reselect as a component selector

import { createSelector } from 'reselect'export const olympiaUsersListSelector = createSelector(
olympiaUsersSelectorAsList,
users => users
? users
.filter(user => user.currentRank > 0)
.sort(...) // we order the users by ranking
.take(100)
: undefined,
)

Finally, I’d like to explain what “semantic layout” means. I personally think there are three types of components in React: Stateful Components, Stateless Components, and Layout Components.

The first two are well known—the important difference are in the third type.

Layout Components are general ones directly coupled to the user-interface where they can be used in all applications you build with React. Most of them are just a HOC of HTML tags enhanced with properties, others are just an export of a certain library (imagine a Tooltip for example).

The idea is to give the necessary tools to Stateless components for describing how the view should look like in a semantic way, customizing it with React’s properties rather than CSS.

Also, Layout Components are great for introducing the company’s style guidelines and therefore are much appreciated by designers and UX experts.

If you would like to change the primary or secondary button color, or the titles’, and subtitles’ size for example, you just need to do it within those components and the entire application will be updated automatically.

Another benefit of using small Layout Components is the option to create them based off of React’s PureComponent taking advantage of its optimizations.

Example of a Layout Component

class Block extends PureComponent {  get blockStyle() {
return {
width: this.props.width,
}
}
render() {
const { margin, center, children, className } = this.props

return (
<div
className={cx(margin, className, { center })}
style={this.blockStyle}
>
{ children }
</div>
)
}
}
Block.propTypes = {
width: PropTypes.string,
margin: PropTypes.string,
center: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
}
export default Block

And how a UI specification looks like following the semantic description

class ScoreBoard extends React.PureComponent {

render() {
const { data, myAccount } = this.props
const hasRows = data && data.size > 1
return (
<Block center>
<PageFrame>
<Block className={cx(‘trophy’)}>
<Img src={trophy} width=”100" />
<Paragraph>Scoreboard</Paragraph>
</Block>
<Paragraph className={cx(‘explanation’)}>
The total score...
</Paragraph>
{ hasRows
? <Table data={data} myAccount={myAccount} />
: <NoRows />
}
<Block margin=”xl” />
</PageFrame>
</Block>
)
}
}

We’ll speak more about this topic in another React-focused post, covering how we have integrated the storybook for testing our components.

Conclusion

We’ve implemented unit and integration tests on all levels: back-end, blockchain, and front-end, hoping that all users will have an excellent experience when they use Olympia. Having said that, we’re working hard on continuously making it faster, better, and more secure.

Stay tuned for Olympia’s official release date! Any feedback and reports of bugs will be very much appreciated — shoot me an email at adolfo@gnosis.pm.

Me at my home office in Ponferrada, Spain.

--

--