Skip to content

React Truffle Box that provides everything you need to quickly build, test and deploy Smart Contracts, and interact with them from a React DApp

License

Notifications You must be signed in to change notification settings

Diegoescalonaro/react-simple-truffle-box

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

52 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

React Simple Truffle Box

This simple box comes with everything you need to start using Truffle to write, compile, test, and deploy smart contracts, and interact with them from a React app.

Installation

βš™οΈ Prerequisites: Node (v16 LTS recommended), Git, NPM, Truffe, MetaMask

  1. First ensure you are in an empty directory.

Run the unbox command using 1 of 2 ways.

# Install Truffle globally and run `truffle unbox`
npm install -g truffle
truffle unbox Diegoescalonaro/react-simple-truffle-box
# Alternatively, run `truffle unbox` via npx
npx truffle unbox Diegoescalonaro/react-simple-truffle-box

Compile and deploy Smart Contracts to Ganache network

  1. Now run the Ganache local network. You can execute the command below or just run the Ganache UI. This will spin up and allow you to interact with ganache, a local test chain on localhost:8545
npx ganache-cli
  1. Compile and migrate the smart contracts. Running migrate will do both. Note inside the development console we don't have to preface commands with truffle. Check /contracts and /migrations folders πŸ”Ž
truffle compile
truffle migrate --network ganache
  1. OPTIONAL: You can run tests written in Solidity or JavaScript against your smart contracts. Check /tests folder πŸ”Ž
truffle test
  1. OPTIONAL: You can run custom scripts against your smart contracts. Check /scripts folder πŸ”Ž
truffle exec scripts/increment.js

Run the client app (React)

  1. Make sure you have MetaMask installed in your browser, and connected to the Ganache network. You will need to add the network configuration as well as import one of the accounts generated by Ganache to be able to interact with the Smart Contract deployed on the Ganache network.

Screenshot 2023-03-14 at 23 00 24

Screenshot 2023-03-14 at 23 00 24

  1. In the /client directory, we run the React app.
cd client
npm start
  Starting the development server...
🚨⚠️ Possible errors running the client app
  • 🚨 Incompatibility with NodeJS version: v18 or higher

If your nodejs version is v18 or higher, sometimes when trying to run the client app an error is thrown: error:0308010C:digital envelope routines::unsupported. In this case, you can downgrade to nodejs v16 or follow the steps below:

  1. Set an env variable: ```NODE_OPTIONS````

For Windows (run in terminal): set NODE_OPTIONS=--openssl-legacy-provider For Unix (run in terminal): export NODE_OPTIONS=--openssl-legacy-provider

  1. Modify the client package file: package.json

Change "start": "react-scripts start" --- to ---> "start": "react-scripts --openssl-legacy-provider start"

  1. Run the app again: npm start
  • 🚨 Not configured NetworkID on App.js

If your client app is running but the browser at localhost:3000 is showing the following error: Cannot read properties of undefined (reading 'address')". You must check that the NetworkID in the App.js file is configured according to the network you're using (used in the SimpleStorage.json compiled contract file.

Set te NetworkID here (in App.js) πŸ‘‡

const CONTRACT_ADDRESS = require("../contracts/SimpleStorage.json").networks[1337].address

  1. OPTIONAL: Build the application for production using the build script. A production build will be in the dist/ folder πŸ”Ž
cd client
npm run build

From there, follow the instructions on the hosted React app. It will walk you through using Truffle and Ganache to deploy the SimpleStorage contract, making calls to it, and sending transactions to change the contract's state.

Customize DApp based on Auction.sol

CONTEXT Information πŸ€“
// Use web3 to get the user's accounts.
const accounts = await web3.eth.getAccounts();

// Get the network ID
const networkId = await web3.eth.net.getId();

// Set data as a component state
this.setState({accounts, networkId})
{/* ---- Context Information: Account & Network ---- */}
<div className="Auction-header">
    <div className="Header-context-information">
      <p> Network connected: {this.state.networkId}</p>
      <p> Your address: {this.state.accounts[0]}</p>
    </div>
</div>
// --------- METAMASK EVENTS ---------
  handleMetamaskEvent = async () => {
    window.ethereum.on('accountsChanged', function (accounts) {
      // Time to reload your interface with accounts[0]!
      alert("Incoming event from Metamask: Account changed 🦊")
      window.location.reload()
    })

    window.ethereum.on('networkChanged', function (networkId) {
      // Time to reload your interface with the new networkId
      alert("Incoming event from Metamask: Network changed 🦊")
      window.location.reload()
    })
  }
// --------- TO LISTEN TO EVENTS AFTER EVERY COMPONENT MOUNT ---------
this.handleMetamaskEvent()
SMART CONTRACT configuration βš™οΈ
const CONTRACT_ADDRESS = require("../contracts/Auction.json").networks[1337].address
const CONTRACT_ABI = require("../contracts/Auction.json").abi;
  
const contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
GET Methods πŸ“–
// ------------ GET AUCTION INFORMATION FUNCTION ------------
getAuctionInformation = async () => {
  const { accounts, contract } = this.state;

  // Get the auction information
  const response = await contract.methods.getAuctionInfo().call({ from: accounts[0] });
  this.setState({ auctionInfo: response })

  // Get the highest price and bidder, and the status of the auction
  const imageURI = await contract.methods.getImageURI().call();
  const highestPrice = await contract.methods.getHighestPrice().call();
  const highestBidder = await contract.methods.getHighestBidder().call();
  const basePrice = await contract.methods.getBasePrice().call();
  const originalOwner = await contract.methods.originalOwner().call();
  const newOwner = await contract.methods.newOwner().call();
  const isActive = await contract.methods.isActive().call();
  this.setState({ imageURI, highestPrice, highestBidder, basePrice, originalOwner, newOwner, isActive })
}
{/* ---- Auction information ---- */}
<div className="Auction-component-1">
  <div className="Auction-component-body">
    <h2 id="inline">Auction information</h2>
    <button id="button-call" onClick={this.getAuctionInformation}> GET INFORMATION</button>
    {
      this.state.auctionInfo &&
      <>
        <div className="Auction-information">
          {/* Auction Image */}
          <div className="Auction-information-img">
            {this.state.imageURI && <img src={this.state.imageURI}></img>}
            {this.state.imageURI && <p><u>Descargar imΓ‘gen</u> &nbsp;&nbsp; <u>Solicitar mΓ‘s imΓ‘genes</u></p>}
          </div>
          {/* Auction information */}
          <div className="Auction-information-text">

            {/* Auction Description */}
            <p>{this.state.auctionInfo[0]}</p>

            {/* Basic Information */}
            <p><b>Status: </b>{this.state.isActive ? "The auction is still active!! 🀩 🀩" : "The auction is not longer active 😭 😭"}</p>
            <p><b>Created at:</b> {this.state.auctionInfo[1]}</p>
            <p><b>Duration:</b> {this.state.auctionInfo[2]} seconds</p>

            {/* More information */}
            {this.state.highestBidder && <p><b>Highest Bidder:</b> {this.state.highestBidder}</p>}
            {this.state.highestPrice && <p><b>Highest Price:</b> {this.state.web3Provider.utils.fromWei(this.state.highestPrice, 'ether')} ether</p>}
            {this.state.basePrice && <p><b>Base price:</b> {this.state.basePrice}</p>}
            {this.state.originalOwner && <p><b>Original Owner:</b> {this.state.originalOwner}</p>}
            {this.state.newOwner && <p><b>New Owner:</b> {this.state.newOwner}</p>}
          </div>
        </div>
      </>
    }
  </div>
</div>
SET Methods πŸ–ŠοΈ
// ------------ BID FUNCTION ------------
bid = async () => {
  const { accounts, contract } = this.state;

  // Bid at an auction for X value
  await contract.methods.bid().send({ from: accounts[0], value: this.state.value });

  // Get the new values: highest price and bidder, and the status of the auction
  const highestPrice = await contract.methods.getHighestPrice().call();
  const highestBidder = await contract.methods.getHighestBidder().call();
  const isActive = await contract.methods.isActive().call();

  // Update state with the result.
  this.setState({ isActive: isActive, highestPrice, highestBidder });
};

// ------------ STOP AUCTION FUNCTION ------------
stopAuction = async () => {
  const { accounts, contract } = this.state;

  // Stop the auction
  await contract.methods.stopAuction().send({ from: accounts[0] });

  // Get the new values: isActive and newOwner
  const isActive = await contract.methods.isActive().call();
  const newOwner = await contract.methods.newOwner().call();

  // Update state with the result.
  this.setState({ isActive, newOwner });
}
{/* ---- Auction actions ---- */}
<div className="Auction-component-2">
  <div className="Auction-component-body">
    <div className="Auction-actions">
      <h2>Auction actions</h2>

      {/* Input & Button to bid */}
      <input placeholder="Insert value in wei" onChange={(e) => this.setState({ value: e.target.value })}></input>
      <button id="button-send" onClick={this.bid}>BID</button>

      {/* Button to stop auction */}
      <button id="button-send" onClick={this.stopAuction}>STOP AUCTION</button>

      {/* Helper to convert wei to ether */}
      {this.state.value && <p>You're gonna bid: {this.state.web3Provider.utils.fromWei(this.state.value, 'ether')} ether</p>}
    </div>
  </div>
</div>

Advanced customization

Switch network (MetaMask) ➑️
// ------------ METAMASK SWITCH NETWORK ------------
switchNetwork = async () => {
  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [
        {
          chainId: this.state.web3Provider.utils.toHex(5)
        }
      ]
    });
  } catch (switchError) {
    if (switchError.code === 4902) {
      try {
        await window.ethereum.request({
          method: 'wallet_addEthereumChain',
          params: [
            {
              chainId: this.state.web3Provider.utils.toHex(5),
              chainName: 'Goerli',
              rpcUrls: ['https://goerli.infura.io/v3/'],
            },
          ],
        });
      } catch (addError) {
        console.log(addError)
      }
    }
  }
}
{this.state.networkId !== 5 && <p id="inline">This DAPP is currently working on GOERLI, please press the button</p>}
{this.state.networkId !== 5 && <button onClick={this.switchNetwork}>Switch to GOERLI</button>}

OPTIONAL: Test to swtich to another testnet like Mumbai (Polygon testnet)

{
	chainId: this.state.web3Provider.utils.toHex(80001),
	chainName: 'Mumbai Testnet',
	rpcUrls: ['https://endpoints.omniatech.io/v1/matic/mumbai/public'],
	nativeCurrency: {
	  name: 'MATIC',
	  symbol: 'MATIC',// 2-6 characters long
	  decimals: 18,
	},
},
Sign message (MetaMask) ✍️
// ------------ SIGN WITH METAMASK ------------
signMessage = async () => {
  const { accounts, web3Provider } = this.state;
  var signature = await web3Provider.eth.personal.sign("Esto es un mensaje que quiero firmar", accounts[0])
  this.setState({ signature: signature, signer: accounts[0] });
}
{/* Button to sign a message (i.e. sign the bid) */}
<button onClick={this.signMessage}>SIGN MESSAGE</button>
<div style={{ overflowWrap: "anywhere" }}>
  {this.state.signature && <p>Signed message: {this.state.signature}</p>}
  {this.state.signer && <p>Signer address: {this.state.signer}</p>}
</div>
Smart Contract event listener πŸ‘‚
// --------- SMART CONTRACT EVENTS ---------
  handleContractEvent = async () => {
    if (!this.state.contract) return
    this.state.contract.events.allEvents()
      .on("connected", function (subscriptionId) {
        console.log("New subscription with ID: " + subscriptionId)
      })
      .on('data', function (event) {
        console.log("New event: %o", event)
        if (event.event == "Result") {
          alert("The auction has finished  πŸ’° πŸ’Έ")
        }
        if (event.event == "Status") {
          alert("New Highest BID πŸ€‘ πŸ’° πŸ’Έ")
        }
      })
  }
// --------- TO LISTEN TO EVENTS AFTER EVERY COMPONENT UPDATE ---------
this.handleContractEvent()

Deployment on public testnet

To deploy your contracts to a public network (such as a testnet or mainnet) there are two approaches. The first uses Truffle Dashboard which provides "an easy way to use your existing MetaMask wallet for your deployments". The second, requires copying your private key or mnemonic into your project so the deployment transactions can be signed prior to submission to the network.

Using Truffle Dashboard (recommended)

Truffle Dashboard ships with Truffle and can be started with truffle dashboard. This in turn loads the dashboard at http://localhost:24012 and beyond that you'll just need to run your migration (truffle migrate --network dashboard). A more detailed guide to using Truffle Dashboard is available here.

Using the env file and Infura

You will need at least one mnemonic to use with the network. The .dotenv npm package has been installed for you, and you will need to create a .env file for storing your mnemonic and any other needed private information.

The .env file is ignored by git in this project, to help protect your private data. In general, it is good security practice to avoid committing information about your private keys to github. The truffle-config.js file expects a MNEMONIC value to exist in .env for running commands on each of these networks, as well as a default MNEMONIC for the Arbitrum network we will run locally.

If you are unfamiliar with using .env for managing your mnemonics and other keys, the basic steps for doing so are below:

  1. Use touch .env in the command line to create a .env file at the root of your project.
  2. Open the .env file in your preferred IDE
  3. Add the following, filling in your own Infura project key and mnemonics:
MNEMONIC="<YOUR MNEMONIC HERE>"
INFURA_KEY="<Your Infura Project ID>"
RINKEBY_MNEMONIC="<Your Rinkeby Mnemonic>"
MAINNET_MNEMONIC="<Your Mainnet Mnemonic>"
MAINNET_MNEMONIC="<Your Mainnet Mnemonic>"
  1. As you develop your project, you can put any other sensitive information in this file. You can access it from other files with require('dotenv').config() and refer to the variable you need with process.env['<YOUR_VARIABLE>'].

About

React Truffle Box that provides everything you need to quickly build, test and deploy Smart Contracts, and interact with them from a React DApp

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published