Introduction
We will learn how to build smart contracts by making an auction on which users can place bids, and deploy the smart contracts on Avalanche. We will then be able to interact with them using ReactJS and Drizzle.
We are going to generate ReactJS boilerplate code using
create-react-app
, which we will modify for our auction bidding frontend. React is useful for this task due to its efficiency and user-friendly blockchain interaction. For the smart contracts which provide the on-chain functionality, Solidity code will be deployed to the Avalanche blockchain using Truffle.Prerequisites
- Basic knowledge of ReactJS.
- Basic knowledge of Avalanche's architecture and smart contracts.
- Basic familiarity with the React context API.
- You will also need an account on DataHub.
Requirements
- NodeJS >= 10.16 and npm >= 5.6 installed.
- Truffle, which can be installed globally with
npm install -g truffle
. - The Metamask wallet software must be added to your browser, which should only be obtained from the official Metamask website: https://metamask.io. Do not download Metamask from an unofficial source!
Project setup
Before starting work on the project, we need to set up a working directory. Follow the steps below to create a directory for the project. Open up a terminal and navigate to the directory where you would like to store this application - typically the user home directory.
Create a new subdirectory with the command
mkdir <directory_name>
. Change the current working directory to this newly created directory using cd <directory_name>
. For instance: if we name it bid
, then:mkdir bid cd bid
Creating the React project
We can create a new React app using
npx
(npm's package runner). The typical use is to download the files needed to run an npm package. Using npx
to execute the package binaries for create-react-app
will generate a new React app scaffold in the specified directory.npx create-react-app client
Now change to the directory "client" using
cd client
, then install the required dependencies using the command:npm install --save dotenv web3 @truffle/contract @truffle/hdwallet-provider @drizzle/store
Open the file
index.js
inside the src
directory and replace the existing code with the following:import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import { Drizzle } from "@drizzle/store"; import drizzleOptions from "./drizzleOptions"; import { DrizzleProvider } from "./drizzleContext"; const drizzle = new Drizzle(drizzleOptions); ReactDOM.render( <DrizzleProvider drizzle={drizzle}> <App /> </DrizzleProvider>, document.getElementById("root") );
Next, open the
App.js
file present inside the src
directory and replace the existing code with the following:import Auction from "./Auction"; import { useDrizzleContext } from "./drizzleContext"; function App() { const { drizzleVariables } = useDrizzleContext(); if (!drizzleVariables.initialized) { return "Loading..."; } else { return <Auction />; } } export default App;
When this is done, the React project setup is complete. We are now ready to set up Truffle.
Setup the Truffle project
Truffle can be used to create the boilerplate code needed for the project. Run the following command in the project root directory:
truffle init
Now the basis of the project is set up. Solidity code will be stored in the
contracts
subdirectory. Truffle deployment functions written in JavaScript will be stored in the migrations
folder. By default, the /build/contracts
folder contains information about the compiled and deployed contract (like the Application Binary Interface) in JSON format. These meta-files are commonly referred to as artifacts
.truffle-config.js
is the configuration file created by the truffle init
command. This file contains the information telling Truffle how to deploy contracts, which network to deploy on, and more. We should keep the default file for reference and we can create a copy of this file using the command:cp truffle-config.js truffle-config-default.js
Now that we have a copy of the default configuration, we can update
truffle-config.js
with the information needed to deploy the smart contract on the Fuji testnet. This will help us in connecting to the DataHub Avalanche node, however we will require an Avalanche API key (from DataHub) along with an Avalanche wallet mnemonic used for deploying the contract on the network.
Replace the existing contents of truffle-config.js
with the following:require("dotenv").config(); const HDWalletProvider = require("@truffle/hdwallet-provider"); // Account credentials from which our contract will be deployed const mnemonic = process.env.MNEMONIC; // API key of your Datahub account for Avalanche Fuji test network const APIKEY = process.env.APIKEY; const DATAHUB_RPC_URL = `https://avalanche--fuji--rpc.datahub.figment.io/apikey/${process.env.APIKEY}/ext/bc/C/rpc`; module.exports = { contracts_build_directory: "./client/src/build/contracts/", networks: { fuji: { provider: function () { return new HDWalletProvider({ mnemonic, providerOrUrl: DATAHUB_RPC_URL, chainId: "0xa869", }); }, network_id: "*", gas: 3000000, gasPrice: 470000000000, skipDryRun: true, }, }, compilers: { solc: { version: "0.8.0", }, }, };
Here we are setting the
gas
and gasprice
to appropriate values for the Avalanche C-Chain. You might notice that contract_build_directory
is being used to change the default location of artifacts
from the project root directory to the src
folder. This is because React is unable to access the files present outside the src
folder.Receive Avalanche Credentials
For the deployment of the smart contracts we need to take care of two things:
- A node connected to the Avalanche network and an account with some AVAX.
- An Avalanche API key is required to access the DataHub Avalanche node through RPC (Remote Procedure Call). Visit the Avalanche Services Dashboard on DataHub to get an Avalanche specific API key.
Next, we need to create a new Avalanche wallet to make transactions on the network and to deposit our funds. To create an Avalanche wallet, go to https://wallet.avax.network and save all the necessary information -- the wallet address and mnemonic seed phrase.
Add .env file
Create a new file named
.env
in the project root folder. Copy your Avalanche API key from DataHub and the Avalanche wallet's mnemonic into the .env
file as shown below. If you have any difficulty in setting up the .env
file then please refer to the Figment Learn guide on dotenv and .env.DATAHUB_API_KEY=<your-api-key> MNEMONIC="<avalanche-wallet-mnemonic>"
Now the project setup is completed! Run the command
npm start
in the project root folder to start the development server. This will allow us to build the React interface with immediate visual feedback.Create Auction smart contract
Next we will need to create our main Solidity smart contract file named
Auction.sol
inside the contracts
subdirectory and add the following:// SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.0; contract AuctionManager { uint public uId = 0; uint public aId = 0; // Structure to store user information struct User { uint userId; string name; address publicAddress; } // Structure to store bid information struct Bid { uint userId; uint bidPrice; } // Structure to store auction information struct Auction { uint auctionId; uint userId; string name; string description; uint msp; uint auctionBidId; } // Structure to store real time analytics of each auction struct AuctionAnalytics { uint auctionId; uint auctionBidId; uint latestBid; uint lowestBid; uint highestBid; } // List of all auctions Auction[] public auctions; // Mapping for storing user info, bids and auction analytics mapping (uint => User) public users; mapping (uint => Bid[]) public bids; mapping (uint => AuctionAnalytics) public auctionAnalytics; // Public function to check the registration of users (public address) function isRegistered(address _publicAddress) public view returns (uint256[2] memory) { uint256[2] memory result = [uint256(0), uint256(0)]; for(uint i = 0; i < uId; i++) { if(_publicAddress == users[i].publicAddress) { result[0] = 1; result[1] = i; return result; } } return result; } // Creating new users function createUser(string memory _name) public { require((isRegistered(msg.sender))[0] == 0, "User already registered!"); users[uId] = User(uId, _name, msg.sender); uId++; } // Creating new auctions function createAuction(string memory _name, string memory _description, uint _msp) public { require((isRegistered(msg.sender))[0] == 1, "User not registered!"); uint MAX_UINT = 2 ** 256 - 1; auctions.push(Auction(aId, isRegistered(msg.sender)[1], _name, _description, _msp, 0)); auctionAnalytics[aId] = AuctionAnalytics(aId, 0, 0, MAX_UINT, 0); aId++; } // Private function to update auction analytics after the new bids function updateAucionAnalytics(uint _aId, uint _latestBid) private { auctionAnalytics[_aId].latestBid = _latestBid; auctionAnalytics[_aId].auctionBidId = auctions[_aId].auctionBidId; if(_latestBid < auctionAnalytics[_aId].lowestBid) { auctionAnalytics[_aId].lowestBid = _latestBid; } if(_latestBid > auctionAnalytics[_aId].highestBid) { auctionAnalytics[_aId].highestBid = _latestBid; } } // Creating new bids function createBid(uint _aId, uint _bidPrice) public { require((isRegistered(msg.sender))[0] == 1, "User not registered!"); bids[_aId].push(Bid((isRegistered(msg.sender))[1], _bidPrice)); auctions[_aId].auctionBidId++; updateAucionAnalytics(_aId, _bidPrice); } // Return list of all auctions function showAuctions() public view returns (Auction[] memory) { return auctions; } }
Auction
is a Solidity contract which enables us to view the auction details and correspondingly its minimum price. We will be accessing the deployed Auction contracts using their deployed address and ABI. Each time a new auction is created, the Solidity code will be deployed to the blockchain.Understanding the contract
Users, bids, auctions and analytics
// List of all auctions Auction[] public auctions; // Mapping for storing user info, bids and auction analytics mapping (uint => User) public users; mapping (uint => Bid[]) public bids; mapping (uint => AuctionAnalytics) public auctionAnalytics;
This code declares public variables for storing user information, their bids, auctions and auction analytics using Solidity mappings. Have a look at the struct definitions used in these variables: They contain the data types and fields which make up a User, Bid or AuctionAnalytics.
Function to check registered user
// Public function to check the registration of users (public address) function isRegistered(address _publicAddress) public view returns (uint256[2] memory) { uint256[2] memory result = [uint256(0), uint256(0)]; for(uint i = 0; i < uId; i++) { if(_publicAddress == users[i].publicAddress) { result[0] = 1; result[1] = i; return result; } } return result; }
This function takes the public address as its argument and returns an integer array with 2 elements - isRegistered at index 0 and userId at index 1. If 0th index is 1 then the user exists and vice-versa. The 1st index represents the userId of the user. This function iterates over the mapping users to check if the required public address exists.
Auction analytics
We have created a mapping for storing analytics like the latest bid, highest bid, and lowest bid for each auction. This mapping will map auctionId to AuctionAnalytic struct. When a new auction is created, we initialize its corresponding entry in the AuctionAnalytics map.
// Private function to update auction analytics after the new bids function updateAucionAnalytics(uint _aId, uint _latestBid) private { auctionAnalytics[_aId].latestBid = _latestBid; auctionAnalytics[_aId].auctionBidId = auctions[_aId].auctionBidId; if(_latestBid < auctionAnalytics[_aId].lowestBid) { auctionAnalytics[_aId].lowestBid = _latestBid; } if(_latestBid > auctionAnalytics[_aId].highestBid) { auctionAnalytics[_aId].highestBid = _latestBid; } }
The auction analytics need to be updated every time there is a new bid. This function is called whenever a bid is created. It takes an auctionId and the latest bid amount as its two arguments, and updates the analytics corresponding to the auction.
The remaining functions are self explanatory, but have been well commented for readers to understand.
Creating Truffle migrations
Create the file
Migration.sol
inside of the contracts
directory and add the following:// SPDX-License-Identifier: MIT pragma solidity >=0.4.22 <0.9.0; contract Migrations { address public owner = msg.sender; uint public last_completed_migration; modifier restricted() { require( msg.sender == owner, "This function is restricted to the contract's owner" ); _; } function setCompleted(uint completed) public restricted { last_completed_migration = completed; } }
This
Migration.sol
smart contract manages the deployment of other contracts that we want to migrate to Avalanche.Next, create a new file in the
migrations
directory named 2_deploy_contracts.js
, and add the following block of code. This handles deploying the Auction
smart contract to the blockchain.const AuctionManager = artifacts.require("./Auction.sol"); module.exports = function (deployer) { deployer.deploy(AuctionManager); };
Compiling contracts with Truffle
If we have altered the code within our Solidity source files or made new ones (like
Auction.sol
), we need to run truffle compile
in the terminal, from inside the project root directory.The expected output would look similar:
Compiling your contracts... =========================== > Compiling ./contracts/Auction.sol > Compiling ./contracts/Migrations.sol > Artifacts written to /home/guest/blockchain/client/build/contracts > Compiled successfully using: - solc: 0.8.0+commit.c7dfd78e.Emscripten.clang
The compiled smart contracts are written as JSON files in the
/src/build/contracts
directory. These are the stored ABI and other necessary metadata - the artifacts.Run migrations on the C-Chain
During the deployment of the smart contract to the C-Chain, there is a required deployment cost. This can be seen inside
truffle-config.js
. The HDWallet Provider will help us in deploying on Fuji C-Chain and the deployment cost will be supplied by the account whose mnemonic has been stored in the .env
file. Therefore, we need to fund the account before we are able to deploy.Fund your account
We need funds in our C-Chain address, as smart contracts on Avalanche are deployed on the C-Chain (Contract-Chain). This address can easily be found on the Avalanche Wallet dashboard. Avalanche network has 3 chains: X-Chain, P-Chain, and C-Chain. The address of all these chains can be found by switching tabs at the bottom of the division, where there is a QR code. So, switch to C-Chain and copy the address. Now fund your account using the Fuji testnet faucet by pasting your C-Chain address in the input field. Refer to the image below, to identify the address section.
Run Migrations
Everything is in place to run the Truffle migrations and now we can easily deploy the
Auction
contract with the command:truffle migrate --network fuji
For rapid development and testing, we can also deploy our contracts on a local network by using Ganache (Truffle's local blockchain simulation) with the command:
truffle migrate --network development
On successful execution of either of the above commands, we should expect to see similar output:
Starting migrations... ====================== > Network name: 'fuji' > Network id: 1 > Block gas limit: 8000000 (0x7a1200) 1_initial_migration.js ====================== Deploying 'Migrations' ---------------------- > transaction hash: 0x094a9c0f12ff3158bcb40e266859cb4f34a274ea492707f673b93790af40e9e9 > Blocks: 0 Seconds: 0 > contract address: 0x0b1f00d0Af6d5c864f86E6b96216e0a2Da111055 > block number: 40 > block timestamp: 1620393171 > account: 0x80599dd7F8c5426096FD189dcC6C40f47e8e3714 > balance: 39.71499696 > gas used: 173118 (0x2a43e) > gas price: 20 gwei > value sent: 0 ETH > total cost: 0.00346236 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.00346236 ETH 2_deploy_contracts.js ===================== Deploying 'AuctionManager' ------------------------ > transaction hash: 0xbeb13fc6bbee250eea9151faf02bfe247ec497294acc84c9b8319ed609ced086 > Blocks: 0 Seconds: 0 > contract address: 0xf30D372A6911CCF6BBa1e84c3CEd51cC0F3D7769 > block number: 42 > block timestamp: 1620393172 > account: 0x80599dd7F8c5426096FD189dcC6C40f47e8e3714 > balance: 39.69235442 > gas used: 1090212 (0x10a2a4) > gas price: 20 gwei > value sent: 0 ETH > total cost: 0.02180424 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.02180424 ETH Summary ======= > Total deployments: 2 > Final cost: 0.0252666 ETH
If you have not created an account on the C-Chain, you'll see this error:
Error: Expected parameter 'from' not passed to function.
If you have not funded the account, you'll see this error:
Error: *** Deployment Unsuccessful*** "Migrations" could not deploy due to insufficient funds * Account: 0x090172CD36e9f4906Af17B2C36D662E69f162282 * Balance: 0 wei * Message: sender doesn't have enough funds to send tx. The upfront cost is: 1410000000000000000 and the sender's account only has: 0 * Try: + Using an adequately funded account
The information and ABI of the deployed contract are present in the
src/build/contracts
directory as Auction.json
.Building the user interface
Our blockchain code, which will act as the backend for this application, is deployed on the blockchain and now we can make the client-side for interacting with the contracts. We will be using Trufflesuite's Drizzle library for connecting our web app with the blockchain. Drizzle makes the integration process very easy and scalable. It also provides a mechanism to cache a particular contract-call, so that we can get real-time updates of any changes to the data on the blockchain.
We will be using React's context API to facilitate this integration. The context API makes the use of variables which are declared in the parent component very easy to access in the child components.
It is based upon the Provider and Consumer concept. The Provider component contains necessary logic and variables that need to be passed along. Then this Provider component is wrapped around the components which want to access its variables. Every child component can access these variables. But in order to access them, we use a Consumer API. This API will return the variables from the Provider component (only when called from its child components). Look at the code below to understand what is happening.
In the
drizzleContext.js
file, DrizzleProvider is the provider component and useDrizzleContext is the consumer function. Look at the return statements of these functions. One is returning the Context Provider (provider), and the other is returning the values of the Context itself (consumer).Drizzle Option component
Create a file
drizzleOption.js
inside the drizzle-auction/client/src/
directory and paste the following code:import AuctionManager from "./build/contracts/AuctionManager.json"; const drizzleOptions = { contracts: [AuctionManager], }; export default drizzleOptions;
The
drizzleOptions
constant contains the configuration like contracts we want to deploy, our custom web3 provider, smart contract events, etc. Here we are just instantiating the AuctionManager
smart contract.Index component
Inside the file
index.js
in the src
directory, add the following code:import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import { Drizzle } from "@drizzle/store"; import drizzleOptions from "./drizzleOptions"; import { DrizzleProvider } from "./drizzleContext"; const drizzle = new Drizzle(drizzleOptions); ReactDOM.render( <DrizzleProvider drizzle={drizzle}> <App /> </DrizzleProvider>, document.getElementById("root") );
Importing
Drizzle
from the @drizzle/store
module will help in instantiating drizzle according to our drizzleOptions. The following line is responsible for this action:const drizzle = new Drizzle(drizzleOptions);
Then we wrap the
App
component inside the DrizzleProvider
so that we can use extracted variables (see drizzleContext.js) inside App
. We pass the drizzle
object to the provider component because it will be required to extract other information from it.Drizzle Context
Create a file
drizzleContext.js
inside the drizzle-auction/client/src/
directory with the following code:import React, { createContext, useContext, useState } from "react"; const Context = createContext(); export function DrizzleProvider({ drizzle, children }) { const [drizzleVariables, setDrizzleVariables] = useState({ initialized: false, state: null, web3: null, accounts: null, AuctionManager: null, subscriber: null, }); const unsubscribe = drizzle.store.subscribe(() => { const drizzleState = drizzle.store.getState(); if (drizzleState.drizzleStatus.initialized) { const { web3, accounts } = drizzleState; const AuctionManager = drizzle.contracts.AuctionManager.methods; const subscriber = drizzleState.contracts.AuctionManager; setDrizzleVariables({ state: drizzleState, web3, accounts, AuctionManager, subscriber, initialized: true, }); } }); drizzleVariables.initialized && unsubscribe(); return ( <Context.Provider value={{ drizzle, drizzleVariables }}> {children} </Context.Provider> ); } export function useDrizzleContext() { const context = useContext(Context); return context; }
The
DrizzleProvider
function takes drizzle
as its argument and extracts other information like whether drizzle contracts are initialized or not, web3 info, account info, deployed contract's instance, etc. We need to subscribe to the drizzle's store for this information because the data is not fetched at once, and since we do not know when will we get the data, we subscribe to the store (where the data resides). Once the drizzle is initialized with our contract data, we unsubscribe from the store, so that it will not re-render infinitely!Drizzle state
const drizzleState = drizzle.store.getState();
This variable holds the state of the store. Cached calls are those contract calls for which we want real-time data from the blockchain. Whenever there is some change in our data on the blockchain, it gets notified in the drizzle state variable of the store.
AuctionManager
const AuctionManager = drizzle.contracts.AuctionManager.methods;
drizzle.contracts
is an object containing instances of all the deployed contracts which are added to drizzle (in drizzleOptions or manually). We are simply storing all the methods of this contract instance so that whenever we want to call functions or public identifiers from this contract, we can simply use AuctionManager.method_name().call()
.App component
Now open
App.js
inside the drizzle-auction/client/src/
directory and add the following code:import Auction from "./Auction"; import { useDrizzleContext } from "./drizzleContext"; function App() { const { drizzleVariables } = useDrizzleContext(); if (!drizzleVariables.initialized) { return "Loading..."; } else { return <Auction />; } } export default App;
drizzleVariables.initialized
would ensure that, Loading...
state is visible until Drizzle is ready for interaction.Auction Component
Create a file
Auction.js
inside the drizzle-auction/client/src/
directory and add the following code. This component deals with the entry-point of our application, where all the data like userInfo
, AuctionLists
, AuctionDetails
etc. get generated.import React, { useState, useEffect } from "react"; import { useDrizzleContext } from "./drizzleContext"; import FetchAuctions from "./AuctionList"; import CreateAuction from "./CreateAuction"; function Auction() { // Importing drizzle variables from drizzle context const { drizzleVariables } = useDrizzleContext(); const { AuctionManager, subscriber, accounts } = drizzleVariables; // Setting up cache keys corresponding to cache calls const [cacheKeys, setCacheKey] = useState({ uId: null, aId: null, showAuctions: null, isRegistered: null, auctionAnalytics: [null], }); const [auctionAnalyticsCacheKey, setAuctionAnalyticsCacheKey] = useState(null); // Setting up cache calls for required functions const cacheCalls = { isRegistered: subscriber?.isRegistered[cacheKeys?.isRegistered]?.value, user: subscriber?.users[cacheKeys?.uId]?.value, aId: subscriber?.aId[cacheKeys?.aId]?.value, showAuctions: subscriber?.showAuctions[cacheKeys?.showAuctions]?.value, auctionAnalytics: [], }; for (var i = 0; i < cacheCalls.aId; i++) { cacheCalls.auctionAnalytics.push( subscriber?.auctionAnalytics[auctionAnalyticsCacheKey[i]]?.value ); } const [isRegistered, setIsRegistered] = useState(false); const [userInfo, setUserInfo] = useState({ id: null, name: null, }); // Initializing cache keys useEffect(() => { const _auctionCacheKey = AuctionManager?.showAuctions?.cacheCall(); const _aIdCacheKey = AuctionManager?.aId?.cacheCall(); const _isRegistered = AuctionManager?.isRegistered?.cacheCall(accounts[0]); setCacheKey({ ...cacheKeys, showAuctions: _auctionCacheKey, aId: _aIdCacheKey, isRegistered: _isRegistered, }); }, []); useEffect(() => { var _auctionAnalyticsCacheKey = []; for (var i = 0; i < cacheCalls.aId; i++) { _auctionAnalyticsCacheKey.push( AuctionManager?.auctionAnalytics?.cacheCall(i) ); } setAuctionAnalyticsCacheKey(_auctionAnalyticsCacheKey); }, [cacheCalls.aId]); useEffect(() => { if ( cacheCalls.isRegistered !== undefined && cacheCalls.isRegistered[0] == 1 ) { setIsRegistered(true); (async () => { const userInfo = await AuctionManager.users( cacheCalls.isRegistered[1] ).call(); setUserInfo({ id: userInfo.userId, name: userInfo.name, }); })(); } else { setIsRegistered(false); } }, [cacheCalls.isRegistered]); const createUser = async (name) => { await AuctionManager?.createUser(name)?.send({ from: accounts[0] }); }; const [userName, setUserName] = useState(""); const handleUserNameChange = (event) => { setUserName(event.target.value); }; const submitLogin = (event) => { event.preventDefault(); createUser(userName); }; const UserInfo = () => { return ( <div> <label style={{ color: "red" }}>ID: </label> {userInfo.id} <label style={{ marginLeft: "50px", color: "green" }}> Name:{" "} </label> {userInfo.name} <br /> <br /> </div> ); }; return ( <div> <h1>Auctions</h1> {isRegistered ? ( <> <UserInfo /> <FetchAuctions cacheCalls={cacheCalls} userInfo={userInfo} /> <br /> <br /> <CreateAuction /> </> ) : ( <form onSubmit={submitLogin}> <font color="red" font="2"> This address is not yet registered! </font> <br /> <br /> <label>Address: </label> <input disabled value={accounts[0]} /> <br /> <br /> <label>Name: </label> <input key="1" value={userName} required onChange={handleUserNameChange} placeholder="Enter your name" /> <br /> <br /> <input type="submit" value="Register" /> </form> )} </div> ); } export default Auction;
In order to keep our data fresh from the blockchain, Drizzle uses the caching mechanism. On our behalf, Drizzle keeps track of every change on the blockchain. If there is any transaction involving our smart contracts, then it will notify our dApp.
We need to define the calls which we want to monitor. Caching a particular method will provide cache keys (hash) to us. Each cached method is associated with a particular unique hash. Using this key, we can get live data from the blockchain. The component will re-render anytime there is some new value associated with this call.
For example, in the above code, we used the following cache keys
const [cacheKeys, setCacheKey] = useState({ uId: null, aId: null, showAuctions: null, isRegistered: null, auctionAnalytics: [null], });
Suppose we want to cache the
isRegistered
method. This can be done using:const _isRegistered = AuctionManager?.isRegistered?.cacheCall(accounts[0]); setCacheKey({ isRegistered: _isRegistered, });
Once a method is cached, the Drizzle
store
creates a key-value pair representing hash-key and the real-time data associated with this call. In the above program, this data is accessed using the subscriber
variable:const realTimeIsRegistered = subscriber?.isRegistered[cacheKeys?.isRegistered]?.value;
In this component, we made a simple object of cached call variables named
cacheCall
, which implements the above code snippet. The cached version of isRegistered
can be accessed as cacheCalls.isRegistered
.Auction List
Create a file
Auctionlist.js
inside the drizzle-auction/client/src/
directory, adding the following code. This component deals with the management of the auction like creating a new bid, displaying the real-time auction analytics, etc. All the data is passed by its parent component i.e. Auction.js
which manages the cache keys and calls.import React, { useState } from "react"; import { useDrizzleContext } from "./drizzleContext"; function FetchAuctions({ cacheCalls, userInfo }) { const { drizzleVariables } = useDrizzleContext(); const { AuctionManager, accounts } = drizzleVariables; const [bidPrices, setBidPrices] = useState(new Map([])); const createBid = async (id, bidPrice) => { await AuctionManager?.createBid(id, bidPrice).send({ from: accounts[0] }); clearBidPriceInput(id); }; const submitNewBid = (event, id) => { event.preventDefault(); createBid(id, bidPrices.get(id)); }; const handleBidPriceChange = (event, id) => { let _bidPrices = bidPrices; _bidPrices.set(id, event.target.value); setBidPrices(_bidPrices); }; const clearBidPriceInput = (id) => { let _bidPrices = bidPrices; _bidPrices[id] = ""; setBidPrices(_bidPrices); }; const getAuctionAnalytics = () => { var auctionAnalytics = []; for (var i = 0; i < cacheCalls.aId; i++) { auctionAnalytics.push(cacheCalls.auctionAnalytics[i]); } return auctionAnalytics; }; const allAuctions = cacheCalls.showAuctions; const auctionAnalytics = getAuctionAnalytics(); return ( <table border="1" style={{ maxWidth: "800px", width: "90%" }}> <tr> <td>Auction ID</td> <td>Auction Details</td> <td>Minimum Price</td> <td>Bid</td> </tr> {allAuctions !== undefined && allAuctions.map((auction, index) => ( <tr> <td>{auction.auctionId}</td> <td> <b> {auction.name}{" "} <font size="2" color="green"> {auction.userId == userInfo.id && "(created by you)"} </font> </b> <br /> <font size="2">{auction.description}</font> <br /> <tr> <td>Total Bids</td> <td>Latest Bid</td> <td>Highest Bid</td> <td>Lowest Bid</td> </tr> <tr> <td>{auctionAnalytics[index]?.auctionBidId}</td> <td>₹{auctionAnalytics[index]?.latestBid}</td> <td>₹{auctionAnalytics[index]?.highestBid}</td> <td> ₹ {auctionAnalytics[index]?.auctionBidId == 0 ? 0 : auctionAnalytics[index]?.lowestBid} </td> </tr> </td> <td>₹{auction.msp}</td> <td> <form onSubmit={(event) => submitNewBid(event, auction.auctionId)} style={{ margin: "10px" }} > <input required type="number" min={auction.msp} onChange={(event) => handleBidPriceChange(event, auction.auctionId) } placeholder="Enter your bid price" /> <br /> <br /> <input type="submit" value="Make Bid" /> <br /> <br /> </form> </td> </tr> ))} </table> ); } export default FetchAuctions;
Creating new Auctions
Create a file
CreateAuction.js
inside the drizzle-auction/client/src/
directory and add the following code. This component deals with creation of new Auctions, by submitting transactions on the network.import React, { useState } from "react"; import { useDrizzleContext } from "./drizzleContext"; function CreateAuction() { // Importing drizzle variables from drizzle context const { drizzleVariables } = useDrizzleContext(); const { AuctionManager, accounts } = drizzleVariables; const createAuction = async ({ title, description, msp }) => { await AuctionManager?.createAuction(title, description, msp)?.send({ from: accounts[0], }); setAuctionDetails({ title: "", description: "", msp: "", }); }; const [auctionDetails, setAuctionDetails] = useState({ title: "", description: "", msp: "", }); const handleAuctionTitleChange = (event) => { setAuctionDetails({ ...auctionDetails, title: event.target.value, }); }; const handleAuctionDescriptionChange = (event) => { setAuctionDetails({ ...auctionDetails, description: event.target.value, }); }; const handleAuctionMspChange = (event) => { setAuctionDetails({ ...auctionDetails, msp: event.target.value, }); }; const submitNewAuction = (event) => { event.preventDefault(); createAuction(auctionDetails); }; return ( <form onSubmit={submitNewAuction} style={{ border: "1px black solid", maxWidth: "400px", padding: "10px" }} > <label>Title: </label> <br /> <input value={auctionDetails.title} onChange={handleAuctionTitleChange} /> <br /> <br /> <label>Description: </label> <br /> <textarea rows="4" value={auctionDetails.description} onChange={handleAuctionDescriptionChange} /> <br /> <br /> <label>MSP: </label> <br /> <input value={auctionDetails.msp} onChange={handleAuctionMspChange} /> <br /> <br /> <input type="submit" value="Create Auction" /> </form> ); } export default CreateAuction;
Now go to the project root directory, i.e.
avalanche-voting
, and run the command npm start
. The React development server will start automatically. Visit http://localhost:3000 in a web browser to interact with the dApp frontend.Don't forget to set up Metamask with the Fuji testnet and also fund the account with Fuji C-Chain test tokens to be able to vote. In the Metamask extension, add a custom RPC endpoint by clicking the network dropdown in the centre of the extension. Fill in the details as shown in the table below.
Info | Value |
---|---|
Network Name | Avalanche Fuji |
New RPC URL | https://avalanche--fuji--rpc.datahub.figment.io/apikey/<YOUR_DATAHUB_API_KEY>/ext/bc/C/rpc |
Chain ID | 43113 |
Currency Symbol | AVAX-C |
Block Explorer URL |
Conclusion
We have successfully built a dApp through which we can organize auctions, bid in them and declare results, with both frontend and smart contracts. We have used the Drizzle library from Trufflesuite for integrating our frontend with the blockchain and to keep our data updated in real-time.
Next Steps
Our dApp currently has very minimalistic designs. We can use Consensys' Rimble UI library for adding modals for each transaction, add links to drip Avalanche's test tokens etc. which can help users to navigate through our dApp.