diff --git a/code/celo101-code-chapter_4/4-1-edit-functions/marketplace.sol b/code/celo101-code-chapter_4/4-1-edit-functions/marketplace.sol new file mode 100644 index 0000000..8ae35fc --- /dev/null +++ b/code/celo101-code-chapter_4/4-1-edit-functions/marketplace.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.0 <0.9.0; + +interface IERC20Token { + function transfer(address, uint256) external returns (bool); + function approve(address, uint256) external returns (bool); + function transferFrom(address, address, uint256) external returns (bool); + function totalSupply() external view returns (uint256); + function balanceOf(address) external view returns (uint256); + function allowance(address, address) external view returns (uint256); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +contract Marketplace { + + uint internal productsLength = 0; + address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1; + + struct Product { + address payable owner; + string name; + string image; + string description; + string location; + uint price; + uint sold; + } + + mapping (uint => Product) internal products; + + function writeProduct( + string memory _name, + string memory _image, + string memory _description, + string memory _location, + uint _price + ) public { + uint _sold = 0; + products[productsLength] = Product( + payable(msg.sender), + _name, + _image, + _description, + _location, + _price, + _sold + ); + productsLength++; + } + + function readProduct(uint _index) public view returns ( + address payable, + string memory, + string memory, + string memory, + string memory, + uint, + uint + ) { + return ( + products[_index].owner, + products[_index].name, + products[_index].image, + products[_index].description, + products[_index].location, + products[_index].price, + products[_index].sold + ); + } + + function buyProduct(uint _index) public payable { + require( + IERC20Token(cUsdTokenAddress).transferFrom( + msg.sender, + products[_index].owner, + products[_index].price + ), + "Transfer failed." + ); + products[_index].sold++; + } + + function editProduct(uint _index, + string memory _name, + string memory _image, + string memory _description, + string memory _location, + uint _price + ) public { + require(products[_index].owner == msg.sender, "You can not edit this product."); + products[_index].name = _name; + products[_index].image = _image; + products[_index].description = _description; + products[_index].location = _location; + products[_index].price = _price; + } + + function getProductsLength() public view returns (uint) { + return (productsLength); + } +} \ No newline at end of file diff --git a/code/celo101-code-chapter_4/4-2-delete-functions/marketplace.sol b/code/celo101-code-chapter_4/4-2-delete-functions/marketplace.sol new file mode 100644 index 0000000..0f26b7b --- /dev/null +++ b/code/celo101-code-chapter_4/4-2-delete-functions/marketplace.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.0 <0.9.0; + +interface IERC20Token { + function transfer(address, uint256) external returns (bool); + function approve(address, uint256) external returns (bool); + function transferFrom(address, address, uint256) external returns (bool); + function totalSupply() external view returns (uint256); + function balanceOf(address) external view returns (uint256); + function allowance(address, address) external view returns (uint256); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +contract Marketplace { + + uint internal productsLength = 0; + address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1; + + struct Product { + address payable owner; + string name; + string image; + string description; + string location; + uint price; + uint sold; + } + + mapping (uint => Product) internal products; + + function writeProduct( + string memory _name, + string memory _image, + string memory _description, + string memory _location, + uint _price + ) public { + uint _sold = 0; + products[productsLength] = Product( + payable(msg.sender), + _name, + _image, + _description, + _location, + _price, + _sold + ); + productsLength++; + } + + function readProduct(uint _index) public view returns ( + address payable, + string memory, + string memory, + string memory, + string memory, + uint, + uint + ) { + return ( + products[_index].owner, + products[_index].name, + products[_index].image, + products[_index].description, + products[_index].location, + products[_index].price, + products[_index].sold + ); + } + + function buyProduct(uint _index) public payable { + require( + IERC20Token(cUsdTokenAddress).transferFrom( + msg.sender, + products[_index].owner, + products[_index].price + ), + "Transfer failed." + ); + products[_index].sold++; + } + + function editProduct(uint _index, + string memory _name, + string memory _image, + string memory _description, + string memory _location, + uint _price + ) public { + require(products[_index].owner == msg.sender, "You can not edit this product."); + products[_index].name = _name; + products[_index].image = _image; + products[_index].description = _description; + products[_index].location = _location; + products[_index].price = _price; + } + + function deleteProduct(uint _index) public { + require(products[_index].owner == msg.sender, "You can not delete this product."); + delete products[_index]; + } + + function getProductsLength() public view returns (uint) { + return (productsLength); + } +} \ No newline at end of file diff --git a/code/celo101-code-chapter_4/4-3-implement-to-frontend/demo.png b/code/celo101-code-chapter_4/4-3-implement-to-frontend/demo.png new file mode 100644 index 0000000..0665853 Binary files /dev/null and b/code/celo101-code-chapter_4/4-3-implement-to-frontend/demo.png differ diff --git a/code/celo101-code-chapter_4/4-3-implement-to-frontend/index.html b/code/celo101-code-chapter_4/4-3-implement-to-frontend/index.html new file mode 100644 index 0000000..41db3c0 --- /dev/null +++ b/code/celo101-code-chapter_4/4-3-implement-to-frontend/index.html @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + Street Food Kigali + + + +
+ + + + + +
+ + Add product + +
+ +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/code/celo101-code-chapter_4/4-3-implement-to-frontend/main.js b/code/celo101-code-chapter_4/4-3-implement-to-frontend/main.js new file mode 100644 index 0000000..925c5e8 --- /dev/null +++ b/code/celo101-code-chapter_4/4-3-implement-to-frontend/main.js @@ -0,0 +1,274 @@ +import Web3 from "web3" +import { newKitFromWeb3 } from "@celo/contractkit" +import BigNumber from "bignumber.js" +import marketplaceAbi from "../contract/marketplace.abi.json" +import erc20Abi from "../contract/erc20.abi.json" + +const ERC20_DECIMALS = 18 +const MPContractAddress = "0x564b27A5499fa3fcd4B763Ae4120282fa0eF6De2" +const cUSDContractAddress = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1" + +let kit +let contract +let products = [] + +const connectCeloWallet = async function () { + if (window.celo) { + notification("⚠️ Please approve this DApp to use it.") + try { + await window.celo.enable() + notificationOff() + + const web3 = new Web3(window.celo) + kit = newKitFromWeb3(web3) + + const accounts = await kit.web3.eth.getAccounts() + kit.defaultAccount = accounts[0] + + contract = new kit.web3.eth.Contract(marketplaceAbi, MPContractAddress) + } catch (error) { + notification(`⚠️ ${error}.`) + } + } else { + notification("⚠️ Please install the CeloExtensionWallet.") + } +} + +async function approve(_price) { + const cUSDContract = new kit.web3.eth.Contract(erc20Abi, cUSDContractAddress) + + const result = await cUSDContract.methods + .approve(MPContractAddress, _price) + .send({ from: kit.defaultAccount }) + return result +} + +const getBalance = async function () { + const totalBalance = await kit.getTotalBalance(kit.defaultAccount) + const cUSDBalance = totalBalance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2) + document.querySelector("#balance").textContent = cUSDBalance +} + +const getProducts = async function() { + const _productsLength = await contract.methods.getProductsLength().call() + const _products = [] + for (let i = 0; i < _productsLength; i++) { + let _product = new Promise(async (resolve, reject) => { + let p = await contract.methods.readProduct(i).call() + resolve({ + index: i, + owner: p[0], + name: p[1], + image: p[2], + description: p[3], + location: p[4], + price: new BigNumber(p[5]), + sold: p[6], + }) + }) + _products.push(_product) + } + products = await Promise.all(_products) + renderProducts() +} + +function renderProducts() { + document.getElementById("marketplace").innerHTML = "" + products.forEach((_product) => { + const newDiv = document.createElement("div") + newDiv.className = "col-md-4" + if (_product.owner == "0x0000000000000000000000000000000000000000") return + newDiv.innerHTML = productTemplate(_product) + document.getElementById("marketplace").appendChild(newDiv) + }) +} + +function productTemplate(_product) { + let configProduct = "" + if (kit.defaultAccount === _product.owner) { + configProduct = ` + + + ` + } + return ` +
+
+ ... +
+ `+ configProduct +` +
+
+
+ ${_product.sold} Sold +
+
+
+ ${identiconTemplate(_product.owner)} +
+

${_product.name}

+

+ ${_product.description} +

+

+ + ${_product.location} +

+ +
+
+ ` +} + +function identiconTemplate(_address) { + const icon = blockies + .create({ + seed: _address, + size: 8, + scale: 16, + }) + .toDataURL() + + return ` +
+ + ${_address} + +
+ ` +} + +function notification(_text) { + document.querySelector(".alert").style.display = "block" + document.querySelector("#notification").textContent = _text +} + +function notificationOff() { + document.querySelector(".alert").style.display = "none" +} + +window.addEventListener("load", async () => { + notification("⌛ Loading...") + await connectCeloWallet() + await getBalance() + await getProducts() + notificationOff() +}); + +document + .querySelector("#newProductBtn") + .addEventListener("click", async (e) => { + const params = [ + document.getElementById("newProductName").value, + document.getElementById("newImgUrl").value, + document.getElementById("newProductDescription").value, + document.getElementById("newLocation").value, + new BigNumber(document.getElementById("newPrice").value) + .shiftedBy(ERC20_DECIMALS) + .toString() + ] + notification(`⌛ Adding "${params[0]}"...`) + try { + const result = await contract.methods + .writeProduct(...params) + .send({ from: kit.defaultAccount }) + } catch (error) { + notification(`⚠️ ${error}.`) + } + notification(`🎉 You successfully added "${params[0]}".`) + getProducts() + }) + +document.querySelector("#marketplace").addEventListener("click", async (e) => { + if (e.target.className.includes("buyBtn")) { + const index = e.target.id + notification("⌛ Waiting for payment approval...") + try { + await approve(products[index].price) + } catch (error) { + notification(`⚠️ ${error}.`) + } + notification(`⌛ Awaiting payment for "${products[index].name}"...`) + try { + const result = await contract.methods + .buyProduct(index) + .send({ from: kit.defaultAccount }) + notification(`🎉 You successfully bought "${products[index].name}".`) + getProducts() + getBalance() + } catch (error) { + notification(`⚠️ ${error}.`) + } + } + if (e.target.className.includes("delBtn")) { + const index = e.target.id + notification(`⌛ Waiting confirm delete..."${products[index].name}"...`) + try { + const result = await contract.methods + .deleteProduct(index) + .send({ from: kit.defaultAccount }) + notification(`🎉 You successfully delete "${products[index].name}".`) + getProducts() + getBalance() + } catch (error) { + notification(`⚠️ ${error}.`) + } + } +}) + +document + .querySelector("#editProductBtn") + .addEventListener("click", async (e) => { + const index = document.getElementById('editProductBtn').getAttribute('data-index') + const name = document.getElementById("editProductName").value + const imageUrl = document.getElementById("editImgUrl").value + const description = document.getElementById("editProductDescription").value + const location = document.getElementById("editLocation").value + const price = new BigNumber(document.getElementById("editPrice").value) + .shiftedBy(ERC20_DECIMALS) + .toString() + notification(`⌛ Edit "${name}"...`) + try { + const result = await contract.methods + .editProduct(index, name, imageUrl, description, location, price) + .send({ from: kit.defaultAccount }) + } catch (error) { + notification(`⚠️ ${error}.`) + } + notification(`🎉 You successfully edit "${name}".`) + getProducts() + }) + +document.getElementById('editModal').addEventListener('show.bs.modal', (e) => { + document.getElementById('editProductName').value = e.relatedTarget.dataset.name + document.getElementById('editImgUrl').value = e.relatedTarget.dataset.image + document.getElementById('editProductDescription').value = e.relatedTarget.dataset.description + document.getElementById('editLocation').value = e.relatedTarget.dataset.location + document.getElementById('editPrice').value = e.relatedTarget.dataset.price + document.getElementById('editProductBtn').setAttribute('data-index', e.relatedTarget.dataset.index) +}); \ No newline at end of file diff --git a/content/celo101_chapter_4.md b/content/celo101_chapter_4.md new file mode 100644 index 0000000..47bb7b7 --- /dev/null +++ b/content/celo101_chapter_4.md @@ -0,0 +1,312 @@ +In previous chapters, you learn how to read, write, buy product in marketplace. But this is a marketplace where buyer and seller exchange goods, the buyer need to edit their product such as price, name, description, etc ... And if they dont want to sell this product, they may need to delete it right ? In this section, you will learn how to create edit & delete function into smart contract and implement it to frontend + +## 4.1 Edit function + +Open a `marketplace.sol` contract you already write in chapter 2. +Create a function to let user edit a new value to their current product. Name it `editProduct`. You have to specific the type of parameters of the function. +You need an index as a parameter to specific which product will edit. And, you also need to add _name, _image, _description, and _location. They are all a string stored in memory and the price is an uint. + +The function use public method so that everyone can edit. But one thing important, only owner of product can edit their own product not everyone, so use `require` function to ensure that ([Learn more about error handling](https://docs.soliditylang.org/en/latest/control-structures.html#error-handling-assert-require-revert-and-exceptions)). + +This method prevent others user try to edit your product, it will display error message. Otherwise smartcontract excute edit function. + +```solidity + function editProduct(uint _index, + string memory _name, + string memory _image, + string memory _description, + string memory _location, + uint _price + ) public { + require(products[_index].owner == msg.sender, "You can not edit this product."); + products[_index].name = _name; + products[_index].image = _image; + products[_index].description = _description; + products[_index].location = _location; + products[_index].price = _price; + } +``` + +You're done with edit function. + +## 4.2 Delete funtion + +Next create a function to let user delete their product. Name it `deleteProduct`. You only need to pass an index as a parameter to specific which product will delete. ([Learn more about delete function](https://docs.soliditylang.org/en/v0.8.2/types.html?highlight=delete#delete)). + +* Notes if you use `delete products`, it will reset all members inside, so in this case `delete products[_index]` will delete the value store at _index. + +Like edit function, delete function use public method and use `require` function to ensure only owner of product can delete it not everyone. + +```solidity + function deleteProduct(uint _index) public { + require(products[_index].owner == msg.sender, "You can not delete this product."); + delete products[_index]; + } +``` + +You're done with delete function. + +## 4.3 Implement to frontend + +You will need to redeploy the marketplace.sol into celo blockchain and replace it with the new one you deploy. (You can watch it in in chapter 2). +``` + const MPContractAddress = "0x564b27A5499fa3fcd4B763Ae4120282fa0eF6De2" +``` +I already deploy it here: +https://alfajores-blockscout.celo-testnet.org/address/0x564b27A5499fa3fcd4B763Ae4120282fa0eF6De2/transactions + +In order to interact with your smart contract that is deployed in bytecode, you need an interface, you need to copy the new ABI (Application Binary Interface) and save it into `marketplace.abi.json` file of the contracts folder in your project. + +In this scenario, you continue use bootstrap and old code from chapter 3. +First open `index.html`, below add new product modal, you need to edit a modal that opens up when the user clicks on the Edit product button. This code is pretty long. + +```html + + + + +``` +### Display edit & delete button + +Open the main.js file inside the src folder of your project. In function ```productTemplate```, create a variable ```configProduct```, this variable is the HTML of button edit & delete. It has condition to check only of owner product will display these button + +```html + let configProduct = "" + if (kit.defaultAccount === _product.owner) { + configProduct = ` + + + ` + } +``` + +And modify the return ```productTemplate``` HTML + +```html +return ` +
+ +
+ ... +
+ `+ configProduct +` +
+
+ +
+ ${_product.sold} Sold +
+
+
+ ${identiconTemplate(_product.owner)} +
+

${_product.name}

+

+ ${_product.description} +

+

+ + ${_product.location} +

+ +
+
+ ` +``` + +### Hide deleted products + +Go to function `renderProducts` and add + +```javascript + if (_product.owner == "0x0000000000000000000000000000000000000000") return +``` + +When use function `delete` in smartcontract, it will set to 0, you need a condition to check if owner is valid so it won't display the deleted product. The complete code will be like this: + +```javascript +function renderProducts() { + document.getElementById("marketplace").innerHTML = "" + products.forEach((_product) => { + const newDiv = document.createElement("div") + newDiv.className = "col-md-4" + if (_product.owner == "0x0000000000000000000000000000000000000000") return + newDiv.innerHTML = productTemplate(_product) + document.getElementById("marketplace").appendChild(newDiv) + }) +} +``` + +### Call function editProduct + +To interact with ```editProduct``` function you should open a modal edit box and adapt a edit button event. Move to button of file main.js, and add event listen when user click on edit button to open modal box. + +```javascript +document.getElementById('editModal').addEventListener('show.bs.modal', (e) => { + document.getElementById('editProductName').value = e.relatedTarget.dataset.name + document.getElementById('editImgUrl').value = e.relatedTarget.dataset.image + document.getElementById('editProductDescription').value = e.relatedTarget.dataset.description + document.getElementById('editLocation').value = e.relatedTarget.dataset.location + document.getElementById('editPrice').value = e.relatedTarget.dataset.price + document.getElementById('editProductBtn').setAttribute('data-index', e.relatedTarget.dataset.index) +}); +``` + +When edit modal is display, user modify product and click submit button to interact with `editProduct` function, you should receive the product name and show them a notification about what is happening. + +```javascript +document + .querySelector("#editProductBtn") + .addEventListener("click", async (e) => { + const index = document.getElementById('editProductBtn').getAttribute('data-index') + const name = document.getElementById("editProductName").value + const imageUrl = document.getElementById("editImgUrl").value + const description = document.getElementById("editProductDescription").value + const location = document.getElementById("editLocation").value + const price = new BigNumber(document.getElementById("editPrice").value) + .shiftedBy(ERC20_DECIMALS) + .toString() + notification(`⌛ Edit "${name}"...`) + try { + const result = await contract.methods + .editProduct(index, name, imageUrl, description, location, price) + .send({ from: kit.defaultAccount }) + } catch (error) { + notification(`⚠️ ${error}.`) + } + notification(`🎉 You successfully edit "${name}".`) + getProducts() + }) +``` + +### Call function deleteProduct + +Move to ```document.querySelector("#marketplace")``` add event delete, if user click on delete button, call to `deleteProduct` function. Then send them a notification and render the products again, + +```javascript + if (e.target.className.includes("delBtn")) { + const index = e.target.id + notification(`⌛ Waiting confirm delete..."${products[index].name}"...`) + try { + const result = await contract.methods + .deleteProduct(index) + .send({ from: kit.defaultAccount }) + notification(`🎉 You successfully delete "${products[index].name}".`) + getProducts() + getBalance() + } catch (error) { + notification(`⚠️ ${error}.`) + } + } +``` + +Finally, you should display all notifications. If there is no error, call your getProducts function again to show the updated products in your DApp, and yourgetBalance function to show the updated balance after the user bought the product. + +![DEMO](../code/celo101-code-chapter_4/4-3-implement-to-frontend/demo.png)