HelloWorld Dapp

Beryllium – Learning Solidity

Project Beryllium

Why?

Enable quickly getting started with smart contract development by providing an example.

What?

  • Use events to log messages.
  • Deploy and interact with contract.
  • Create a dapp that listens for events and display messages.

How?

Learn about events at: https://solidity.readthedocs.io/en/v0.5.3/contracts.html#events.

Setup prerequisites at: https://blog.baowebdev.com/2019/10/using-openzeppelin-tutorial-starter-kit/.

Create the contract.

pragma solidity ^0.5.0;

contract HelloWorld {
  event Message(string message);

  function Say(string memory message) public {
    // actual storage is byte32(message) in data portion of transaction log. 
    emit Message(message);
  }
}

Deploy the contract.

oz create
 ✓ Compiled contracts with solc 0.5.12 (commit.7709ece9)
? Pick a contract to instantiate
    = Your contracts =
   Counter
 ❯ HelloWorld
   Wallet

oz create
  ✓ Compiled contracts with solc 0.5.12 (commit.7709ece9) 
 ? Pick a contract to instantiate HelloWorld
 ? Pick a network (Use arrow keys)
 ❯ development
   ropsten
   kovan
   rinkeby
   main

oz create
 ✓ Compiled contracts with solc 0.5.12 (commit.7709ece9)
 ? Pick a contract to instantiate HelloWorld
 ? Pick a network development
 ✓ Added contract HelloWorld
 ✓ Contract HelloWorld deployed
 All contracts have been deployed
 ? Do you want to call a function on the instance after creating it? Yes
 ? Select which function Say(message: string)
 ? message (string): hello world!
 ✓ Instance created at 0xe982E462b094850F12AF94d21D470e21bE9D0E9C
 0xe982E462b094850F12AF94d21D470e21bE9D0E9C

Listen for events and display the message.

Use web3 library as the Ethereum client.

npm install web3

 web3@1.2.2
 added 176 packages from 57 contributors, updated 29 packages and moved 4 packages in 50.264s 

Refer to this guide to learn how to use web3 to interact with local blockchain: https://docs.openzeppelin.com/sdk/2.5/interacting.

Add header entry for Hello World dapp.

Append the following snippet to the end of the Header template.

         <li>
          <a href="/helloworld" className={styles.link}>
            {' '}
            Hello World
          </a>
        </li>

The Dapp is updated to show Hello World in the header and URL path.

Add Hello World page to the dapp.

Update logic to render the Hello World page when the URL path is navigated to helloworld.

  renderHelloWorld() {
    return (
      <div className={styles.wrapper}>
        <Instructions ganacheAccounts={this.state.ganacheAccounts} name="helloworld" accounts={this.state.accounts} />
      </div>
    );
  }

  render() {
    return (
      <div className={styles.App}>
        <Header />
        {this.state.route === '' && this.renderInstructions()}
        {this.state.route === 'counter' && this.renderBody()}
        {this.state.route === 'evm' && this.renderEVM()}
        {this.state.route === 'faq' && this.renderFAQ()}
        {this.state.route === 'helloworld' && this.renderHelloWorld()}
        <Footer />
      </div>
    );
  }
Update Instructions template to display placeholder while dapp is loading.

Update logic to render placeholder when URL path is helloworld.

  renderHelloWorld() {
    return (
      <div className={styles.instructions}>
        <h1> Hello World Dapp </h1>
        <div className={styles.step}>
          <div className={styles.instruction}>Congratulations! Your application is correctly setup.</div>
        </div>

        <div className={styles.step}>
          <div className={styles.instruction}>
            Listening for events...
          </div>
        </div>
      </div>
    );
  }

  render() {
    const { name } = this.props;
    switch (name) {
      case 'setup':
        return this.renderSetup();
      case 'metamask':
        return this.renderMetamask();
      case 'upgrade':
        return this.renderUpgrade();
      case 'upgrade-auto':
        return this.renderAutoUpgrade();
      case 'counter':
        return this.renderCounterSetup();
      case 'faq':
        return this.renderFAQ();
      case 'evm':
        return this.renderEVM();
      case 'helloworld':
        return this.renderHelloWorld();
      default:
        return this.renderSetup();
    }
  }
Wait for the contract to initialize and render the dapp.

The end result looks like below.

The strategy is to reuse the Counter template. Start by loading the prerequisites in App.js. Require the HelloWorld.sol contract. Initialize the contact and assign it to instanceHelloWorld. Assign it to this.state.helloWorld variable so it can be used throughout the dapp.

import HelloWorld from './components/HelloWorld/index.js';

  componentDidMount = async () => {
    const hotLoaderDisabled = solidityLoaderOptions.disabled;
    let Counter = {};
    let Wallet = {};
    let HelloWorld = {};
    try {
      Counter = require('../../contracts/Counter.sol');
      Wallet = require('../../contracts/Wallet.sol');
      HelloWorld = require('../../contracts/HelloWorld.sol');
    } catch (e) {
      console.log(e);
    }


      let instance = null;
      let instanceWallet = null;
      let instanceHelloWorld = null;

 
      if (HelloWorld.networks) {
        deployedNetwork = HelloWorld.networks[networkId.toString()];
        if (deployedNetwork) {
          instanceHelloWorld = new web3.eth.Contract(HelloWorld.abi, deployedNetwork && deployedNetwork.address);
        }
      }
      if (instance || instanceWallet || instanceHelloWorld) {
        // Set web3, accounts, and contract to the state, and then proceed with an
        // example of interacting with the contract's methods.
        this.setState(
          {
            web3,
            ganacheAccounts,
            accounts,
            balance,
            networkId,
            networkType,
            hotLoaderDisabled,
            isMetaMask,
            contract: instance,
            wallet: instanceWallet,
            helloWorld: instanceHelloWorld
          },
          () => {
            this.refreshValues(instance, instanceWallet, instanceHelloWorld);
            setInterval(() => {
              this.refreshValues(instance, instanceWallet, instanceHelloWorld);
            }, 5000);
          },
        );
      } else {
        this.setState({
          web3,
          ganacheAccounts,
          accounts,
          balance,
          networkId,
          networkType,
          hotLoaderDisabled,
          isMetaMask,
        });
      } 

  refreshValues = (instance, instanceWallet, instanceHelloWorld) => {
    if (instance) {
      this.getCount();
    }
    if (instanceWallet) {
      this.updateTokenOwner();
    }
    // optionally, add logic to refresh the UX every 5000ms.
  };

Reuse renderBody() in App.js. Rename it to renderHelloWorld().

  renderHelloWorld() {
    const { accounts, ganacheAccounts } = this.state;
    return (
      <div className={styles.wrapper}>
        {!this.state.web3 && this.renderLoader()}
        {this.state.web3 && !this.state.helloWorld && this.renderDeployCheck('helloworld')}
        {this.state.web3 && this.state.helloWorld && (
          <div className={styles.contracts}>
            <h1>Hello World Contract is good to Go!</h1>
            <p>Interact with your contract on the right.</p>
            <p> You can see your account info on the left </p>
            <div className={styles.widgets}>
              <Web3Info {...this.state} />
              <HelloWorld {...this.state} />
            </div>
            {this.state.balance < 0.1 && (
              <Instructions ganacheAccounts={ganacheAccounts} name="metamask" accounts={accounts} />
            )}
          </div>
        )}
      </div>
    );
  }
Reuse the state check logic.

If the web3 client has not been loaded, then render a placeholder indicating the dapp is loading.

If the web3 client is loaded, and the HelloWorld contract has not been loaded, then render instructions on how to deploy the contract.

Invoking this.renderDeployCheck('helloWorld') renders the Instructions page.

Update the Instructions page with steps to deploy the HelloWorld contract.
  renderHelloWorld() {
    return (
      <div className={styles.instructions}>
        <h1> Hello World Dapp </h1>
        <div className={styles.step}>
          <div className={styles.instruction}>
            Create an instance of the HelloWorld contract and deploy it using the create command, follow the cli prompts.
          </div>
          <div className={styles.code}>
            <code>openzeppelin create HelloWorld</code>
          </div>
        </div>
        <div className={styles.step}>
          <div className={styles.instruction}>
            Done! Refresh the page to interact with your instance of the HelloWorld contract.
          </div>
          <br />
          <Button onClick={() => window.location.reload()}>Reload</Button>
        </div>
      </div>
    );
  }

If the HelloWorld contract has been loaded then render the Hello World dapp. Reuse the Web3Info component to display account information.

Create a new HelloWorld component to interact with the HelloWorld contract.

Import rimble-ui components. See: https://consensys.github.io/rimble-ui/?path=/story/getting-started–welcome. Setup the component in the constructor by setting the initial form validation state and message. Register event listener to the Message event in the HelloWorld contract. Bind the event handler to the HelloWorld component so the event handler has access to state variables. See: https://reactjs.org/docs/handling-events.html.

import React, { Component } from 'react';
import { Form, Button, Text } from 'rimble-ui';
import styles from './HelloWorld.module.scss';

export default class HelloWorld extends Component {
  constructor(props) {
    super(props);
    const { helloWorld } = this.props;
    this.state = { validated: false, message: "" };
    this.handleEvent = this.handleEvent.bind(this);
    helloWorld.events.Message({}, this.handleEvent);
  }

  handleEvent = (error, event) => {
    this.state.message = event.returnValues.message;
  }

  handleSubmit = e => {
    e.preventDefault();
    const { helloWorld, accounts } = this.props;
    helloWorld.methods.Say(e.target.saySomething.value).send({ from: accounts[0] });
  };

  handleValidation = e => {
    e.target.parentNode.classList.add('was-validated');
  };

  render() {
    const { helloWorld } = this.props;
    return (
      <div className={styles.helloWorld}>
        <Form onSubmit={this.handleSubmit}>
          <h3> Your HelloWorld Contract Instance </h3>
          <Form.Field validated={this.state.validated} label="Say Something" name="saySomething" width={1} >
            <Form.Input type="text" placeholder="e.g. Hello World!" name="saySomething" required width={1} onChange={this.handleValidation} />
          </Form.Field>
          <div className={styles.buttons}>
            <Button type="submit" size="small" disabled={!helloWorld.methods.Say} >
              Talk
            </Button>
          </div>
        </Form>
        <Text.p>{this.state.message}</Text.p>
      </div>
    );
  }
}

Wire up the form to this.handleSubmit. This function will be invoked when the submit Button is invoked. The submit logic will invoke the Say method in the HelloWorld contract. The contract will emit a Message event. The handleEvent method will be invoked to update this.state.message with the value emitted by the Message event.

%d bloggers like this: