A Simple DApp¶
Note
This tutorial requires basic Javascript programming skills and basic understanding of React Framework.
Note
You can find the finished React project source code here. You can test it live here.
Let’s implement a simple decentralized application using SimpleEscrowWithERC1497
contract.
We will create the simplest possible UI, as front-end development is out of the scope of this tutorial.
Tools used in this tutorial:
- Yarn
- React
- Create React App
- Bootstrap
- IPFS
- MetaMask
Arbitrable Side¶
Scaffolding The Project And Installing Dependencies¶
- Run
yarn create react-app a-simple-dapp
to create a directory “a-simple-dapp” under your working directory and scaffold your application. - Run
yarn add web3@1.0.0-beta.37 react-bootstrap
to install required dependencies. Using exact versions for web3 and ipfs-http-client is recommended. - Add the following Bootstrap styleshet in
index.html
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
- Inside the application directory, running
yarn start
should run your application now. By default it runs on port 3000.
Ethereum Interface¶
Under the src
directory, let’s create a directory called ethereum
for Ethereum-related files.
Setting Up Web3¶
Let’s create a new file called web3.js
under ethereum
directory. We will put a helper inside it which will let us access MetaMask for sending transactions and querying the blockchain. For more details please see the MetaMask documentation .
import Web3 from 'web3'
let web3
window.addEventListener('load', async () => {
// Modern dapp browsers...
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
try {
// Request account access if needed
await window.ethereum.enable()
// Acccounts now exposed
} catch (_) {
// User denied account access...
}
}
// Legacy dapp browsers...
else if (window.web3) window.web3 = new Web3(web3.currentProvider)
// Acccounts always exposed
// Non-dapp browsers...
else
console.log(
'Non-Ethereum browser detected. You should consider trying MetaMask!'
)
})
if (typeof window !== 'undefined' && typeof window.web3 !== 'undefined') {
console.log('Using the web3 object of the window...')
web3 = new Web3(window.web3.currentProvider)
}
export default web3
Preparing Helper Functions For SimpleEscrowWithERC1497 And Arbitrator Contracts¶
We need to call functions of SimpleEscrowWithERC1497
and the arbitrator (for arbitrationCost
, to be able to send the correct amount when creating a dispute), so we need helpers for them.
We will import build artifacts of SimpleEscrowWithERC1497
and Arbitrator
contracts to use their ABIs (application binary interface).
So we copy those under ethereum
directory and create two helper files (arbitrator.js
and simple-escrow-with-erc1497.js
) using each of them.
import SimpleEscrowWithERC1497 from './simple-escrow-with-erc1497.json'
import web3 from './web3'
export const contractInstance = address =>
new web3.eth.Contract(SimpleEscrowWithERC1497.abi, address)
export const deploy = (payer, payee, amount, arbitrator, metaevidence) =>
new web3.eth.Contract(SimpleEscrowWithERC1497.abi)
.deploy({
arguments: [payee, arbitrator, metaevidence],
data: SimpleEscrowWithERC1497.bytecode
})
.send({ from: payer, value: amount })
export const reclaimFunds = (senderAddress, instanceAddress, value) =>
contractInstance(instanceAddress)
.methods.reclaimFunds()
.send({ from: senderAddress, value })
export const releaseFunds = (senderAddress, instanceAddress) =>
contractInstance(instanceAddress)
.methods.releaseFunds()
.send({ from: senderAddress })
export const depositArbitrationFeeForPayee = (
senderAddress,
instanceAddress,
value
) =>
contractInstance(instanceAddress)
.methods.depositArbitrationFeeForPayee()
.send({ from: senderAddress, value })
export const reclamationPeriod = instanceAddress =>
contractInstance(instanceAddress)
.methods.reclamationPeriod()
.call()
export const arbitrationFeeDepositPeriod = instanceAddress =>
contractInstance(instanceAddress)
.methods.arbitrationFeeDepositPeriod()
.call()
export const createdAt = instanceAddress =>
contractInstance(instanceAddress)
.methods.createdAt()
.call()
export const remainingTimeToReclaim = instanceAddress =>
contractInstance(instanceAddress)
.methods.remainingTimeToReclaim()
.call()
export const remainingTimeToDepositArbitrationFee = instanceAddress =>
contractInstance(instanceAddress)
.methods.remainingTimeToDepositArbitrationFee()
.call()
export const arbitrator = instanceAddress =>
contractInstance(instanceAddress)
.methods.arbitrator()
.call()
export const status = instanceAddress =>
contractInstance(instanceAddress)
.methods.status()
.call()
export const value = instanceAddress =>
contractInstance(instanceAddress)
.methods.value()
.call()
export const submitEvidence = (instanceAddress, senderAddress, evidence) =>
contractInstance(instanceAddress)
.methods.submitEvidence(evidence)
.send({ from: senderAddress })
import Arbitrator from './arbitrator.json'
import web3 from './web3'
export const contractInstance = address =>
new web3.eth.Contract(Arbitrator.abi, address)
export const arbitrationCost = (instanceAddress, extraData) =>
contractInstance(instanceAddress)
.methods.arbitrationCost(web3.utils.utf8ToHex(extraData))
.call()
Evidence and Meta-Evidence Helpers¶
Recall Evidence Standard JSON format. These two javascript object factories will be used to create JSON objects according to the standard.
export default (fileURI, name, description) => ({
fileURI,
name,
description
})
export default (payer, payee, amount, title, description) => ({
category: 'Escrow',
title: title,
description: description,
question: 'Does payer deserves to be refunded?',
rulingOptions: {
type: 'single-select',
titles: ['Refund the Payer', 'Pay the Payee'],
descriptions: [
'Select to return funds to the payer',
'Select to release funds to the payee'
]
},
aliases: {
[payer]: 'payer',
[payee]: 'payee'
},
amount
})
Evidence Storage¶
We want to make sure evidence files are tamper-proof. So we need an immutable file storage. IPFS is perfect fit for this use-case. The following helper will let us publish evidence on IPFS, through the IPFS node at https://ipfs.kleros.io .
/**
* Send file to IPFS network via the Kleros IPFS node
* @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type.
* @param {ArrayBuffer} data - The raw data from the file to upload.
* @return {object} ipfs response. Should include the hash and path of the stored item.
*/
const ipfsPublish = async (fileName, data) => {
const buffer = await Buffer.from(data)
return new Promise((resolve, reject) => {
fetch('https://ipfs.kleros.io/add', {
method: 'POST',
body: JSON.stringify({
fileName,
buffer
}),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(success => resolve(success.data))
.catch(err => reject(err))
})
}
export default ipfsPublish
React Components¶
We will create a single-page react application to keep it simple. The main component, App
will contain two sub-components:
Deploy
Interact
Deploy
component will contain a form for arguments of SimpleEscrowWithERC1497
deployment and a deploy button.
Interact
component will have an input field for entering a contract address that is deployed already, to interact with. It will also have badges to show some state variable values of the contract.
In addition, it will have three buttons for three main functions: releaseFunds
, reclaimFunds
and depositArbitrationFeeForPayee
.
Lastly, it will have a file picker and submit button for submitting evidence.
App
will be responsible for accessing Ethereum. So it will give callbacks to Deploy
and Interact
to let them access Ethereum through App
.
App¶
import React from 'react'
import web3 from './ethereum/web3'
import generateEvidence from './ethereum/generate-evidence'
import generateMetaevidence from './ethereum/generate-meta-evidence'
import * as SimpleEscrowWithERC1497 from './ethereum/simple-escrow-with-erc1497'
import * as Arbitrator from './ethereum/arbitrator'
import Ipfs from 'ipfs-http-client'
import ipfsPublish from './ipfs-publish'
import Container from 'react-bootstrap/Container'
import Jumbotron from 'react-bootstrap/Jumbotron'
import Button from 'react-bootstrap/Button'
import Form from 'react-bootstrap/Form'
import Row from 'react-bootstrap/Row'
import Col from 'react-bootstrap/Col'
import Deploy from './deploy.js'
import Interact from './interact.js'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
activeAddress: '0x0000000000000000000000000000000000000000',
lastDeployedAddress: '0x0000000000000000000000000000000000000000'
}
this.ipfs = new Ipfs({
host: 'ipfs.kleros.io',
port: 5001,
protocol: 'https'
})
}
deploy = async (amount, payee, arbitrator, title, description) => {
const { activeAddress } = this.state
let metaevidence = generateMetaevidence(
web3.utils.toChecksumAddress(activeAddress),
web3.utils.toChecksumAddress(payee),
amount,
title,
description
)
const enc = new TextEncoder()
const ipfsHashMetaEvidenceObj = await ipfsPublish(
'metaEvidence.json',
enc.encode(JSON.stringify(metaevidence))
)
let result = await SimpleEscrowWithERC1497.deploy(
activeAddress,
payee,
amount,
arbitrator,
'/ipfs/' +
ipfsHashMetaEvidenceObj[1]['hash'] +
ipfsHashMetaEvidenceObj[0]['path']
)
this.setState({ lastDeployedAddress: result._address })
}
load = contractAddress =>
SimpleEscrowWithERC1497.contractInstance(contractAddress)
reclaimFunds = async (contractAddress, value) => {
const { activeAddress } = this.state
await SimpleEscrowWithERC1497.reclaimFunds(
activeAddress,
contractAddress,
value
)
}
releaseFunds = async contractAddress => {
const { activeAddress } = this.state
await SimpleEscrowWithERC1497.releaseFunds(activeAddress, contractAddress)
}
depositArbitrationFeeForPayee = (contractAddress, value) => {
const { activeAddress } = this.state
SimpleEscrowWithERC1497.depositArbitrationFeeForPayee(
activeAddress,
contractAddress,
value
)
}
reclamationPeriod = contractAddress =>
SimpleEscrowWithERC1497.reclamationPeriod(contractAddress)
arbitrationFeeDepositPeriod = contractAddress =>
SimpleEscrowWithERC1497.arbitrationFeeDepositPeriod(contractAddress)
remainingTimeToReclaim = contractAddress =>
SimpleEscrowWithERC1497.remainingTimeToReclaim(contractAddress)
remainingTimeToDepositArbitrationFee = contractAddress =>
SimpleEscrowWithERC1497.remainingTimeToDepositArbitrationFee(
contractAddress
)
arbitrationCost = (arbitratorAddress, extraData) =>
Arbitrator.arbitrationCost(arbitratorAddress, extraData)
arbitrator = contractAddress =>
SimpleEscrowWithERC1497.arbitrator(contractAddress)
status = contractAddress => SimpleEscrowWithERC1497.status(contractAddress)
value = contractAddress => SimpleEscrowWithERC1497.value(contractAddress)
submitEvidence = async (contractAddress, evidenceBuffer) => {
const { activeAddress } = this.state
const result = await ipfsPublish('name', evidenceBuffer)
let evidence = generateEvidence(
'/ipfs/' + result[0]['hash'],
'name',
'description'
)
const enc = new TextEncoder()
const ipfsHashEvidenceObj = await ipfsPublish(
'evidence.json',
enc.encode(JSON.stringify(evidence))
)
SimpleEscrowWithERC1497.submitEvidence(
contractAddress,
activeAddress,
'/ipfs/' + ipfsHashEvidenceObj[0]['hash']
)
}
async componentDidMount() {
if (window.web3 && window.web3.currentProvider.isMetaMask)
window.web3.eth.getAccounts((_, accounts) => {
this.setState({ activeAddress: accounts[0] })
})
else console.error('MetaMask account not detected :(')
window.ethereum.on('accountsChanged', accounts => {
this.setState({ activeAddress: accounts[0] })
})
}
render() {
const { lastDeployedAddress } = this.state
return (
<Container>
<Row>
<Col>
<h1 className="text-center my-5">
A Simple DAPP Using SimpleEscrowWithERC1497
</h1>
</Col>
</Row>
<Row>
<Col>
<Deploy deployCallback={this.deploy} />
</Col>
<Col>
<Interact
arbitratorCallback={this.arbitrator}
arbitrationCostCallback={this.arbitrationCost}
escrowAddress={lastDeployedAddress}
loadCallback={this.load}
reclaimFundsCallback={this.reclaimFunds}
releaseFundsCallback={this.releaseFunds}
depositArbitrationFeeForPayeeCallback={
this.depositArbitrationFeeForPayee
}
remainingTimeToReclaimCallback={this.remainingTimeToReclaim}
remainingTimeToDepositArbitrationFeeCallback={
this.remainingTimeToDepositArbitrationFee
}
statusCallback={this.status}
valueCallback={this.value}
submitEvidenceCallback={this.submitEvidence}
/>
</Col>
</Row>
<Row>
<Col>
<Form action="https://centralizedarbitrator.netlify.com">
<Jumbotron className="m-5 text-center">
<h1>Need to interact with your arbitrator contract?</h1>
<p>
We have a general purpose user interface for centralized
arbitrators (like we have developed in the tutorial) already.
</p>
<p>
<Button type="submit" variant="primary">
Visit Centralized Arbitrator Dashboard
</Button>
</p>
</Jumbotron>
</Form>
</Col>
</Row>
</Container>
)
}
}
export default App
Deploy¶
import React from 'react'
import Container from 'react-bootstrap/Container'
import Form from 'react-bootstrap/Form'
import Button from 'react-bootstrap/Button'
import Card from 'react-bootstrap/Card'
class Deploy extends React.Component {
constructor(props) {
super(props)
this.state = {
amount: '',
payee: '',
arbitrator: '',
title: '',
description: ''
}
}
onAmountChange = e => {
this.setState({ amount: e.target.value })
}
onPayeeChange = e => {
this.setState({ payee: e.target.value })
}
onArbitratorChange = e => {
this.setState({ arbitrator: e.target.value })
}
onTitleChange = e => {
this.setState({ title: e.target.value })
}
onDescriptionChange = e => {
this.setState({ description: e.target.value })
}
onDeployButtonClick = async e => {
e.preventDefault()
const { amount, payee, arbitrator, title, description } = this.state
console.log(arbitrator)
await this.props.deployCallback(
amount,
payee,
arbitrator,
title,
description
)
}
render() {
const { amount, payee, arbitrator, title, description } = this.state
return (
<Container>
<Card className="my-4 text-center " style={{ width: 'auto' }}>
<Card.Body>
<Card.Title>Deploy</Card.Title>
<Form>
<Form.Group controlId="amount">
<Form.Control
as="input"
rows="1"
value={amount}
onChange={this.onAmountChange}
placeholder={'Escrow Amount in Weis'}
/>
</Form.Group>
<Form.Group controlId="payee">
<Form.Control
as="input"
rows="1"
value={payee}
onChange={this.onPayeeChange}
placeholder={'Payee Address'}
/>
</Form.Group>
<Form.Group controlId="arbitrator">
<Form.Control
as="input"
rows="1"
value={arbitrator}
onChange={this.onArbitratorChange}
placeholder={'Arbitrator Address'}
/>
</Form.Group>
<Form.Group controlId="title">
<Form.Control
as="input"
rows="1"
value={title}
onChange={this.onTitleChange}
placeholder={'Title'}
/>
</Form.Group>
<Form.Group controlId="description">
<Form.Control
as="input"
rows="1"
value={description}
onChange={this.onDescriptionChange}
placeholder={'Describe The Agreement'}
/>
</Form.Group>
<Button
variant="primary"
type="button"
onClick={this.onDeployButtonClick}
block
>
Deploy
</Button>
</Form>
</Card.Body>
</Card>
</Container>
)
}
}
export default Deploy
Interact¶
import React from 'react'
import Container from 'react-bootstrap/Container'
import Form from 'react-bootstrap/Form'
import Button from 'react-bootstrap/Button'
import Badge from 'react-bootstrap/Badge'
import ButtonGroup from 'react-bootstrap/ButtonGroup'
import Card from 'react-bootstrap/Card'
import InputGroup from 'react-bootstrap/InputGroup'
class Interact extends React.Component {
constructor(props) {
super(props)
this.state = {
escrowAddress: this.props.escrowAddress,
remainingTimeToReclaim: 'Unassigned',
remainingTimeToDepositArbitrationFee: 'Unassigned',
status: 'Unassigned',
arbitrator: 'Unassigned',
value: 'Unassigned'
}
}
async componentDidUpdate(prevProps) {
if (this.props.escrowAddress !== prevProps.escrowAddress) {
await this.setState({ escrowAddress: this.props.escrowAddress })
this.updateBadges()
}
}
onEscrowAddressChange = async e => {
await this.setState({ escrowAddress: e.target.value })
this.updateBadges()
}
updateBadges = async () => {
const { escrowAddress, status } = this.state
try {
await this.setState({
status: await this.props.statusCallback(escrowAddress)
})
} catch (e) {
console.error(e)
this.setState({ status: 'ERROR' })
}
try {
this.setState({
arbitrator: await this.props.arbitratorCallback(escrowAddress)
})
} catch (e) {
console.error(e)
this.setState({ arbitrator: 'ERROR' })
}
try {
this.setState({ value: await this.props.valueCallback(escrowAddress) })
} catch (e) {
console.error(e)
this.setState({ value: 'ERROR' })
}
if (Number(status) === 0)
try {
this.setState({
remainingTimeToReclaim: await this.props.remainingTimeToReclaimCallback(
escrowAddress
)
})
} catch (e) {
console.error(e)
this.setState({ status: 'ERROR' })
}
if (Number(status) === 1)
try {
this.setState({
remainingTimeToDepositArbitrationFee: await this.props.remainingTimeToDepositArbitrationFeeCallback(
escrowAddress
)
})
} catch (e) {
console.error(e)
this.setState({ status: 'ERROR' })
}
}
onReclaimFundsButtonClick = async e => {
e.preventDefault()
const { escrowAddress } = this.state
let arbitrator = await this.props.arbitratorCallback(escrowAddress)
console.log(arbitrator)
let arbitrationCost = await this.props.arbitrationCostCallback(
arbitrator,
''
)
await this.props.reclaimFundsCallback(escrowAddress, arbitrationCost)
this.updateBadges()
}
onReleaseFundsButtonClick = async e => {
e.preventDefault()
const { escrowAddress } = this.state
await this.props.releaseFundsCallback(escrowAddress)
this.updateBadges()
}
onDepositArbitrationFeeFromPayeeButtonClicked = async e => {
e.preventDefault()
const { escrowAddress } = this.state
let arbitrator = await this.props.arbitratorCallback(escrowAddress)
let arbitrationCost = await this.props.arbitrationCostCallback(
arbitrator,
''
)
await this.props.depositArbitrationFeeForPayeeCallback(
escrowAddress,
arbitrationCost
)
this.updateBadges()
}
onInput = e => {
console.log(e.target.files)
this.setState({ fileInput: e.target.files[0] })
console.log('file input')
}
onSubmitButtonClick = async e => {
e.preventDefault()
const { escrowAddress, fileInput } = this.state
console.log('submit clicked')
console.log(fileInput)
var reader = new FileReader()
reader.readAsArrayBuffer(fileInput)
reader.addEventListener('loadend', async () => {
const buffer = Buffer.from(reader.result)
this.props.submitEvidenceCallback(escrowAddress, buffer)
})
}
render() {
const { escrowAddress, fileInput } = this.state
return (
<Container className="container-fluid d-flex h-100 flex-column">
<Card className="h-100 my-4 text-center" style={{ width: 'auto' }}>
<Card.Body>
<Card.Title>Interact</Card.Title>
<Form.Group controlId="escrow-address">
<Form.Control
className="text-center"
as="input"
rows="1"
value={escrowAddress}
onChange={this.onEscrowAddressChange}
/>
</Form.Group>
<Card.Subtitle className="mt-3 mb-1 text-muted">
Smart Contract State
</Card.Subtitle>
<Badge className="m-1" pill variant="info">
Status Code: {this.state.status}
</Badge>
<Badge className="m-1" pill variant="info">
Escrow Amount in Weis: {this.state.value}
</Badge>
<Badge className="m-1" pill variant="info">
Remaining Time To Reclaim Funds:{' '}
{this.state.remainingTimeToReclaim}
</Badge>
<Badge className="m-1" pill variant="info">
Remaining Time To Deposit Arbitration Fee:{' '}
{this.state.remainingTimeToDepositArbitrationFee}
</Badge>
<Badge className="m-1" pill variant="info">
Arbitrator: {this.state.arbitrator}
</Badge>
<ButtonGroup className="mt-3">
<Button
className="mr-2"
variant="primary"
type="button"
onClick={this.onReleaseFundsButtonClick}
>
Release
</Button>
<Button
className="mr-2"
variant="secondary"
type="button"
onClick={this.onReclaimFundsButtonClick}
>
Reclaim
</Button>
<Button
variant="secondary"
type="button"
onClick={this.onDepositArbitrationFeeFromPayeeButtonClicked}
block
>
Deposit Arbitration Fee For Payee
</Button>
</ButtonGroup>
<InputGroup className="mt-3">
<div className="input-group">
<div className="custom-file">
<input
type="file"
className="custom-file-input"
id="inputGroupFile04"
onInput={this.onInput}
/>
<label
className="text-left custom-file-label"
htmlFor="inputGroupFile04"
>
{(fileInput && fileInput.name) || 'Choose evidence file'}
</label>
</div>
<div className="input-group-append">
<button
className="btn btn-primary"
type="button"
onClick={this.onSubmitButtonClick}
>
Submit
</button>
</div>
</div>
</InputGroup>
</Card.Body>
</Card>
</Container>
)
}
}
export default Interact
Arbitrator Side¶
To interact with an arbitrator, we can use Centralized Arbitrator Dashboard. It let’s setting up an arbitrator easily and provides UI to interact with, very useful for debugging and testing arbitrable implementations. As arbitrator, it deploys AutoAppealableArbitrator which is very similar to the one we developed in the tutorials.
To Use Centralized Arbitrator Dashboard (CAD):
- Deploy a new arbitrator by specifying arbitration fee, choose a tiny amount for convenience, like 0.001 Ether.
- Copy the arbitrator address and use this address as the arbitrator, in your arbitrable contract.
- Create a dispute on your arbitrable contract.
- Go back to CAD, select the arbitrator you created in the first step, by entering the contract address.
- Now you should be able to see the dispute you created. You can give rulings to it using CAD.
Alternatively, you can use Kleros Arbitrator on Kovan network for testing. In that case, use this arbitrator address in your arbitrable contract, then simply go to https://court.kleros.io and switch your web3 provider to Kovan network. To be able to stake in a court, you will need Kovan PNK token, which you can buy from https://court.kleros.io/tokens.
Finally, when your arbitrable contract is ready, use Kleros Arbitrator on main network to integrate with Kleros.