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

  1. Run yarn create react-app a-simple-dapp to create a directory “a-simple-dapp” under your working directory and scaffold your application.
  2. 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.
  3. 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"
/>
  1. 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 .

web3.js
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.

simple-escrow-with-erc1497.js
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 })
arbitrator.js
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.

generate-evidence.js
export default (fileURI, name, description) => ({
  fileURI,
  name,
  description
})
generate-meta-evidence.js
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 .

ipfs-publish.js
/**
 * 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

app.js
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.fyi">
              <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

deploy.js
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

interact.js
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 (status == 0)
      try {
        this.setState({
          remainingTimeToReclaim: await this.props.remainingTimeToReclaimCallback(
            escrowAddress
          )
        })
      } catch (e) {
        console.error(e)
        this.setState({ status: 'ERROR' })
      }

    if (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 can load arbitrators with a given address to interact with, also can deploy an AutoAppealableArbitrator which is very similar to the one we developed in the tutorials.