Skip to content

Instantly share code, notes, and snippets.

@khandar-william
Last active December 8, 2018 03:55
Show Gist options
  • Save khandar-william/ab11b6453b919b479ecef3cb7a2a9f5c to your computer and use it in GitHub Desktop.
Save khandar-william/ab11b6453b919b479ecef3cb7a2a9f5c to your computer and use it in GitHub Desktop.

Brown Bag Ethereum Notes

Prerequisite:

  1. Install ethereum dan solc

    sudo apt-get install software-properties-common
    sudo add-apt-repository -y ppa:ethereum/ethereum
    sudo apt-get update
    sudo apt-get install ethereum solc
    
  2. Prepare directory

    mkdir project
    cd project
    mkdir node1
    mkdir source
    

Step by Step: Create Private Network:

  1. Create genesis.json

    {
      "config": {
        "chainId": 1907,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0
      },
      "difficulty": "10",
      "gasLimit": "2100000",
      "alloc": {}
    }
    • chainId: unique id for publicly identifiable network
      • jangan sampai private network lu conflict dengan ID network orang lain
    • difficulty: mining difficulty, makin tinggi makin lambat bikin block
    • alloc: initial saldo akun2, yg ga ditulis dianggap 0
    • homesteadBlock, eip155Block, eip158Block: obsolete fields
  2. Initialize filesystem storage

    • // bikin ethereum network di "directory ini" pakai "file genesis ini"
    geth --datadir node1 init genesis.json
    > INFO [09-01|10:16:28.870] Writing custom genesis block
    > INFO [09-01|10:16:28.872] Successfully wrote genesis state         database=chaindata                                                        hash=649448…9ebc29
    
  3. Launch the network and interact directly *****

    • // This is an interactive javascript console
    • // networkid is just some random number
    geth --datadir node1 --networkid 98765 console --rpc
    > Welcome to the Geth JavaScript console
    
    • // Sample output
    > eth.
    eth._requestManager            eth.defaultAccount             eth.getBlockNumber             eth.getMining                  eth.getTransactionCount        eth.isSyncing                  eth.sendTransaction
    eth.accounts                   eth.defaultBlock               eth.getBlockTransactionCount   eth.getPendingTransactions     eth.getTransactionFromBlock    eth.mining                     eth.sign
    eth.blockNumber                eth.estimateGas                eth.getBlockUncleCount         eth.getProtocolVersion         eth.getTransactionReceipt      eth.namereg                    eth.signTransaction
    eth.call                       eth.filter                     eth.getCode                    eth.getRawTransaction          eth.getUncle                   eth.pendingTransactions        eth.submitTransaction
    eth.coinbase                   eth.gasPrice                   eth.getCoinbase                eth.getRawTransactionFromBlock eth.getWork                    eth.protocolVersion            eth.submitWork
    eth.compile                    eth.getAccounts                eth.getCompilers               eth.getStorageAt               eth.hashrate                   eth.resend                     eth.syncing
    eth.constructor                eth.getBalance                 eth.getGasPrice                eth.getSyncing                 eth.iban                       eth.sendIBANTransaction
    eth.contract                   eth.getBlock                   eth.getHashrate                eth.getTransaction             eth.icapNamereg                eth.sendRawTransaction
    > personal.
    personal._requestManager personal.deriveAccount   personal.getListAccounts personal.importRawKey    personal.listWallets     personal.newAccount      personal.sendTransaction personal.signTransaction
    personal.constructor     personal.ecRecover       personal.getListWallets  personal.listAccounts    personal.lockAccount     personal.openWallet      personal.sign            personal.unlockAccount
    > web3.
    
  4. Create new account on the network

    • // Fun fact: ketika ada akun baru dicreate, ga ada announcement ke network

    • // Network ga tau keberadaan suatu akun selama akun tersebut ga pernah melakukan transaksi

    • // Jadi kalian bisa salah ngirim ether ke address yg belum ada dan tetap dianggap valid transaction

    • // Create new wallet, MUST REMEMBER YOUR OWN PASSWORD, also remember the address (randomly generated)

    personal.newAccount('asdf')
    > "0xa765971aa7f37175bee6ddcc7977c90816ae7272"
    personal.newAccount("asdf")
    > "0x64cffa39c380be8797e0783272602728d7b1949d"
    
    • // Display balance (show the balance of the first account in ether unit)
    web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
    > 0
    
  5. Mine (raining money!!! tapi bohongan...)

    • // Lakukan mining ke "akun" ini, hati2 kalo salah masukin address, orang lain yg dapat
    miner.setEtherbase(eth.accounts[0])
    
    • // start mining with 1 thread
    miner.start(1)
    > INFO [09-01|15:35:34.006] Successfully sealed new block            number=2 sealhash=bcff3d…794f71 hash=55ff6f…222f94 elapsed=6.411s
    > INFO [09-01|15:35:34.007] 🔨 mined potential block                  number=2 hash=55ff6f…222f94
    > INFO [09-01|15:35:34.007] Commit new mining work                   number=3 sealhash=576bb7…6b816e uncles=0 txs=0 gas=0 fees=0 elapsed=123.45µs
    > INFO [09-01|11:30:12.424] 🔗 block reached canonical chain          number=65 hash=de032d…d5ca19
    miner.stop()
    
  6. Let's check our balance now (RICH!)

    • // cek saldo
    web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
    > 20
    
  7. Let's send some ether

    • // Account itu mode defaultnya adalah "locked", tidak boleh melakukan transaksi yg akan mengurangi saldo
    • // Jadi, sebelum melakukan transaksi, perlu di-unlock dulu
    • // unlock akun pertama
    personal.unlockAccount(eth.accounts[0], "asdf")
    
    • // Cek saldo sebelum dan sesudah, make sure miner ada yg jalan
    eth.getBalance(eth.accounts[1])
    > 0
    
    • // Send 3 ether to second account
    eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(3,"ether")})
    > INFO [09-01|17:24:39.151] Submitted transaction                    fullhash=0x5cda91a396ddd920a35ccc82ced3816981a40883d2ae8ff12f2eec673c924389 recipient=0x64cffA39c380be8797E0783272602728D7b1949d
    miner.start(1) ... miner.stop()
    eth.getBalance(eth.accounts[1])
    > 3000000000000000000
    
    • // getBalance itu satuannya wei, 1 ether = 1.000.000.000.000.000.000 wei = 10^18 wei
    web3.fromWei(eth.getBalance(eth.accounts[1]), "ether")
    > 3
    

Step by Step: Create Smart Contract

  1. Write the source code of the contract (Solidity language)

    // source/greeter.sol
    contract mortal { ... }
    
    • // behavior contract greeter: string apapun yg dikasih di constructor, akan direturn
  2. Compile the source code

    • // --bin dan --abi artinya "generate file .bin dan .abi"
    solc --bin --abi  -o source source/greeter.sol --overwrite
    
    • // Sekarang ada file2 .abi dan .bin, tiap 1 contract
  3. Create deploy code

    • // Create JS code that will deploy the compiled contract
    // source/deploy.js
    var myContract = web3.eth.contract(/* File .abi here */);
    personal.unlockAccount(eth.accounts[0], "asdf");
    var newContract = myContract.new(
      /* constructor params here */,
      {
        from: eth.accounts[0],
        data: '0x /* File .bin here */',
        gas: '1000000' // make sure it's enough
      },
      function (e, contract) {
        console.log(e, contract);
        if (typeof contract.address !== 'undefined') {
          console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
        }
      }
    );
  4. Execute the deploy code

    • // Masuk interactive console
    loadScript('source/deploy.js')
    >  INFO [09-01|14:11:33.575] Submitted contract creation              fullhash=0x3e13f9e017c98233e33912014772db1929b1c4c7c2246e149800dd6edb5649cd contract=0x2CB1fD00f34193d8f783F931e2294398bEf0aBA0
    miner.start(1) ... miner.stop()
    > Contract mined! address: 0xa93e210e892ee40d74d90c90e481237712d92267 transactionHash: 0x450013780871ab37874971265cc1ada447f6045eadd81525cbf4270bff404745
    
  5. Interact with the contract via newly created variable

    • // We accessing the "cache" of our network
    newContract.address
    > "0xa93e210e892ee40d74d90c90e481237712d92267"
    newContract.greet()
    > "HELLO WARLD!"
    
  6. Call a contract method that modifies state

    • // Manggil fungsi2 contract itu biasanya perlu "bayar" kecuali kalau tipenya "constant/view/pure"
    • // Bayarnya pakai ether, istilahnya "gas", gunanya sebagai "kompensasi" bagi miner2
    • // Makanya "calling a function" itu menjadi "send transaction"
    newContract.change.sendTransaction('AAAA', {from: eth.accounts[0]})
    newContract.change("baru", {from:eth.accounts[0]})
    > INFO [09-01|17:55:52.267] Submitted transaction                    fullhash=0x557596df0ed984dd90baeae68de14952116e70768ff097053305764c0651a8ea recipient=0x33b35900881bE2dd70f4282bA2246b15F3d8A42c
    miner.start(1) ... miner.stop()
    
    • // Check if changed
    newContract.greet()
    > "AAAA"
    
  7. Call an existing contract

    • // What if you want to call another contract that's not created by you
    • // You need:
    • // 1. the contract address, 0xe95c8554b7862486b306d1100ebfd5cb102a2a28
    • // 2. the contract ABI, Simple.abi
    var existingContract = web3.eth.contract(/* ABI \*/).at('0xe95c8554b7862486b306d1100ebfd5cb102a2a28')
    
    • // Warning: kalau salah address ga ada error message...
    existingContract.multiply(99, 99)
    > 9801
    > 0 -> kalo contract ga ada, akan dapat default value, .multiply() return int, jadi 0
    

Step by Step Adding more Nodes

  1. Cek address network yg pertama

    admin.nodeInfo
    > enode: "enode://604956ca684..."
    
    • // Pastikan pakai yg ...@127.0.0.1 !!! (toh laptop yg sama, lebih pasti bisa nyambung)
  2. Create new directory and accounts

    • // use the same genesis.json
    mkdir node2
    geth --datadir node2 account new
    geth --datadir node2 init genesis.json
    
  3. Connect to other node

    geth --datadir node2 --networkid 98765 --port 30304 --bootnodes "enode://4e6d70f5149006bc35ed7021facb9ae50ca0076627a1566f1424a1b96c2d4251d272cee363c950a611b6981b1555bb8654746727ba305f77792b5b2a377021f3@127.0.0.1:30303" console
    > INFO [09-01|18:59:13.028] Block synchronisation started
    > INFO [09-01|18:59:13.072] Imported new state entries               count=17 elapsed=234.263µs processed=17 pending=0 retry=0 duplicate=0 unexpected=0
    
  4. Send some ethers to this new account

    • // To prove we are in the same network
    (node 1) eth.sendTransaction({from: eth.accounts[0], to: '0x4c8c3fd34970e2bd04315294aca6b94117d47174', value: web3.toWei(15,"ether")})
    (node 1) miner.start(1) ... miner.stop()
    (node 2) web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
    > 15
    

Step by Step: Create dApp

  1. "dApp" = conventional application (e.g. website) yg connect ke Ethereum

  2. Write the source code of the contract (Solidity language)

    // source/FoodCart.sol
    contract FoodCart { ... }
    
  3. Compile the source code

    solc --bin --abi  -o source source/FoodCart.sol --overwrite
    
  4. Create deploy code

    // source/deploy-FoodCart.js
    ... // see above deploy.js
    
  5. Execute the deploy code

    • // Masuk interactive console (remember CORS)
    geth --datadir node1 --networkid 98765 console --rpc  --rpccorsdomain "*"
    
    • // "rpccorsdomain" itu untuk CORS
    loadScript('source/deploy-FoodCart.js')
    > INFO [11-13|19:49:01.210] Submitted contract creation              fullhash=0x73f04bb43a5637ee8cbc1d8d1ed9f3c1d945dc51ee6680e6794bfa343f98f6cc contract=0x3cec03fe7c97bfa9139ab3ff0c0edd2593c34576
    miner.start(1) ... miner.stop()
    > Contract mined! address: 0x3cec03fe7c97bfa9139ab3ff0c0edd2593c34576 transactionHash: 0x73f04bb43a5637ee8cbc1d8d1ed9f3c1d945dc51ee6680e6794bfa343f98f6cc
    
  6. Sebelum bikin website, coba interaksi dengan contract tadi lewat console

    newContract.foodItems(0)
    newContract.fetchFoodItem(0)
    newContract.addFoodItem.sendTransaction('MADAGASCAR', 100, {from: eth.accounts[0]})
    newContract.addFoodItem('Emas', 999, {from: eth.accounts[0]})
    // addFoodItem akan gagal tanpa "from" karena perlu bayar
    // fetchFoodItem ga perlu bayar karena ada "view"
    miner.start(1) ... miner.stop()
    newContract.foodItems(0)
    newContract.fetchFoodItem(0)
    newContract.buyFoodItem(0, {from: eth.accounts[0], value: '200'})
    web3.eth.getTransactionReceipt('/* fullhash */')
    miner.start(1) ... miner.stop()
    web3.eth.getTransactionReceipt('/* fullhash */')
    newContract.foodItems(0)
    newContract.fetchFoodItem(0)
    
    • // .foodItems adalah global variable, tipenya "mapping"

    • // .fetchFoodItems adalah public method

    • // Aneh: muncul "Error: new BigNumber() not a base 16 number" setelah buyFoodItem

    • // Known unresolved web3js bug: web3/web3.js#434

    • // Ga ada cara gampang untuk "do something ketika transaksi gw X selesai di-mined"

  7. Code the initial website

    • // Install simple http server
    sudo npm install -g http-server
    
    • // prepare the website directory
    mkdir dapp
    cd dapp
    http-server
    
    • // Write initial files
    // dapp/index.html
    <!DOCTYPE html>
    <html>
    <head>
      <title>Hello World DApp</title>
      <link rel="stylesheet" href="./pure.css" />
      <!-- <script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script> -->
      <script src="./web3.js"></script>
      <script src="./jquery-3.3.1.slim.min.js"></script>
    </head>
    <body>
      <script src="./index.js"></script>
    </body>
    </html>
    • // downloaded jquery, old friend
    // dapp/index.js
    // from the interactive console: `HTTP endpoint opened                     url=http://127.0.0.1:8545`
    var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
    var abi = /* Put .abi here */;
    var FoodCartContract = web3.eth.contract(abi);
    // must remember the contract deployed address: `contract=0x3cec03fe7c97bfa9139ab3ff0c0edd2593c34576`
    var contractInstance = FoodCartContract.at('/* Contract address here 0x... */');
    
    // test something
    console.log(contractInstance.foodItems);
    Access to XMLHttpRequest at 'http://localhost:8545/' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
    
    • // Apparently this is another unsolved issue lol: web3/web3.js#1802
    • // For now, just edit that code (or, use v0.20.6)
    // web3.js
    request.withCredentials = this.user && this.password;
    • // If we open the browser console, we can interact with the web3 just like in the interactive console
    • // (Remember to unlock the account first)
    web3.eth.accounts
    contractInstance.foodItems(0).toString()
    > ",0,0,0,false"
    contractInstance.addFoodItem('Emas', 999, {from: web3.eth.accounts[0]})
    // in interactive console, there's a log "Submitted transaction ..."
    contractInstance.foodItems(0).toString()
    > "Emas,0,999,0,true"
    contractInstance.skuCount
    > f () {...}
    contractInstance.skuCount()
    > BigNumber {...}
    contractInstance.skuCount().toNumber()
    > 5
    let count2 = contractInstance.skuCount().toNumber();
    while (count2--) {console.log(contractInstance.fetchFoodItem(count2));};
    • // unint8 itu menjadi class BigNumber di js, BUKAN NUMBER!!!!
    • // BigNumber(5) + 1 = "51" -> ini geblek, hati2!
  8. Display the list of items

    // dapp/index.html
    <body>
      <h1 style="text-align: center">M A G I C</h1>
      <table id="food-items" class="pure-table" style="margin:auto">
        <thead>
          <tr>
            <th>SKU</th>
            <th>Name</th>
            <th>Price</th>
            <th>State</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody id="food-items-body">
    
          <tr id="food-items-add">
            <td>#</td>
            <td><input id="new-name" /></td>
            <td><input id="new-price" /></td>
            <td>-</td>
            <td><button type="button" id="button-add-item">Add</button></td>
          </tr>
        </tbody>
      </table>
      <script src="./index-updated.js"></script>
    </body>
  9. Make "Buy" button works

    • // See updated file "dapp/index-updated.js"

Questions:

  1. What is a "bootnode"?

    • an ethereum node
  2. What is "geth.ipc" file?

    • file yg akan menjadi sumber komunikasi dengan interactive console
  3. Di dalam genesis bisa allocate saldo awal, tapi kan address itu random, gimana cara claimnya?

  4. Ada "chainId" apa aja?

    • di public network, ini chainId yg sudah ter-define/reserved
      • 0: Olympic; Ethereum public pre-release testnet
      • 1: Frontier; Homestead, Metropolis, the Ethereum public main network
      • 1: Classic; The (un)forked public Ethereum Classic main network, chain ID 61
      • 1: Expanse; An alternative Ethereum implementation, chain ID 2
      • 2: Morden; The public Ethereum testnet, now Ethereum Classic testnet
      • 3: Ropsten; The public cross-client Ethereum testnet
      • 4: Rinkeby: The public Geth Ethereum testnet
      • 42: Kovan; The public Parity Ethereum testnet
      • 77: Sokol; The public POA testnet
      • 99: POA; The public Proof of Authority Ethereum network
    • sisanya unclaimed, bisa kalian pakai semaunya
  5. What is .bin file?

    • EVM file, indicated by the *.bin extension.
    • hexadecimal
    • This corresponds to the contract bytecode generated by the web-based compiler, which is easier to use and will be described below.
  6. What is .abi file?

    • ABI file, indicated by an *.abi extension.
    • formatnya json
    • An application binary interface is like an outline or template of your contract and helps you and others interact with the contract when it’s live on the blockchain.
  7. What happen if we put inaccurate .abi or wrong contract address in the script?

    • Will get error messages when doing operations
    • And you don't know the cause is wrong contract interface just from the error messages
    • That's the thing, you just have to manually make sure that you are using:
      • the correct .abi
      • the correct contract address
  8. Contract instantiation var contractInstance = Contract.at('...ADDRESS...'):

    • What if we put the wrong address?
    • Because the web3 relies on .abi, your code will "appearantly" work as usual
    • e.g. Calling .butFoodItem() will create new transaction on blockchain
    • but no error message apparent
    • your contract would simply "does not work" without any explicit error
  9. Some frustration met while making this:

    • I purposefully not using the Truffle framework
    • buggy smart contract: "new BigNumber() not a base 16 number"
      • this happens to the "uint16 price" field, after calling "buyFoodItem", which doesn't make sense because that field is not changed
      • so I changed it to uint8
      • UPDATE: ternyata bukan gara2 uint16, tapi gara2 "buyFoodItem" ngeset foodItemExists = false
      • sehingga fetchFoodItem jadi ngembaliin empty value
    • solc was upgraded to v0.5.0 and it's not backward compatible (codes from tutorial are for v0.4.x)
    • no easy way to receive error message when something doesn't work
      • using wrong contract address? no error
      • transaction was failed? no error
    • web3 is the most popular library for interacting with ethereum (built-in with geth console)
      • yet it felt buggy, wh- HO- wha- ... !!!
    • some simple operations in web3 is not intuitive
    • once your contract is deployed, you can't "patch" it, it's "forever" on the blockchain with that code
      • there are advanced techniques to handle contract upgrade/migration, but it's not built-in nor easy
    • "struct" di Solidity itu akan dibaca sebagai array di js
      • nama key di struct diabaikan
  10. Di contract FoodCart tadi, field "foodItemExist" ini menarik

    • secara business, field itu ga guna, semua item yg berada di dalam map "foodItems" pasti valuenya true
    • ternyata, web3 ini kalau retrieve value dari mapping yg key-nya ga ada, akan tetap return value
    • seperti null object pattern, return struct dengan value default semua
      • foodItems('gaada') => '',0,0,0,false
    • jadi, dibuatlah 1 field yg kita jadikan "flag" apakah ini value "beneran" atau "null"
ETC

miner.start(1); setTimeout(function () { miner.stop(); }, 14000);

loadScript('source/deploy-FoodCart.js')

newContract.addFoodItem('Emas0', 999, {from: eth.accounts[0]});newContract.addFoodItem('Emas1', 999, {from: eth.accounts[0]});newContract.addFoodItem('Emas2', 999, {from: eth.accounts[0]});newContract.addFoodItem('Emas3', 999, {from: eth.accounts[0]})

newContract.buyFoodItem(0, {from: eth.accounts[0], value: 1000})

newContract.fetchFoodItem(0)

newContract.ForSale(function(err, data) { console.log('ForSale ' + JSON.stringify(data)) })
contractInstance.ForSale(function(err, data) { console.log('ForSale ' + JSON.stringify(data)) })

newContract.Sold(function(err, data) { console.log('Sold ' + JSON.stringify(data)) })
contractInstance.Sold(function(err, data) { console.log('Sold ' + JSON.stringify(data)) })
var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
var abi = [{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_price","type":"uint16"}],"name":"addFoodItem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"skuCount","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sku","type":"uint8"}],"name":"fetchFoodItem","outputs":[{"name":"name","type":"string"},{"name":"sku","type":"uint8"},{"name":"price","type":"uint16"},{"name":"state","type":"uint8"},{"name":"foodItemExist","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint8"}],"name":"foodItems","outputs":[{"name":"name","type":"string"},{"name":"sku","type":"uint8"},{"name":"price","type":"uint16"},{"name":"state","type":"uint8"},{"name":"foodItemExist","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sku","type":"uint8"}],"name":"buyFoodItem","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"name","type":"string"},{"indexed":false,"name":"sku","type":"uint8"},{"indexed":false,"name":"price","type":"uint16"},{"indexed":false,"name":"state","type":"uint8"},{"indexed":false,"name":"foodItemExist","type":"bool"}],"name":"ForSale","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"name","type":"string"},{"indexed":false,"name":"sku","type":"uint8"},{"indexed":false,"name":"price","type":"uint16"},{"indexed":false,"name":"state","type":"uint8"},{"indexed":false,"name":"foodItemExist","type":"bool"}],"name":"Sold","type":"event"}];
var FoodCartContract = web3.eth.contract(abi);
// must remember the contract deployed address: `contract=0x3cec03fe7c97bfa9139ab3ff0c0edd2593c34576`
var contractInstance = FoodCartContract.at('0x12944e98e3d1d0e54efdf8a2c8d221acbab83a83');
// Retrieve initial value
var initSkuCount = contractInstance.skuCount().toNumber();
var cacheFoodItems = {};
for (var i=0; i<initSkuCount; i++) {
var foodTuple = contractInstance.fetchFoodItem(i);
cacheFoodItems[i] = {
name: foodTuple[0],
sku: foodTuple[1].toNumber(),
price: foodTuple[2].toNumber(),
state: foodTuple[3].toNumber(),
foodItemExist: foodTuple[4]
};
}
// Whenever there's an update
function updateData(foodItem) {
cacheFoodItems[foodItem.sku] = {
name: foodItem.name,
sku: foodItem.sku.toNumber(),
price: foodItem.price.toNumber(),
state: foodItem.state.toNumber(),
foodItemExist: foodItem.foodItemExist
};
displayFoodItems();
}
contractInstance.ForSale(function(err, data) {
console.log('ForSale event: ' + JSON.stringify(data));
updateData(data.args);
});
contractInstance.Sold(function(err, data) {
console.log('Sold event: ' + JSON.stringify(data));
updateData(data.args);
});
// Display the items
function displayFoodItems() {
var $tbody = $('#food-items-body');
var $trAdd = $('#food-items-add');
$trAdd.detach();
$tbody.html('');
$tbody.append($trAdd);
var states = {
0: 'ForSale',
1: 'Sold'
};
for (var sku in cacheFoodItems) {
var $newTr = $('<tr></tr>');
var foodItem = cacheFoodItems[sku];
$newTr.append('<td>' + foodItem.sku + '</td>');
$newTr.append('<td>' + foodItem.name + '</td>');
$newTr.append('<td>' + foodItem.price + '</td>');
$newTr.append('<td>' + states[foodItem.state] + '</td>');
$newTr.append(createActionButton(foodItem));
$newTr.insertBefore($trAdd);
}
}
function createActionButton(foodItem) {
if (foodItem.state !== 0) {
return '<td><button disabled="disabled">Sold Out</button></td>';
} else {
var $button = $('<button type="button">Buy</button>');
$button.on('click', function () {
// need to unlock eth account first
contractInstance.buyFoodItem(foodItem.sku, { from: web3.eth.accounts[0], value: foodItem.price });
$button.text('...');
});
return $('<td>').append($button);
}
}
displayFoodItems();
// Add new Item
var $buttonAdd = $('#button-add-item');
$buttonAdd.on('click', function () {
var $newName = $('#new-name');
var $newPrice = $('#new-price');
// need to unlock eth account first
contractInstance.addFoodItem($newName.val(), $newPrice.val(), { from: web3.eth.accounts[0] });
$newName.val('');
$newPrice.val('');
});
<!DOCTYPE html>
<html>
<head>
<title>Hello World DApp</title>
<!-- <script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script> -->
<script src="./web3.js"></script>
<script src="./jquery-3.3.1.slim.min.js"></script>
</head>
<body>
<h1 style="text-align: center">M A G I C</h1>
<table id="food-items" class="pure-table" style="margin:auto">
<thead>
<tr>
<th>SKU</th>
<th>Name</th>
<th>Price</th>
<th>State</th>
<th>Action</th>
</tr>
</thead>
<tbody id="food-items-body">
<tr id="food-items-add">
<td>#</td>
<td><input id="new-name" /></td>
<td><input id="new-price" /></td>
<td>-</td>
<td><button type="button" id="button-add-item">Add</button></td>
</tr>
</tbody>
</table>
<script src="./index-updated.js"></script>
</body>
</html>
pragma solidity ^0.5.0;
contract FoodCart{
/* set owner of contract */
address owner;
/* a variable to track the most recent sku of a food item */
uint8 public skuCount;
/* an enum to store food item state */
enum State {ForSale, Sold}
/* add event to emit when a new food item is added to cart */
event ForSale(string name, uint8 sku, uint16 price, uint8 state, bool foodItemExist);
/* add event to emit when a new food item is sold */
event Sold(string name, uint8 sku, uint16 price, uint8 state, bool foodItemExist);
/* a struct to store details of a food item */
struct FoodItem {
string name;
uint8 sku;
uint16 price;
State state;
bool foodItemExist;
}
/* a map that maps sku to food item */
mapping (uint8 => FoodItem) public foodItems;
/* a modifier to check that food item exist */
modifier doesFoodItemExist(uint8 _sku){
require(foodItems[_sku].foodItemExist);
_;
}
/* a modifier to check that an item is forsale */
modifier isFoodItemForSale(uint8 _sku){
require(foodItems[_sku].state == State.ForSale);
_;
}
/* a modifier to check that the right price is paid for the food item */
modifier hasBuyerPaidEnough(uint8 _sku){
require(msg.value >= foodItems[_sku].price);
_;
}
constructor() public {
owner = msg.sender;
skuCount = 0;
}
function addFoodItem (string memory _name, uint16 _price) public {
foodItems[skuCount] = FoodItem({
name: _name,
sku: skuCount,
price: _price,
state: State.ForSale,
foodItemExist: true
});
emit ForSale(_name, skuCount , _price, uint8(State.ForSale), true);
skuCount = skuCount + 1;
}
function buyFoodItem (uint8 _sku)
payable
public
doesFoodItemExist(_sku)
isFoodItemForSale(_sku)
hasBuyerPaidEnough(_sku)
{
foodItems[_sku].state = State.Sold;
emit Sold(foodItems[_sku].name,
foodItems[_sku].sku,
foodItems[_sku].price,
uint8(foodItems[_sku].state),
foodItems[_sku].foodItemExist);
}
function fetchFoodItem(uint8 _sku)
doesFoodItemExist(_sku)
public
view
returns (string memory name, uint8 sku, uint16 price, uint8 state, bool foodItemExist)
{
name = foodItems[_sku].name;
sku = foodItems[_sku].sku;
price = foodItems[_sku].price;
state = uint8(foodItems[_sku].state);
foodItemExist = foodItems[_sku].foodItemExist;
return (name, sku, price, state, foodItemExist);
}
function () external payable {
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment