|
| 1 | +# Web3 with ReactJS |
| 2 | + |
| 3 | +Lets create folders named *backend* and *frontend* |
| 4 | + |
| 5 | +create a react typescript app inside *frontend* folder |
| 6 | + |
| 7 | + > npx create-react-app my-app --template typescript -frontend |
| 8 | +
|
| 9 | +this will create a react project with following files and folder |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | +Before connecting frontend with web3 we need to create a smart contract |
| 14 | + |
| 15 | +now lets create a truffle project inside our backend folder |
| 16 | + |
| 17 | + > npm install -g truffle ( if not installed already) |
| 18 | + > truffle init |
| 19 | +
|
| 20 | +this will create a truffle project with following files and folder |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | +change truffle config, uncomment the following |
| 25 | + ```javascript |
| 26 | + networks: { |
| 27 | + development: { |
| 28 | + host: "127.0.0.1", // Localhost (default: none) |
| 29 | + port: 8545, // Standard Ethereum port (default: none) |
| 30 | + network_id: "*", // Any network (default: none) |
| 31 | + }, |
| 32 | + ``` |
| 33 | +
|
| 34 | + update solidity compiler version |
| 35 | + ```javascript |
| 36 | + compilers: { |
| 37 | + solc: { |
| 38 | + version: "0.8.6", // Fetch exact version from solc-bin (default: truffle's version) |
| 39 | + // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) |
| 40 | + // settings: { // See the solidity docs for advice about optimization and evmVersion |
| 41 | + optimizer: { |
| 42 | + enabled: false, |
| 43 | + runs: 200 |
| 44 | + }, |
| 45 | + // evmVersion: "byzantium" |
| 46 | + // } |
| 47 | + } |
| 48 | + }, |
| 49 | + |
| 50 | + ``` |
| 51 | + add the following to mention the build directory storing contracts ABI inside frontend's folder |
| 52 | + ```javascript |
| 53 | +{ |
| 54 | + contracts_directory: './contracts/', |
| 55 | + contracts_build_directory: '../frontend/src/abis/', |
| 56 | +} |
| 57 | + ``` |
| 58 | +
|
| 59 | +- create a file in contracts folder *Marketplace.sol* to write our smart contract |
| 60 | +- add the following code: |
| 61 | +
|
| 62 | +```javascript |
| 63 | +pragma solidity ^0.8.6; |
| 64 | + |
| 65 | +contract Marketplace { |
| 66 | + string public name; |
| 67 | + uint public productCount = 0; |
| 68 | + |
| 69 | + struct Product { |
| 70 | + uint id; |
| 71 | + string name; |
| 72 | + uint price; |
| 73 | + address payable owner; |
| 74 | + bool purchased; |
| 75 | +} |
| 76 | + mapping(uint => Product) public products; |
| 77 | + |
| 78 | + constructor(){ |
| 79 | + name = "Marketplace"; |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | +we are creating a contract with |
| 84 | +- a string variable *name*. |
| 85 | +- a uint variable *productCount* |
| 86 | +- a struct named Product |
| 87 | +- a map named products having *id* and *Product* as key value. |
| 88 | +
|
| 89 | +Next add tow functions to our code *createProduct*: creates a new product in blockchain and *purchaseProduct*: to make a transaction, transfering the ownership of the product to the buyer and pay the amount to the product creator. |
| 90 | +
|
| 91 | +```javascript |
| 92 | +function createProduct(string memory _name, uint _price) public { // Require a valid name |
| 93 | + require(bytes(_name).length > 0); |
| 94 | + // Require a valid price |
| 95 | + require(_price > 0); |
| 96 | + // Increment product count |
| 97 | + productCount ++; |
| 98 | + // Create the product |
| 99 | + // msg.sender is the address of the user creating the product. |
| 100 | + products[productCount] = Product(productCount, _name, _price,payable(msg.sender), false); |
| 101 | +} |
| 102 | + |
| 103 | + function purchaseProduct(uint _id) public payable { |
| 104 | + // Fetch the product |
| 105 | + Product memory _product = products[_id]; |
| 106 | + // Fetch the owner |
| 107 | + address payable _seller = _product.owner; |
| 108 | + // Make sure the product has a valid id |
| 109 | + require(_product.id > 0 && _product.id <= productCount); |
| 110 | + // Require that there is enough Ether in the transaction |
| 111 | + require(msg.value >= _product.price); |
| 112 | + // Require that the product has not been purchased already |
| 113 | + require(!_product.purchased); |
| 114 | + // Require that the buyer is not the seller |
| 115 | + require(_seller != msg.sender); |
| 116 | + // Transfer ownership to the buyer |
| 117 | + _product.owner = payable(msg.sender); |
| 118 | + // Mark as purchased |
| 119 | + _product.purchased = true; |
| 120 | + // Update the product |
| 121 | + products[_id] = _product; |
| 122 | + // Pay the seller by sending them Ether |
| 123 | + _seller.transfer(msg.value); |
| 124 | +} |
| 125 | +``` |
| 126 | +Finally add the events to the code |
| 127 | +
|
| 128 | +```javascript |
| 129 | +event ProductCreated( |
| 130 | + uint id, |
| 131 | + string name, |
| 132 | + uint price, |
| 133 | + address payable owner, |
| 134 | + bool purchased |
| 135 | +); |
| 136 | + |
| 137 | +event ProductPurchased( |
| 138 | + uint id, |
| 139 | + string name, |
| 140 | + uint price, |
| 141 | + address payable owner, |
| 142 | + bool purchased |
| 143 | +); |
| 144 | + |
| 145 | +``` |
| 146 | +and add the emitter to the respective functions |
| 147 | +
|
| 148 | +```javascript |
| 149 | + emit ProductCreated(productCount, _name, _price, payable(msg.sender), false); |
| 150 | + ``` |
| 151 | + in the createProduct function, after product is created successfully and |
| 152 | + ```javascript |
| 153 | + emit ProductPurchased(productCount, _product.name, _product.price, payable(msg.sender), true); |
| 154 | + ``` |
| 155 | + in the purchaseProduct function, after transaction is successfull |
| 156 | +
|
| 157 | +now lets complile the code |
| 158 | +> truffle compile |
| 159 | +
|
| 160 | +Finally we need to deploy this smart contract to Ganache |
| 161 | +> create a file *2_deploy_contracts.js* in migrations folder |
| 162 | +
|
| 163 | +This file tells Truffle to to deploy our smart contract to the blockchain, numbered to order the run. |
| 164 | +
|
| 165 | + add the following : |
| 166 | +```javascript |
| 167 | +const Marketplace = artifacts.require("Marketplace"); |
| 168 | + |
| 169 | +module.exports = function(deployer) { |
| 170 | + deployer.deploy(Marketplace); |
| 171 | +}; |
| 172 | + |
| 173 | +``` |
| 174 | +Now run |
| 175 | + > truffle migrate --reset |
| 176 | +
|
| 177 | + # Smart contract tests |
| 178 | +
|
| 179 | + Create a file * Marketplace.test.js* for the smart contract tests in the *tests* folder: |
| 180 | +
|
| 181 | + install the dependencies for testing. |
| 182 | + > npm i chai chai-as-promised |
| 183 | +
|
| 184 | +add tools to our test suite |
| 185 | +
|
| 186 | +```javascript |
| 187 | +require('chai') |
| 188 | +.use(require('chai-as-promised')) |
| 189 | +.should() |
| 190 | + |
| 191 | +``` |
| 192 | +
|
| 193 | +we have created tests for the following: |
| 194 | +- Contract is deployed successfully |
| 195 | +- Products are created |
| 196 | +- Products are listing as expected |
| 197 | +- Products are sold with proper transaction |
| 198 | +
|
| 199 | +Now test them. |
| 200 | +> truffle test |
| 201 | +
|
| 202 | +lets compile with |
| 203 | +
|
| 204 | +> truffle migrate --reset |
| 205 | +
|
| 206 | + |
| 207 | +
|
| 208 | +
|
| 209 | +# Now move on to frontend |
| 210 | +
|
| 211 | + install metamask to your browser |
| 212 | +
|
| 213 | + Download Ganache from https://www.trufflesuite.com/ganache |
| 214 | +
|
| 215 | +Connect Metamask to our Ganache personal blockchain instance following the steps |
| 216 | +- start Ganache |
| 217 | +
|
| 218 | +- click on new workspace |
| 219 | + |
| 220 | +
|
| 221 | +- go to server section |
| 222 | + |
| 223 | +
|
| 224 | +- change port number to 8545 |
| 225 | + |
| 226 | +
|
| 227 | +- now click on save workspace |
| 228 | + |
| 229 | +
|
| 230 | +- then open metamask in your browser |
| 231 | +- create a new account |
| 232 | +
|
| 233 | +- click on ethereum mainet |
| 234 | + |
| 235 | +
|
| 236 | +- in the dropdown menu |
| 237 | +- select localhost:8545 |
| 238 | + |
| 239 | +
|
| 240 | +- now you are connected |
| 241 | +
|
| 242 | +- go to ganache and choose any account you want and click on key |
| 243 | + |
| 244 | +
|
| 245 | +- select the whole key and copy it |
| 246 | + |
| 247 | +
|
| 248 | +- go to metamask |
| 249 | +- click on the account icon |
| 250 | + |
| 251 | +
|
| 252 | +- then select import account from dropdown menu |
| 253 | + |
| 254 | +
|
| 255 | +- paste the copied key |
| 256 | +- and click import |
| 257 | + |
| 258 | +
|
| 259 | +- you should see the ethereum balance (note that this ethereum has no value, its just for testing) |
| 260 | +
|
| 261 | +## Client Side application |
| 262 | +
|
| 263 | +install web3.js |
| 264 | +
|
| 265 | +> npm i web3 |
| 266 | +
|
| 267 | +install typechain for generating types of contract |
| 268 | +
|
| 269 | +> npm i typechain @typechain/web3-v1 |
| 270 | +
|
| 271 | +- add the scripts for generating types |
| 272 | +```json |
| 273 | + "scripts": { |
| 274 | + "start": "react-scripts start", |
| 275 | + "build": "react-scripts build", |
| 276 | + "test": "react-scripts test", |
| 277 | + "eject": "react-scripts eject", |
| 278 | + "generate-types": "typechain --target=web3-v1 \"./src/abi/*.json\"" , |
| 279 | + "postinstall": "yarn generate-types" |
| 280 | + } |
| 281 | + |
| 282 | + ``` |
| 283 | +- generate types by running command *npm generate-types* |
| 284 | +this will create types folder inside src directory |
| 285 | +
|
| 286 | +Start coding, add the following code in App.js |
| 287 | +
|
| 288 | +```javascript |
| 289 | +import Web3 from 'web3' |
| 290 | +``` |
| 291 | +
|
| 292 | +- instantiate web3. |
| 293 | +
|
| 294 | +```javascript |
| 295 | + const loadWeb3 = async () => { |
| 296 | + if (window.ethereum) { |
| 297 | + window.web3 = new Web3(window.ethereum); |
| 298 | + try{ |
| 299 | + // Request account access if needed |
| 300 | + await window.ethereum.enable(); |
| 301 | + }catch (error) { |
| 302 | + window.alert( |
| 303 | + "User denied account access...!" |
| 304 | + ); |
| 305 | + } |
| 306 | + }else if (window.web3) { |
| 307 | + window.web3 = new Web3(window.web3.currentProvider); |
| 308 | + } else { |
| 309 | + window.alert( |
| 310 | + "Non-Ethereum browser detected. You should consider trying MetaMask!" |
| 311 | + ); |
| 312 | + } |
| 313 | + }; |
| 314 | + |
| 315 | +``` |
| 316 | +Once web3 is initialized, lets get the data from smart contract, |
| 317 | +
|
| 318 | +```javascript |
| 319 | + const loadBlockchainData = async () => { |
| 320 | + const web3: Web3 = window.web3; |
| 321 | + const accounts = await web3.eth.getAccounts(); |
| 322 | + try { |
| 323 | + //Read the networkID to determine the network |
| 324 | + const networkId : number= await web3.eth.net.getId(); |
| 325 | + const netId = networkId as unknown as keyof typeof Marketplace.networks |
| 326 | + const networkData = Marketplace.networks[netId]; |
| 327 | + if (networkData) { |
| 328 | + //instantiate the smart contract |
| 329 | + const marketContract = await new web3.eth.Contract( |
| 330 | + Marketplace.abi as AbiItem[], |
| 331 | + networkData.address |
| 332 | + ) as unknown as MPType |
| 333 | + |
| 334 | + const productCount = await marketContract.methods.productCount().call() as unknown as number |
| 335 | + // Load products |
| 336 | + let productarray: Product[] = []; |
| 337 | + for (var i = 1; i <= productCount; i++) { |
| 338 | + const product : Product = await marketContract.methods.products(i).call(); |
| 339 | + productarray.push(product); |
| 340 | + }} else { |
| 341 | + window.alert("Marketplace contract not deployed to detected network."); |
| 342 | + }}catch (error) { |
| 343 | + window.alert("network not detected."); |
| 344 | + }}; |
| 345 | +``` |
| 346 | +We need 2 functions to call smart contract methods |
| 347 | +### CreateProduct |
| 348 | +
|
| 349 | +```javascript |
| 350 | + const createProduct = async(name: string, price: number) => { |
| 351 | + if(!marketplace){ |
| 352 | + return |
| 353 | + } |
| 354 | + try { |
| 355 | + await marketplace.methods |
| 356 | + .createProduct(name, price) |
| 357 | + .send({ from: account }) |
| 358 | + .on("error", function (error: Error) { |
| 359 | + window.alert(error.message); |
| 360 | + }); |
| 361 | + await loadBlockchainData() |
| 362 | + } catch (error) { |
| 363 | + window.alert(error.message); |
| 364 | + } }; |
| 365 | +``` |
| 366 | +
|
| 367 | +### PurchaseProduct |
| 368 | +```javascript |
| 369 | + const purchaseProduct = async(id: string, price: number) => { |
| 370 | + let idn = Number(id); |
| 371 | + if(!marketplace){ |
| 372 | + return |
| 373 | + } |
| 374 | + try { |
| 375 | + marketplace.methods |
| 376 | + .purchaseProduct(idn) |
| 377 | + .send({ from: account, value: price }) |
| 378 | + .on("error", function (error: Error) { |
| 379 | + window.alert(error.message); |
| 380 | + }); |
| 381 | + await loadBlockchainData() |
| 382 | + } catch (error) { |
| 383 | + window.alert(error.message); |
| 384 | + }}; |
| 385 | +``` |
| 386 | +
|
| 387 | +
|
0 commit comments