Prerequisite:
-
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
-
Prepare directory
mkdir project cd project mkdir node1 mkdir source
-
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
- chainId: unique id for publicly identifiable network
-
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
-
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.
-
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
-
-
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()
-
Let's check our balance now (RICH!)
- // cek saldo
web3.fromWei(eth.getBalance(eth.accounts[0]), "ether") > 20
-
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
-
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
-
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
-
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); } } );
-
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
-
Interact with the contract via newly created variable
- // We accessing the "cache" of our network
newContract.address > "0xa93e210e892ee40d74d90c90e481237712d92267" newContract.greet() > "HELLO WARLD!"
-
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"
-
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
-
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)
-
Create new directory and accounts
- // use the same genesis.json
mkdir node2 geth --datadir node2 account new geth --datadir node2 init genesis.json
-
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
-
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
-
"dApp" = conventional application (e.g. website) yg connect ke Ethereum
-
Write the source code of the contract (Solidity language)
// source/FoodCart.sol contract FoodCart { ... }
-
Compile the source code
solc --bin --abi -o source source/FoodCart.sol --overwrite
-
Create deploy code
// source/deploy-FoodCart.js ... // see above deploy.js
-
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
-
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"
-
-
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);
- // Open in browser http://localhost:8080/
- // ERROR:
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!
- // yg bener: BigNumer(5).toNumber() + 1
- // BigNumber yg dipakai web3: https://github.com/MikeMcl/bignumber.js/
-
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>
-
Make "Buy" button works
- // See updated file "dapp/index-updated.js"
-
What is a "bootnode"?
- an ethereum node
-
What is "geth.ipc" file?
- file yg akan menjadi sumber komunikasi dengan interactive console
-
Di dalam genesis bisa allocate saldo awal, tapi kan address itu random, gimana cara claimnya?
-
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
- di public network, ini chainId yg sudah ter-define/reserved
-
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.
-
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.
-
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
-
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
-
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
- BigNumber cannot use arithmetic operator directly, e.g. skuCount() + 1 => "41"
- skuCount().toNumber() + 1 => 5
- so we must read carefully the web3 doc
- web3 doc:
- BigNumber cannot use arithmetic operator directly, e.g. skuCount() + 1 => "41"
- 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
-
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)) })