Skip to content

An example for developers showing an application built on EOSIO combining UAL, Manifest Spec, and Ricardian Contracts

License

Notifications You must be signed in to change notification settings

EOSIO/tropical-example-web-app

Repository files navigation

🌴 Tropical Example

Tropical Example is a mock application for renting properties. It will be referenced throughout this guide as an example for application developers to start building secure applications with a good user experience on the EOSIO blockchain.

EOSIO Labs

About EOSIO Labs

EOSIO Labs repositories are experimental. Developers in the community are encouraged to use EOSIO Labs repositories as the basis for code and concepts to incorporate into their applications. Community members are also welcome to contribute and further develop these repositories. Since these repositories are not supported by Block.one, we may not provide responses to issue reports, pull requests, updates to functionality, or other requests from the community, and we encourage the community to take responsibility for these.

Overview

Try it out in Gitpod

Gitpod launches the app for you. It starts the required blockchain in the background, launches the web server, and opens a preview window. NOTES:

  1. There are several times during startup it might look like startup hangs, namely... near the end of the docker build, once the IDE comes up, and then once the preview shows.
  2. Sometimes when Gitpod launches the webapp preview, it does so prematurely. Just click the small refresh circular arrow icon in the top left of the preview window.
  3. Gitpod generates a dynamic URL for the browser to access the app from. This URL is needed in numerous parts of the app, so note that there is code in this repo specifically meant for Gitpod compatibility. A comment has been added in those locations to point it out.
  4. To use Scatter in Gitpod, launch the demo in Gitpod, then go into Scatter's Networks section and add a custom network with the following settings as well as adding the account name. Note that these settings will need to be updated each time you launch the demo in Gitpod (because the URL will be different each time).
    • Network Settings
      • Host: <the hostname Gitpod generated to view the web page; it's what's in the address bar of the browser it opened for you, except without the "https://" or a final "/">
      • Protocol: https
      • Port: 443
      • Chain ID: cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f (This is the test chainId used in the example) Customer Network Configuation
    • Adding account name
      • After you've imported the private key from this example (see other parts of the README for those instructions), Scatter might not pull the "example" account from the network. If that's the case, in the Wallet section, if "example" doesn't show up under your imported key with a balance to the right, on a Mac, you'll hold Ctrl when you click the three horizontal dot button to the right of your imported key. Ctrl will enable a normally-hidden item called "Link Account". Click that and in the first box, type "example" and in the drop-down, select the custom network you created above. See screenshot below for what this looks like. Link Account Read more about Gitpod workspaces here

The following open source repositories are utilized in Tropical Example:

Table of Contents

Universal Authenticator Library (UAL)

An Authenticator provides the ability to communicate with an authentication provider. Authentication providers generally allow users to add, modify, and delete private / public key pairs and use these key pairs to sign proposed transactions.

UAL allows developers to support multiple Authenticators with only a few lines of code. This significantly reduces the start up time of creating applications by removing the need to detect and create an interface for interacting with the Authenticators.

UAL provides users with a common login interface in which they can select the Authenticator of their choice to interact with the EOSIO protocol. Once a user selects the Authenticator of their choice and logs in, the application developer will have access to an activeUser object which contains all necessary fields and functions to sign transactions and customize their user experience.

Installation

First install your UAL Renderer of choice. Tropical Example uses the UAL Renderer for ReactJS and the rest of the examples will be demonstrating usage with the React Renderer. Please view the UAL documentation for links to all available renderers with documentation and examples of their usage.

yarn add ual-reactjs-renderer

Then install the Authenticators you want to allow users to interact with. Tropical Example uses the following Authenticators:

yarn add ual-eosio-reference-authenticator
yarn add ual-scatter
yarn add ual-lynx
yarn add ual-token-pocket

Setup

In your root React component (for most projects this will be index.js) wrap your App component with UALProvider.

The UALProvider requires an array of Chains you wish your app to transact on, an array of Authenticators you want to allow users to interact with, and an application name. Each Authenticator requires at least an array of Chains that you want to allow the Authenticator to interact with and a second options parameter that may be required. Please see the documentation of the Authenticator if this options argument is required and what fields are necessary.

// UAL Required Imports
import { UALProvider } from 'ual-reactjs-renderer'
// Authenticator Imports
import { EOSIOAuth } from 'ual-eosio-reference-authenticator'
import { Scatter } from 'ual-scatter'
import { Lynx } from 'ual-lynx'
import { TokenPocket } from 'ual-token-pocket'
...
const appName = 'Tropical-Example'

// Chains
const chain = {
  chainId: process.env.REACT_APP_CHAIN_ID,
  rpcEndpoints: [
    {
      protocol: process.env.REACT_APP_RPC_PROTOCOL,
      host: process.env.REACT_APP_RPC_HOST,
      port: process.env.REACT_APP_RPC_PORT,
    },
  ],
}

// Authenticators
const eosioAuth = new EOSIOAuth([chain], { appName, protocol: 'eosio' })
const scatter = new Scatter([chain], { appName })
const lynx = new Lynx([chain])
const tokenPocket = new TokenPocket([chain])

const supportedChains = [chain]
const supportedAuthenticators = [eosioAuth, scatter, lynx, tokenPocket]

ReactDOM.render(
  <UALProvider chains={supportedChains} authenticators={supportedAuthenticators} appName={appName}>
    <App />
  </UALProvider>,
  document.getElementById('root'),
)

UAL Context

The UAL Renderer for ReactJS uses Context to expose the objects and functions needed to interact with UAL. The context is created by the UALProvider. There are two methods to gain access to this context:

  1. The withUAL HOC (Higher Order Component) can be used to pass the UALProvider context as props to the wrapped component.
  • When using the withUAL HOC all of the UALProvider context will be available under the parent prop ual
  import { withUAL } from 'ual-reactjs-renderer'
  class Example extends React.Component {
    render() {
      const { ual: { logout } } = this.props
      return <div onClick={logout}>Logout</div>
    }
  }

  export default withUAL(Example)
  1. The static contextType property can be set on a class to the renderer's exported context object, UALContext. This allows the context to be referenced using this.context within the class.
  • Using the static contextType to access the context is currently only supported by React component classes and not supported by functional components. For functional components, withUAL must be used if access to the context is required.
  import { UALContext } from 'ual-reactjs-renderer'
  class Example extends React.Component {
    static contextType = UALContext

    render() {
      const { logout } = this.context
      return <div onClick={logout}>Logout</div>
    }
  }

Login

Modal

By default, the UALProvider provides a modal at the root level of your application. This modal will render the login buttons of all the configured Authenticators that can be detected in the user’s environment. The modal is hidden by default, but can be displayed and dismissed by calling the functions showModal and hideModal, respectively. Both functions are set in the UALProvider context.

import { withUAL } from 'ual-reactjs-renderer'
class App extends React.Component {
  ...
  displayLoginModal = (display) => {
    const { ual: { showModal, hideModal } } = this.props
    if (display) {
      showModal()
    } else {
      hideModal()
    }
  }
  ...
}

export default withUAL(App)

Account Name

After logging in, an activeUser object is returned by the Authenticator and set in the UALProvider context.

On the activeUser object a getAccountName method is available. This method returns a promise, which will resolve to a string containing the signed in account name.

import { UALContext } from 'ual-reactjs-renderer'
class UserInfo extends React.Component {
  static contextType = UALContext
  ...
  async componentDidMount() {
    const { activeUser } = this.context
    if (activeUser) {
      const accountName = await activeUser.getAccountName()
      this.setState({ accountName })
    }
  }
  ...
}

Transactions

In order to propose transactions, your application needs access to the activeUser object returned by the logged in Authenticator.

At the time of signing, call activeUser.signTransaction with a valid transaction object and a configuration object. This will propose the transaction to the logged in Authenticator.

It is highly recommended in the transaction configuration to provide a expireSeconds property of a time greater than at least 300 seconds or 5 minutes. This will allow sufficient time for users to review and accept their transactions before expiration.

import { UALContext } from 'ual-reactjs-renderer'
import { generateLikeTransaction } from 'utils/transaction'
...
class Property extends React.Component {
  static contextType = UALContext
  ...
  onLike = async () => {
    const { login, displayError } = this.props
    // Via static contextType = UALContext, access to the activeUser object on this.context is now available
    const { activeUser } = this.context
    if (activeUser) {
      try {
        const accountName = await activeUser.getAccountName()
        const transaction = generateLikeTransaction(accountName)
        // The activeUser.signTransaction will propose the passed in transaction to the logged in Authenticator
        await activeUser.signTransaction(transaction, { broadcast: true, expireSeconds: 300 })
        this.setState({ liked: true })
      } catch (err) {
        displayError(err)
      }
    } else {
      login()
    }
  }
  ...
}

export default Property

The method activeUser.signTransaction returns a promise, which, if signing is successful, will resolve to the signed transaction response.

Logout

If you want to logout, you can use the logout function set in the UALProvider context.

import { UALContext } from 'ual-reactjs-renderer'
class UserInfo extends React.Component {
  static contextType = UALContext
  ...
  renderDropdown = () => {
    const { logout } = this.context
    return (
      <div className='user-info-dropdown-content'>
        <UserDropdown logout={logout} />
      </div>
    )
  }
  ...
}

export default UserInfo

Errors

Errors thrown by UAL are of type UALError, which extends the generic Error class, but has extra information that is useful for debugging purposes.

Login Errors

During login, errors are set in the UALProvider context.

// Using withUAL() HOC
this.props.ual.error
// Using static contextType
this.context.error

Transactions Errors

During signing, errors will be thrown by activeUser.signTransaction. It is recommended to use the try...catch statement to capture these thrown errors.

try {
  await activeUser.signTransaction(transaction, transactionConfig)
} catch (error) {
  // Using JSON.parse(JSON.stringify(error)) creates a copy of the error object to ensure
  // that you are printing the value of object at the moment you log it
  console.error('UAL Error', JSON.parse(JSON.stringify(error)))
}

If you need information not covered in this guide, you can reference the full UAL repository here.

Manifest Specification

Tropical Example follows the Manifest Specification by providing the following:

If you need information not covered in this guide, you can reference the Manifest Specification here.

Ricardian Specification

Tropical Example follows the Ricardian Specification by providing the following:

  • A tropical.contracts.md, which defines the Ricardian Contract of the like action of the tropical contract.
  • Generating the tropical abi file with eosio-cpp by passing the -abigen flag, which will auto generate an abi including the tropical.contracts.md into the ricardian_contract field of the like action.

If you need information not covered in this guide, you can reference the Ricardian Specification here.

WebAuthn

Tropical Example implements WebAuthn as a 2nd factor.

After logging in, under the user menu, you'll find an option to "enroll" a 2FA device. Use this option in conjunction with your device's build-in biometric scanner, secure element, or external hardware key to enroll a key with the Tropical Example.

Then, on the Properties Search Results page, you'll see a 'Rent' button. Where liking something is a relatively low-risk activity, the Rent button represents a real-world use case for commiting yourself to rent that property. In this case where money is on the line, the app will request you sign for the Rent action with the enrolled key.

Read more about this example and technology here -- REQUIRE LINK to blog or Release Notes of some kind

Running Tropical Example

Required Tools

  • Yarn with support at ^1.15.2 (latest stable).
  • Docker with support at Docker Engine 18.09.2 (latest stable).
  • Docker Compose.
  • Node.js with support at ^10.15.3 LTS. NOTICE This project will not build on the current version of Node.js 12.3.1 due to an error in a sub-dependency of react-scripts.

This project was bootstrapped with Create React App.

Setup

Create a .env file from the default.env

cp default.env .env

Tropical Example uses an environment configuration for the Chain and RPC endpoints. By default it will query the local node setup by Docker Compose in this repo. If you want to use another Chain, update the values in the .env file you created in the first step to set the preferred Chain you wish your app to transact on.

.env file defaults

REACT_APP_CHAIN_ID=cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f
REACT_APP_RPC_PROTOCOL=http
REACT_APP_RPC_HOST=localhost
REACT_APP_RPC_PORT=8888

Installation

yarn

Run this first to install all the project's dependencies.

Running Nodeos

Before the app can be run, the Tropical Example contract must be deployed on the chain configured in the .env to the account tropical.

This repo provides a docker-compose.yml that will setup and deploy the tropical contract using Docker Compose.

Then run the following to start up a local node:

yarn up

You can view the contract in the eosio/contracts directory.

Running Frontend

yarn start

This command runs the app in development mode over SSL. You will need to install a self-signed SSL certificate or enable allow-insecure-localhost if running over SSL in chrome. Open https://localhost:3000 to view it in the browser.

The page will reload if you make edits.

Login

The Docker Compose setup scripts provide an, example account, that can be imported into your Authenticator of choice to login and sign transactions:

⚠️ Never use this development key for a production account! Doing so will most certainly result in the loss of access to your account, this private key is publicly known. ⚠️

# Example Account Public Key
EOS6TWM95TUqpgcjYnvXSK5kBsi6LryWRxmcBaULVTvf5zxkaMYWf
# Example Account Private Key
5KkXYBUb7oXrq9cvEYT3HXsoHvaC2957VKVftVRuCy7Z7LyUcQB

Using WebAuthn

After setting up the application and logging in, you can enable WebAuthn if you want to be able to rent a property. Enabling WebAuthn

Once you enable WebAuthn with your choice of hardware, you can browse to the list of properties and select rent. Scatter will prompt you to allow this action by authenticating with your hardware. Renting A Property

After confirming the transaction, you should now see an indicator that your property has been rented successfully. Rented Property

Other Available Actions

You can like a property (WebAuthn not required). After browsing to the list of properties and selecting like, scatter will prompt you to allow this action. Liking A Property

After confirming the transaction, you should now see an indicator that your property has been liked successfully. Liked Property

Docker Compose Command Reference

# Create and start the docker container
docker-compose up eosio
# Stop the docker container
docker-compose down eosio
# Open a bash terminal into the docker container
docker-compose exec eosio /bin/bash

Links

Contributing

Check out the Contributing guide and please adhere to the Code of Conduct

License

MIT licensed

Important

See LICENSE for copyright and license terms.

All repositories and other materials are provided subject to the terms of this IMPORTANT notice and you must familiarize yourself with its terms. The notice contains important information, limitations and restrictions relating to our software, publications, trademarks, third-party resources, and forward-looking statements. By accessing any of our repositories and other materials, you accept and agree to the terms of the notice.