Awesome
🌴 Tropical Example <!-- omit in toc -->
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.
About EOSIO Labs <!-- omit in toc -->
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 <!-- omit in toc -->
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:
- 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.
- 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.
- 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.
- 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)
- 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. Read more about Gitpod workspaces here
- Network Settings
The following open source repositories are utilized in Tropical Example:
- Using the Universal Authenticator Library (UAL) for quick and easy integration with multiple authentication providers (wallets).
- Increasing the security and transparency of your application by following the Manifest Specification.
- Displaying human readable Ricardian Contracts of your proposed EOSIO actions by following the Ricardian Specification.
Table of Contents <!-- omit in toc -->
- Universal Authenticator Library (UAL)
- Manifest Specification
- Ricardian Specification
- WebAuthn
- Running Tropical Example
- Links
- Contributing
- License
- Important
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:
- The
withUAL
HOC (Higher Order Component) can be used to pass theUALProvider
context as props to the wrapped component.
- When using the
withUAL
HOC all of theUALProvider
context will be available under the parent propual
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)
- 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 usingthis.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:
- A publicly accessible app-metadata.json.
- A publicly accessible chain-manifests.json
- Registering the app's Manifest on the local chain via cleos
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 thetropical
contract. - Generating the
tropical
abi file with eosio-cpp by passing the-abigen
flag, which will auto generate an abi including thetropical.contracts.md
into thericardian_contract
field of thelike
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.
- Mac and Windows environments - By default the Docker Compose tool is installed with Docker.
- Linux - Follow these instructions to install Docker Compose.
- Node.js with support at
^10.15.3
LTS. NOTICE This project will not build on the current version of Node.js12.3.1
due to an error in a sub-dependency ofreact-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.
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.
After confirming the transaction, you should now see an indicator that your property has been rented successfully.
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.
After confirming the transaction, you should now see an indicator that your property has been liked successfully.
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
- Universal Authenticator Library (UAL)
- Manifest Specification
- Ricardian Specification
- Docker Compose CLI Reference
Contributing
Check out the Contributing guide and please adhere to the Code of Conduct
License
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.