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.
βοΈ Prerequisites: Node (v16 LTS recommended), Git, NPM, Truffe, MetaMask
- 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
- 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
- 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
- OPTIONAL: You can run tests written in Solidity or JavaScript against your smart contracts. Check
/tests
folder π
truffle test
- OPTIONAL: You can run custom scripts against your smart contracts. Check
/scripts
folder π
truffle exec scripts/increment.js
- 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.
- 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:
- 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
- Modify the client package file:
package.json
Change "start": "react-scripts start" --- to ---> "start": "react-scripts --openssl-legacy-provider start"
- 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
- 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> <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>
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()
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.
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.
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:
- Use touch .env in the command line to create a .env file at the root of your project.
- Open the .env file in your preferred IDE
- 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>"
- 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>'].