-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
425 lines (301 loc) · 22.9 KB
/
Copy pathindex.html
File metadata and controls
425 lines (301 loc) · 22.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script> <!-- import ABI file -->
</head>
<body>
<div id="txStatus"></div>
<div id="zombies"></div>
<script>
// Start here
var cryptoZombies;
function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress); // instatiating the web3.js contract
// 2. Create `setInterval` code here
var accountInterval = setInterval(function() {
// Check if account has changed
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);
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
}).on("error", console.error);
}
function displayZombies(ids) {
// Start here
$("#zombies").empty();
for (const id of ids) {
getZombieDetails(id)
.then(function(zombie) {
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});
}
}
function createRandomZombie(name) {
// This is going to take a while, so update the UI to let the user know
// the transaction has been sent
$("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
// Send the tx to our contract:
//Note: You can optionally specify gas and gasPrice when you call send, e.g. .send({ from: userAccount, gas: 3000000 }). If you don't specify this, MetaMask will let the user choose these values.
return cryptoZombies.methods.createRandomZombie(name)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Successfully created " + name + "!");
// Transaction was accepted into the blockchain, let's redraw the UI
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// Do something to alert the user their transaction has failed
$("#txStatus").text(error);
});
}
function feedOnKitty(zombieId, kittyId) {
$("#txStatus").text("Eating a kitty. This may take a while...");
return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Ate a kitty and spawned a new Zombie!");
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}
function levelUp(zombieId) {
$("#txStatus").text("Leveling up your zombie...");
return cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
.on("receipt", function(receipt) {
$("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call();
}
// 1. Define `zombieToOwner` here
function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call();
}
// 2. Define `getZombiesByOwner` here
function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call();
}
// metamask connection
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()
})
</script>
</body>
</html>
<!--
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:
// Yeah... Good luck writing all your function calls this way!
// Scroll right ==>
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}
Luckily, Web3.js hides these nasty queries below the surface, so you only need to interact with a convenient and easily readable JavaScript interface.
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" })
TO QUERY A NODE AND READ DATA FROM BLOCKCHAIN
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. Using Infura as a provider, you can reliably send and receive messages to/from the Ethereum blockchain without needing to set up and maintain your own node.
You can set up Web3 to use Infura as your web3 provider as follows:
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"))
TO QUERY AND SIGN TRANSACTIONS
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. (If you haven't used it before, you'll definitely want to go and install it — then your browser is Web3 enabled, and you can now interact with any website that communicates with the Ethereum blockchain!).
And as a developer, if you want users to interact with your DApp through a website in their web browser (like we're doing with our CryptoZombies game), you'll definitely want to make it Metamask-compatible.
Note: Metamask uses Infura's servers under the hood as a web3 provider, just like we did above — but it also gives the user the option to choose their own web3 provider. So by using Metamask's web3 provider, you're giving the user a choice, and it's one less thing you have to worry about in your app.
Using Metamask's web3 provider
Metamask injects their web3 provider into the browser in the global JavaScript object web3. So your app can check to see if web3 exists, and if it does use web3.currentProvider as its provider.
Here's some template code provided by Metamask for how we can detect to see if the user has Metamask installed, and if not tell them they'll need to install it to use our app:
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()
})
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.
CALL AND SEND
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 only runs on the local node, and won't create a transaction on the blockchain.
Review: view and pure functions are read-only and don't change state on the blockchain. They also don't cost any gas, and the user won't be prompted to sign a transaction with MetaMask.
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.
Note: sending a transaction will require the user to pay gas, and will pop up their Metamask to prompt them to sign a transaction. When we use Metamask as our web3 provider, this all happens automatically when we call send(), and we don't need to do anything special in our code. Pretty cool!
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().
Getting Zombie 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).
Here's how we would write a JavaScript function in our front-end that would take a zombie id, query our contract for that zombie, and return the result:
Note: All the code examples we're using in this lesson are using version 1.0 of Web3.js, which uses promises instead of callbacks. Many other tutorials you'll see online are using an older version of Web3.js. The syntax changed a lot with version 1.0, so if you're copying code from other tutorials, make sure they're using the same version as you!
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));
});
Let's walk through what's happening here.
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.
Note that this is asynchronous, like an API call to an external server. So Web3 returns a promise here. (If you're not familiar with JavaScript promises... Time to do some additional homework before continuing!) See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
Once the promise resolves (which means we got an answer back from the web3 provider), our example code continues with the then statement, which logs result to the console.
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.
}
We could then have some front-end logic to parse this object and display it in a meaningful way on the front-end.
DISPLAYING DATA
Displaying zombie data — a rough example
We've added an empty <div id="zombies"></div> to the body of our document, as well as an empty displayZombies function.
Recall that in the previous chapter we called displayZombies from inside startApp() with the result of a call to getZombiesByOwner. It will be passed an array of zombie IDs that looks something like:
[0, 13, 47]
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:
// Look up zombie details from our contract. Returns a `zombie` object
getZombieDetails(id)
.then(function(zombie) {
// Using ES6's "template literals" to inject variables into the HTML.
// Append each one to our #zombies div
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});
What about displaying the zombie sprites?
In the above example, we're simply displaying the DNA as a string. But in your DApp, you would want to convert this to images to display your zombie.
We did this by splitting up the DNA string into substrings, and having every 2 digits correspond to an image. Something like:
// Get an integer 1-7 that represents our zombie head:
var head = parseInt(zombie.dna.substring(0, 2)) % 7 + 1
// We have 7 head images with sequential filenames:
var headSrc = "../assets/zombieparts/head-" + head + ".png"
Each component is positioned with CSS using absolute positioning, to overlay it over the other images.
If you want to see our exact implementation, we've open sourced the Vue.js component we use for the zombie's appearance, which you can view here: https://github.com/loomnetwork/zombie-char-component
SUBSCRIBING TO EVENTS
There's one more aspect we want to cover — subscribing to events from your contract.
Listening for New Zombies
If you recall from zombiefactory.sol, we had an event called NewZombie that we fired every time a new zombie was created:
event NewZombie(uint zombieId, string name, uint dna);
In Web3.js, you can subscribe to an event so your web3 provider triggers some logic in your code every time it fires:
cryptoZombies.events.NewZombie()
.on("data", function(event) {
let zombie = event.returnValues;
// We can access this event's 3 return values on the `event.returnValues` object:
console.log("A new zombie was born!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);
Note that this would trigger an alert every time ANY zombie was created in our DApp — not just for the current user. What if we only wanted alerts for the current user?
Using indexed
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);
In this case, because _from and _to are indexed, that means we can filter for them in our event listener in our front end:
// Use `filter` to only fire this code when `_to` equals `userAccount`
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
// The current user just received a zombie!
// Do something here to update the UI to show it
}).on("error", console.error);
As you can see, using events and indexed fields can be quite a useful practice for listening to changes to your contract and reflecting them in your app's front-end.
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.
If you recall, saving data to the blockchain is one of the most expensive operations in Solidity. 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.
For example, we could use this as a historical record of zombie battles — we could create an event for every time one zombie attacks another and who won. The smart contract doesn't need this data to calculate any future outcomes, but it's useful data for users to be able to browse from the app's front-end.
-->
<!--
WHAT TO DO AFTER
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.
-->