Blockchain Important notes:
Transactions: Transaction is a unit of data in blockchain. Several transaction(meaning changing state of current data) requests are kept in mempool.
Consistent transactions are kept in mempool before undergoing consensus.
Block: List of Transactions+ hash of previous block + timestamp+merkle root + nonce
Smart contract: Send method creates transactions (Changes data). Call method doesn’t change data
Nonce: Generate a number by brute force so that by adding it to transaction you get a number starting with constant number of 0s.Nonce adds immutability. Transaction e ekta dot dileo invalidate hoye jabe.
Jodi ekta block er hash o keu change kore, tahole pura oi block theke baki shob invalidate hoye jay.
Blockchain stores utxos: Unused bitcoin store kore
How transactions are handled in bigchaindb? https://www.bigchaindb.com/developers/guide/key-concepts-of-bigchaindb/
Hyperledger fabric:
Channels: copy of transactions which every participant can keep
https://hyperledger-fabric.readthedocs.io/en/release-2.2/fabric_model.html
Arrays:
ArrayType[] public arrayName;
Here [] indicates dynamic size, public indicates other contracts can read but cannot write.
Function:
function eatHamburgers(string memory _name//value, uint _amount//reference) public {
}
We're also providing instructions about where the _name variable should be stored- in memory.
Well, there are two ways in which you can pass an argument to a Solidity function:
By value, which means that the Solidity compiler creates a new copy of the parameter's value and passes it to your function. This allows your function to modify the value without worrying that the value of the initial parameter gets changed.
By reference, which means that your function is called with a... reference to the original variable. Thus, if your function changes the value of the variable it receives, the value of the original variable gets changed.
Note: It's convention (but not required) to start function parameter variable names with an underscore (_) in order to differentiate them from global variables. We'll use that convention throughout our tutorial.
Same thing that is needed to check ownership
require(msg.sender == zombieToOwner[_zombieId]);
Private functions, only can be called within contract. Need _ before func name.
Events:
Addressing
The Ethereum blockchain is made up of accounts, which you can think of like bank accounts. An account has a balance of Ether (the currency used on the Ethereum blockchain), and you can send and receive Ether payments to other accounts, just like your bank account can wire transfer money to other bank accounts.
Address number is unique, address is owned by a specific user. Address is a data type.
Mapping
mapping (address => uint) public accountBalance;
// Or could be used to store / lookup usernames based on userId
mapping (uint => string) userIdToName;
mapping (key>=value) public name;
Msg.Sender
A global variable available to all functions. Its an address.
mapping (address => uint) favoriteNumber;//mapping
favoriteNumber[msg.sender] = _myNumber; //assigned _myNumber under msg.sender
name[key]=value;
In Solidity, you can increase a uint with ++, just like in javascript:
Throwing error
function sayHiToVitalik(string memory _name) public returns (string memory) {
// Compares if _name equals "Vitalik". Throws an error and exits if not true.
// (Side note: Solidity doesn't have native string comparison, so we
// compare their keccak256 hashes to see if the strings are equal)
require(keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked("Vitalik")));
// If it's true, proceed with the function:
return "Hi!";
}
Inheritance
contract ZombieFeeding is ZombieFactory{
}
Import
To import another file use import keyword
import "./someothercontract.sol";
Imports links two files like .h header
Storage vs Memory
Storage variables are stored permanently. Memory variables are temporary.
Storage variables are written permanently on the blockchain.
//Sandwich storage mySandwich = sandwiches[_index];
//Sandwich memory anotherSandwich = sandwiches[_index + 1];
Visibility of functions
Private functions are not visible to other contracts not even inherited ones. Public is public.
Internal: Same as private but function visible to child contracts.
External: Same as public but function can only be called outside the origin contract. (The contract in which it is declared cannot use it)
Reading other contracts on the chain
For our contract to talk to another contract on the blockchain that we don't own, first we need to define an interface.
let's say we had an external contract that wanted to read the data in this contract using the getNum function.
First we'd have to define an interface of the LuckyNumber contract:
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
For one, we're only declaring the functions we want to interact with — in this case getNum — and we don't mention any of the other functions or state variables.
Secondly, we're not defining the function bodies. Instead of curly braces ({ and }), we're simply ending the function declaration with a semi-colon (;). (Like header files of c++)
By including this interface in our dapp's code our contract knows what the other contract's functions look like, how to call them, and what sort of response to expect.
In solidity, we can return multiple values from a function.
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
)
In this way, your contract can interact with any other contract on the Ethereum blockchain, as long they expose those functions as public or external.
Handling Multiple return values:
// This is how you do multiple assignment:
(a, b, c) = multipleReturns();
// We can just leave the other fields blank:
(,,c) = multipleReturns();
Contract deployment
Once we're ready to deploy this contract to Ethereum we'll just compile and deploy ZombieFeeding — since this contract is our final contract that inherits from ZombieFactory, and has access to all the public methods in both contracts.
Immutability
To start with, after you deploy a contract to Ethereum, it’s immutable, which means that it can never be modified or updated again.
The initial code you deploy to a contract is there to stay, permanently, on the blockchain. This is one reason security is such a huge concern in Solidity. If there's a flaw in your contract code, there's no way for you to patch it later.
To prevent such bugs, avoid hard coding stuff on the smart contract.
Ownable
External functions can be accessed by anybody. To prevent this, we can use ownable. meaning they have an owner (you) who has special privileges.
Constructors: constructor() is a constructor, which is an optional special function that has the same name as the contract. It will get executed only one time, when the contract is first created.
Function Modifiers: modifier onlyOwner(). Modifiers are kind of half-functions that are used to modify other functions, usually to check some requirements prior to execution. In this case, onlyOwner can be used to limit access so only the owner of the contract can run this function. We'll talk more about function modifiers in the next chapter, and what that weird _; does.
So the Ownable contract basically does the following:
When a contract is created, its constructor sets the owner to msg.sender (the person who deployed it)
constructor() internal {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
It adds an onlyOwner modifier, which can restrict access to certain functions to only the owner.
It allows you to transfer the contract to a new owner
This is because of how contract inheritance works. Remember:
ZombieFeeding is ZombieFactory
ZombieFactory is Ownable
Thus ZombieFeeding is also Ownable, and can access the functions / events / modifiers from the Ownable contract. This applies to any contracts that inherit from ZombieFeeding in the future as well.
modifier onlyOwner() {
require(isOwner()); // if false the function is returned
_;
}
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
Notice the onlyOwner modifier on the renounceOwnership function. When you call renounceOwnership, the code inside onlyOwner executes first. Then when it hits the _; statement in onlyOwner, it goes back and executes the code inside renounceOwnership.
GAS
In Solidity, your users have to pay every time they execute a function on your DApp using a currency called gas. Users buy gas with Ether (the currency on Ethereum), so your users have to spend ETH in order to execute functions on your DApp.
The total gas cost will depend on how much computational effort will be needed.
Hence code optimization is a must in Dapps. Making a sloppy app will make the users pay unnecessary fees.
WHY GAS?
Ethereum is like a big, slow, but extremely secure computer. When you execute a function, every single node on the network needs to run that same function to verify its output — thousands of nodes verifying every function execution is what makes Ethereum decentralized, and its data immutable and censorship-resistant.
Struct packing to save gas
uint by default takes 256bit space if uintS(uint8, uint16, uint32) is not mentioned. So uint takes up more gas.
There is an exception to this. Inside structS.
If you have multiple uints inside a struct, using a smaller-sized uint when possible will allow Solidity to pack these variables together to take up less storage.
So we should use the minimum variable size we can get away with inside structs.
You'll also want to cluster identical data types together (i.e. put them next to each other in the struct) so that Solidity can minimise the required storage space.
For example, a struct with fields
uint c;
uint32 a;
uint32 b;
will cost less gas than a struct with fields
uint32 a;
uint c;
uint32 b;
because the uint32 fields are clustered together.
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
}
The readyTime property requires a bit more explanation. The goal is to add a "cooldown period", an amount of time a zombie has to wait after feeding or attacking before it's allowed to feed / attack again. Without this, the zombie could attack and multiply 1,000 times per day, which would make the game way too easy.
Now variable
The variable now will return the current unix timestamp of the latest block (the number of seconds that have passed since January 1st 1970).
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
Solidity also contains the time units seconds, minutes, hours, days, weeks and years.
Passing struct as argument
Struct can be passed in a private or internal function.
The syntax looks like this:
function _doStuff(Zombie storage _zombie) internal {
// do stuff with _zombie
}
This way we can pass a reference to our zombie into a function instead of passing in a zombie ID and looking it up.
Next, create a function called _isReady. This function will also take a Zombie storage argument named _zombie. It will be an internal view function, and return a bool.
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);// same as if(_zombie.readyTime <=now) return true;
}
Be careful about public and external functions
An important security practice is to examine all your public and external functions, and try to think of ways users might abuse them. Remember — unless these functions have a modifier like onlyOwner, any user can call them and pass them any data they want to.
Extra function modifier features
We can pass arguments to function modifiers. Remember _; means go back to original function.
Calldata: calldata is somehow similar to memory, but it's only available to external functions.
View functions:
View functions do not cost any gas when called externally by the user.
Because view functions are used only to read the data.
Note: If a view function is called internally from another function in the same contract that is not a view function, it will still cost gas.
Returning an array
function getZombiesByOwner(address _owner) external view returns(uint[] memory)
Storage
Storage variables are permanently stored on the blockchain
Memory arrays:
You can use the memory keyword with arrays to create a new array inside a function without needing to write anything to storage. The array will only exist until the end of the function call, and this is a lot cheaper gas-wise than updating an array in storage — free if it's a view function called externally.
Declaring new array
uint[] memory values = new uint[](3);
uint[] memory result= new uint[](ownerZombieCount[_owner]);//array size equal to ownerZombieCount
For loops
function getEvens() pure external returns(uint[] memory) {
uint[] memory evens = new uint[](5);
// Keep track of the index in the new array:
uint counter = 0;
// Iterate 1 through 10 with a for loop:
for (uint i = 1; i <= 10; i++) {
// If `i` is even...
if (i % 2 == 0) {
// Add it to our array
evens[counter] = i;
// Increment counter to the next empty index in `evens`:
counter++;
}
}
return evens;
}
State modifiers:
We also have state modifiers, which tell us how the function interacts with the BlockChain: view tells us that by running the function, no data will be saved/changed. pure tells us that not only does the function not save any data to the blockchain, but it also doesn't read any data from the blockchain. Both of these don't cost any gas to call if they're called externally from outside the contract (but they do cost gas if called internally by another function).
Modifiers can be stacked together
Payable:
Payable special type of modifier. To pay money while calling contract at the same time, this modifier is used.
contract OnlineStore {
function buySomething() external payable {
// Check to make sure 0.001 ether was sent to the function call:
require(msg.value == 0.001 ether);
// If so, some logic to transfer the digital item to the caller of the function:
transferThing(msg.sender);
}
}
Here msg.value indicates how much ether was sent to the contract during function call.
Let's say our game has a feature where users can pay ETH to level up their zombies. The ETH will get stored in the contract, which you own — this a simple example of how you could make money on your games!
After you send Ether to a contract, it gets stored in the contract's Ethereum account, and it will be trapped there — unless you add a function to withdraw the Ether from the contract.
Transfer keyword:
Withdraw:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
address payable _owner = address(uint160(owner()));
_owner.transfer(address(this).balance);
}
}
But the _owner variable is of type uint160, meaning that we must explicitly cast it to address payable.
You can use transfer to send funds to any Ethereum address. For example, you could have a function that transfers Ether back to the msg.sender if they overpaid for an item:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
Or in a contract with a buyer and a seller, you could save the seller's address in storage, then when someone purchases his item, transfer him the fee paid by the buyer:
seller.transfer(msg.value).
These are some examples of what makes Ethereum programming really cool — you can have decentralized marketplaces like this that aren't controlled by anyone.
Zombie battles:
Inheritance structure in the code: ZombieBattleSystem
Random number generation:
Best way to generate random number in solidity is using keccak256.
uint randNonce = 0;
uint random = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
randNonce++;
uint random2 = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
Nonce: the number used only once so that everytime hashfunction generates different value. (SEED)
In Ethereum, when you call a function on a contract, you broadcast it to a node or nodes on the network as a transaction. The nodes on the network then collect a bunch of transactions, try to be the first to solve a computationally-intensive mathematical problem as a "Proof of Work", and then publish that group of transactions along with their Proof of Work (PoW) as a block to the rest of the network.
Random number generation in this way is insecure. Use oracle for better feature
Probability:
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
}
function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
// Start here
if(rand<=attackVictoryProbability){
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
}
}
}
Token:
Real life asset.
A token on Ethereum is a smart contract with common rules.
It has the following functions:
transferFrom(address _from, address _to, uint256 _tokenId) and balanceOf(address _owner).
Token hocche blockchain er upor smart contract use kore banano asset. So different from cryptocurrency.
ERC 20 Fungible token
ERC 721 Non Fungible token(Non divisible, each item is unique)
For zombies, ERC721 is needed.
When implementing a token contract, the first thing we do is copy the interface to its own Solidity file and import it, import "./erc721.sol";.
Then we have our contract inherit from it, and we override each method with a function definition.
Multiple Inheritance is supported in Solidity.
ERC functions:
balanceOf:
function balanceOf(address _owner) external view returns (uint256 _balance);
This function simply takes an address, and returns how many tokens that address owns.
ownerOf
function ownerOf(uint256 _tokenId) external view returns (address _owner);
This function takes a token ID (in our case, a Zombie ID), and returns the address of the person who owns it.
Again, this is very straightforward for us to implement, since we already have a mapping in our DApp that stores this information. We can implement this function in one line, just a return statement.
function balanceOf(address _owner) external view returns (uint256) {
// 1. Return the number of zombies `_owner` has here
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) external view returns (address) {
// 2. Return the owner of `_tokenId` here
return zombitToOwner[_tokenId];
}
Naming ambiguity in solidity:
Function and modifiers cannot have the same name.
Cant change ERC721 function names because
No, we can't do that!!! Remember, we're using the ERC721 token standard, which means other contracts will expect our contract to have functions with these exact names defined. That's what makes these standards useful — if another contract knows our contract is ERC721-compliant, it can simply talk to us without needing to know anything about our internal implementation decisions.
transferFrom
The first way is the token's owner calls transferFrom with his address as the _from parameter, the address he wants to transfer to as the _to parameter, and the _tokenId of the token he wants to transfer.
approve
The second way is the token's owner first calls approve with the address he wants to transfer to, and the _tokenID . The contract then stores who is approved to take a token, usually in a mapping (uint256 => address). Then, when the owner or the approved address calls transferFrom, the contract checks if that msg.sender is the owner or is approved by the owner to take the token, and if so it transfers the token to him.
Events in ERC721:
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
To approve, approval event must be fired.
Overflows:
We must not send tokens to wrong addresses, as it becomes unrecoverable.
Uint8 can hold upto 255. If we add one more to 255 it will cause overflow and the value will reset to 0. This is overflow.
Similarly, subtracting 1 from 0 in uint8 causes value to reset to 255. This is underflow.
To prevent this, OpenZeppelin has created a library called SafeMath that prevents these issues by default.
c=a/b;
If b==0, solidity automatically throws.
Libraries need to imported by using keyword
Assert:
The assert() and require() functions are a part of the error handling aspect in Solidity. Solidity makes use of state-reverting error handling exceptions. This means all changes made to the contract on that call or any sub-calls are undone if an error is thrown. It also flags an error.
They are quite similar as both check for conditions and if they are not met, would throw an error.
assert is similar to require, where it will throw an error if false. The difference between assert and require is that require will refund the user the rest of their gas when a function fails, whereas assert will not. So most of the time you want to use require in your code; assert is typically used when something has gone horribly wrong with the code (like a uint overflow).
uint test = 2;
test = test.mul(3); // test now equals 6
test = test.add(5); // test now equals 11
For 32 bit int we need SafeMath32
For 16bit SafeMath16
https://en.wikipedia.org/wiki/Year_2038_problem
WHAT IS web3,js
Remember, the Ethereum network is made up of nodes, with each containing a copy of the blockchain. When you want to call a function on a smart contract, you need to query one of these nodes and tell it:
The address of the smart contract
The function you want to call, and
The variables you want to pass to that function.
Ethereum nodes only speak a language called JSON-RPC, which isn't very human-readable. A query to tell the node you want to call a function on a contract looks something like this:
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}
Instead of needing to construct the above query, calling a function in your code will look something like this:
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
.send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })
We've created a shell of an HTML project file for you, index.html. Let's assume we have a copy of web3.min.js in the same folder as index.html.
Hosting the nodes:
You could host your own Ethereum node as a provider. However, there's a third-party service that makes your life easier so you don't need to maintain your own Ethereum node in order to provide a DApp for your users — Infura.
Infura
Infura is a service that maintains a set of Ethereum nodes with a caching layer for fast reads, which you can access for free through their API.
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
However, since our DApp is going to be used by many users — and these users are going to WRITE to the blockchain and not just read from it — we'll need a way for these users to sign transactions with their private key.
Note: Ethereum (and blockchains in general) use a public / private key pair to digitally sign transactions. Think of it like an extremely secure password for a digital signature. That way if I change some data on the blockchain, I can prove via my public key that I was the one who signed it — but since no one knows my private key, no one can forge a transaction for me.
Cryptography is complicated, so unless you're a security expert and you really know what you're doing, it's probably not a good idea to try to manage users' private keys yourself in our app's front-end.
But luckily you don't need to — there are already services that handle this for you. The most popular of these is Metamask.
Metamask
Metamask is a browser extension for Chrome and Firefox that lets users securely manage their Ethereum accounts and private keys, and use these accounts to interact with websites that are using Web3.js.
You can use this boilerplate code in all the apps you create in order to require users to have Metamask to use your DApp.
Note: There are other private key management programs your users might be using besides MetaMask, such as the web browser Mist. However, they all implement a common pattern of injecting the variable web3, so the method we describe here for detecting the user's web3 provider will work for these as well.
window.addEventListener('load', function() {
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have web3. Probably
// show them a message telling them to install Metamask in
// order to use our app.
}
// Now you can start your app & access web3js freely:
startApp()
})
Code has to be compiled and deployed in ethereum.
Contract address: A contract after deployment gets a fixed address on ethereum where it will live forever.
For example the address of CryptoKitties is 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d.
Contract ABI:
ABI is application binary interface. Representation of code in json format to make web3.js understand.
When you compile your contract to deploy to Ethereum (which we'll cover in Lesson 7), the Solidity compiler will give you the ABI, so you'll need to copy and save this in addition to the contract address.
Once you have your contract's address and ABI, you can instantiate it in Web3 as follows:
// Instantiate myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
Declaring variable in script tag->
var varname;
create a function named startApp(). We'll fill in the body in the next 2 steps.
The first thing startApp() should do is declare a var named cryptoZombiesAddress and set it equal to the string "YOUR_CONTRACT_ADDRESS" (this is the address of the CryptoZombies contract on mainnet).
Web3.js has two methods we will use to call functions on our contract: call and send.
Call:
Call is used for view and pure functions. It runs on local node and creates no tx on the chain.
(Read only)
Using Web3.js, you would call a function named myMethod with the parameter 123 as follows:
myContract.methods.myMethod(123).call()
Send:
send will create a transaction and change data on the blockchain. You'll need to use send for any functions that aren't view or pure.
Sending requires users to pay gas. It will cause metamask to sign the transaction.
Using Web3.js, you would send a transaction calling a function named myMethod with the parameter 123 as follows:
myContract.methods.myMethod(123).send()
The syntax is almost identical to call().
Retrieving data
Now let's look at a real example of using call to access data on our contract.
Recall that we made our array of zombies public:
Zombie[] public zombies;
In Solidity, when you declare a variable public, it automatically creates a public "getter" function with the same name. So if you wanted to look up the zombie with id 15, you would call it as if it were a function: zombies(15).
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}
// Call the function and do something with the result:
getZombieDetails(15)
.then(function(result) {
console.log("Zombie 15: " + JSON.stringify(result));
});
cryptoZombies.methods.zombies(id).call() will communicate with the Web3 provider node and tell it to return the zombie with index id from Zombie[] public zombies on our contract.
Here web3 promises that it will return the zombies id.
JS promises(OFFTOPIC):
A JavaScript Promise object can be:
Pending
Fulfilled
Rejected
The Promise object supports two properties: state and result.
While a Promise object is "pending" (working), the result is undefined.
When a Promise object is "fulfilled", the result is a value.
When a Promise object is "rejected", the result is an error object.
How to promise:
myPromise.then(
function(value) { /* code if successful */ },
function(error) { /* code if some error */ }
);
function getZombieDetails(id) { —----->remember no datatype no return type
return cryptoZombies.methods.zombies(id).call()
}
getZombieDetails(15)
.then(function(result) {
console.log("Zombie 15: " + JSON.stringify(result));
});
result will be a javascript object that looks like this:
{
"name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
"dna": "1337133713371337",
"level": "9999",
"readyTime": "1522498671",
"winCount": "999999999",
"lossCount": "0" // Obviously.
}
Getting the user's account in MetaMask:
Metamask allows user to manage multiple accounts
var userAccount = web3.eth.accounts[0];
To find which account is currently active
//To check if the account is changed a setInterval loop is ran
var accountInterval = setInterval(function() {
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call some function to update the UI with the new account
getZombiesByOwner(userAccount).then(displayZombies);
}
}, 100);
}
In every 100ms, change will be monitored
Parsing and displaying data:
We passed displayZombies in then statement to obtain the list of zombies owned by the metamask user.
The result will be shown like:
Thus we'll want our displayZombies function to:
First clear the contents of the #zombies div, if there's anything already inside it. (This way if the user changes their active MetaMask account, it will clear their old zombie army before loading the new one).
Loop through each id, and for each one call getZombieDetails(id) to look up all the information for that zombie from our smart contract, then
Put the information about that zombie into an HTML template to format it for display, and append that template to the #zombies div.
Again, we're just using JQuery here, which doesn't have a templating engine by default, so this is going to be ugly. But here's a simple example of how we could output this data for each zombie:
HW:
Sending transaction:
The sender will become msg.sender in code. We will need this to the user of DApp because metamask will prompt the user to sign the transaction.
Sending requires a from address which is the user.
Sending costs gas
A time is needed from the moment the transaction in sent to the time it is actually displayed on the chain. his is because we have to wait for the transaction to be included in a block, and the block time for Ethereum is on average 15 seconds. If there are a lot of pending transactions on Ethereum or if the user sends too low of a gas price, our transaction may have to wait several blocks to get included, and this could take minutes.
Our function sends a transaction to our Web3 provider, and chains some event listeners:
receipt will fire when the transaction is included into a block on Ethereum, which means our zombie has been created and saved on our contract
error will fire if there's an issue that prevented the transaction from being included in a block, such as the user not sending enough gas. We'll want to inform the user in our UI that the transaction didn't go through so they can try again.
Calling payable methods:
WEI: During sending using functions we need to send using Wei not ether.
Smaller unit of ether. 1ether=10^18 wei.
Theres a built in converter for ether to wei in we3.js
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
Return in function:
Subscribing to events:
In order to filter events and only listen for changes related to the current user, our Solidity contract would have to use the indexed keyword, like we did in the Transfer event of our ERC721 implementation:
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
Querying Past events:
We can even query past events using getPastEvents, and use the filters fromBlock and toBlock to give Solidity a time range for the event logs ("block" in this case referring to the Ethereum block number):
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
// `events` is an array of `event` objects that we can iterate, like we did above
// This code will get us a list of every zombie that was ever created
});
Because you can use this method to query the event logs since the beginning of time, this presents an interesting use case: Using events as a cheaper form of storage.
Saving data on chain is the most expensive operation.
But using events is much much cheaper in terms of gas.
The tradeoff here is that events are not readable from inside the smart contract itself. But it's an important use-case to keep in mind if you have some data you want to be historically recorded on the blockchain so you can read it from your app's front-end.
Event listeners are added at the end of startApp() func
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
}).on("error", console.error);
}
Next Steps
This lesson was intentionally basic. We wanted to show you the core logic you would need in order to interact with your smart contract, but didn't want to take up too much time in order to do a full implementation since the Web3.js portion of the code is quite repetitive, and we wouldn't be introducing any new concepts by making this lesson any longer.
So we've left this implementation bare-bones. Here's a checklist of ideas for things we would want to implement in order to make our front-end a full implementation for our zombie game, if you want to run with this and build it on your own:
Implementing functions for attack, changeName, changeDna, and the ERC721 functions transfer, ownerOf, balanceOf, etc. The implementation of these functions would be identical to all the other send transactions we covered.
Implementing an "admin page" where you can execute setKittyContractAddress, setLevelUpFee, and withdraw. Again, there's no special logic on the front-end here — these implementations would be identical to the functions we've already covered. You would just have to make sure you called them from the same Ethereum address that deployed the contract, since they have the onlyOwner modifier.
There are a few different views in the app we would want to implement:
a. An individual zombie page, where you can view info about a specific zombie with a permalink to it. This page would render the zombie's appearance, show its name, its owner (with a link to the user's profile page), its win/loss count, its battle history, etc.
b. A user page, where you could view a user's zombie army with a permalink. You would be able to click on an individual zombie to view its page, and also click on a zombie to attack it if you're logged into MetaMask and have an army.
c. A homepage, which is a variation of the user page that shows the current user's zombie army. (This is the page we started implementing in index.html).
Some method in the UI that allows the user to feed on CryptoKitties. We could have a button by each zombie on the homepage that says "Feed Me", then a text box that prompted the user to enter a kitty's ID (or a URL to that kitty, e.g. https://www.cryptokitties.co/kitty/578397). This would then trigger our function feedOnKitty.
Some method in the UI for the user to attack another user's zombie.
One way to implement this would be when the user was browsing another user's page, there could be a button that said "Attack This Zombie". When the user clicked it, it would pop up a modal that contains the current user's zombie army and prompt them "Which zombie would you like to attack with?"
The user's homepage could also have a button by each of their zombies that said "Attack a Zombie". When they clicked it, it could pop up a modal with a search field where they could type in a zombie's ID to search for it. Or an option that said "Attack Random Zombie", which would search a random number for them.
We would also want to grey out the user's zombies whose cooldown period had not yet passed, so the UI could indicate to the user that they can't yet attack with that zombie, and how long they will have to wait.
The user's homepage would also have options by each zombie to change name, change DNA, and level up (for a fee). Options would be greyed out if the user wasn't yet high enough level.
For new users, we should display a welcome message with a prompt to create the first zombie in their army, which calls createRandomZombie().
We'd probably want to add an Attack event to our smart contract with the user's address as an indexed property, as discussed in the last chapter. This would allow us to build real-time notifications — we could show the user a popup alert when one of their zombies was attacked, so they could view the user/zombie who attacked them and retaliate.
We would probably also want to implement some sort of front-end caching layer so we aren't always slamming Infura with requests for the same data. (Our current implementation of displayZombies calls getZombieDetails for every single zombie every time we refresh the interface — but realistically we only need to call this for the new zombie that's been added to our army).
A real-time chat room so you could trash talk other players as you crush their zombie army? Yes plz.
That's just a start — I'm sure we could come up with even more features — and already it's a massive list.
Since there's a lot of front-end code that would go into creating a full interface like this (HTML, CSS, JavaScript and a framework like React or Vue.js), building out this entire front-end would probably be an entire course with 10 lessons in itself. So we'll leave the awesome implementation to you.
Note: Even though our smart contract is decentralized, this front-end for interacting with our DApp would be totally centralized on our web-server somewhere.
However, with the SDK we're building at Loom Network, soon you'll be able to serve front-ends like this from their own DAppChain instead of a centralized web server. That way between Ethereum and the Loom DAppChain, your entire app would run 100% on the blockchain.
Api: https://youtu.be/pGnYlUW5aLI
Comments
Post a Comment