Now in the last lesson, we managed to get our collection of NFTs to display under the section My NFTs. 1
And whenever we mint a new NFT and it gets created, it's going to be loaded up when we head over to 2
this route. 3
Now, the next step is to be able to sell our NFTs if we wanted to, because after all, this is 4
an NFT marketplace that we're creating, 5
right? 6
So we're going to add a little button below each NFT so we can click on it, set a price, and then 7
get it to be sold on our marketplace. Which means it's going to be transferred over to the marketplace 8
OpenD, 9
so the OpenD can hold on to the ownership. 10
And then once somebody buys that NFT then OpenD can transfer it to the buyer 11
and also the buyer's money gets transferred back to us, 12
the owners of the NFT. 13
So a little bit convoluted and there's quite a lot of code and logic that we have to crack through. 14
So let's get on with it. 15
Now, the first thing we're going to do is we're going to create a button underneath our NFT cards. 16
I want you to go ahead and create a new component for this. In the components folder, 17
we're going to create a new button with the file name of button.jsx. 18
And inside this file button.jsx, we're going to need to create a functional component, which we're 19
going to call Button. 20
And inside this button component, all that it's going to do is to return the HTML which I've already 21
included for you in the read me file. 22
So if you just go ahead and copy it, wholesale everything in there or if it's easier, you can take 23
a look at it in the preview because then it gets rid of all these markdown formatting and you actually 24
copy over the correct parts, which is everything between the open and closing div. 25
And then we're going to paste it inside this return. 26
And I've got an error here because I'm missing my parentheses for my function. 27
And finally, we're going to export, default, button. 28
Now, this line of code is going to require us to import React. 29
So let's go ahead and copy that from another jsx file and we can paste it in here. 30
We don't need the useEffect, useState so we can delete all of that. 31
All we need to do is import React from react. 32
Now here we've got an onClick and we've got a button name. 33
Now this onClick can actually take a function that's passed in through the props. 34
So we could actually have a prop that we pass in called props.handle click, which will be a function 35
that's passed in here, essentially a callback that will get triggered when this onClick gets triggered. 36
Now that we've got our button created, let's go back to our item.jsx and below where we've set 37
our owner, set our image, 38
we're going to set our button. And 39
we're going to set it to that new componen, button and hopefully it should import by itself if you select 40
it from the dropdown. 41
And then let's close off that tag and make sure that we pass in the required props. 42
The prop is called handleClick. 43
So I'm just going to copy it over here and I'm going to set that equal to a function that we've got 44
in here. 45
So I'm going to call this function handleSell, 46
because that is what it's going to do 47
after all. This handleSell is going to be passed in as the callback function, 48
and all we going to do for now, just to make sure that it works, is to console.log 49
Sell Clicked. 50
And now we've got this set button, but we don't actually have the button created. 51
So I'm going to create a new state tracker called button and setButton, and then I'm going to use 52
State to keep track of it. 53
So where is the button going to go? 54
Well, it's going to go just below this final paragraph tag right here. And now 55
if we hit save and we go back to our frontend, then you should be able to see these styled buttons 56
underneath each of your NFTs. 57
And if I go ahead and bring up my console and click on sell, then you can see that it says Sell Clicked 58
every time I click on it. 59
That means we've managed to pass over this function over to our button and it gets triggered whenever 60
that button gets clicked. 61
So this is a really neat way of working with React when you're passing functions as props. 62
Now what do we want to happen when somebody clicks on the sell button? 63
Well, we want to be able to give them some sort of way of inputting how much they want to sell the 64
item for. 65
So what we'll need is some sort of input. 66
If you head over to the README.md, you should see a section called the Price Input HTML. 67
What we want to do is we want to insert it just above our button here. 68
So I'm going to create something called priceInput 69
and up here we're going to track its state. 70
So it's priceInput and setPriceInput. 71
And now if I copy my priceInput again, then I want to set it right here. 72
So when handleSell is triggered, it means the sell button was clicked, right? 73
So at this point we want to set the priceInput to include this new input, and we want that to show up 74
right here above the button. 75
And we want to create a variable with the let keyword called price, which is going to be used to display 76
the value that has been entered into this input. 77
And we also need to update the onChange to set this price whenever the user types into the input. 78
And in the output section, we're going to set the price to equal e.target.value. 79
Now normally we would probably use state for this, but because we're going to be doing a lot of asynchronous 80
calls, it's not certain that the state in the input will be updated before we needed to use it to sell 81
the item or to set the price. 82
Now let's take a look and see what this looks like. 83
So now when we click the sell button, it triggers the handle sell function, which console logs sell 84
clicked. And in addition sets the priceInput from previously, which was nothing, to this input HTML 85
which shows up like this. 86
So we can enter a number in here, which is the price in DANG that we want to sell an FTA for. 87
And now we want to be able to click on this button to confirm our price. 88
That means we want to change this button up a little bit. 89
We want it to have a different function for the onClick to do something else and also to change its 90
text. Down here, just below where we've set our priceInput, we're going to set the button to a new button 91
component. 92
We want to be able to pass over the props for the button text and I want it to say confirm this time. 93
Previously it should have a text attribute that says sell. 94
And now once they've set the price, then I want it to say confirm. 95
So now going into our button, we're going to make sure that we replace this hardcoded text with the 96
props.text, which is what we set over here and over here. 97
So now let's try that again. 98
Initially it says Sell, but as soon as I click on it, it updates to confirm. 99
In addition, I don't want it to call handleSell again because this time I actually want to make the 100
sale. 101
So I'm going to create another function which is probably going to need to be an asynchronous function 102
because we're going to be working with the back end in this function to sell our item. This function, 103
I'm going to call it as a sellItem. 104
And this is what's going to be triggered when the second button click happens. 105
So we're going to replace that handle 106
Click here with selIItem and I'm going to console.log 107
confirm clicked. 108
Let's try this again. 109
We click sell, sell clicked. And then I put in a price, 110
click confirm and confirm gets clicked. 111
We can actually access the price in here as well. 112
So if I want to console.log the set price, 113
then I can tap into that variable called price right here. 114
And if I click on sell, give it a price, click confirm, 115
you can see my setPrice is available right here for me to do something with. 116
So what's the next thing I want to do? 117
Well, I want to be able to list this item in my main canister and keep track of it somewhere in a Hash 118
Map so that I'll know which of these items are listed and how much they're listed for and who it belongs 119
to. 120
So that's what's going to happen in my sellItem function, 121
but first, we have to write the logic on the backend. 122
I'm going to create a new public shared function 123
because this is going to allow me to get hold of the caller's id, which is going to be the user who 124
owns that NFT. 125
And that's going to be really helpful when I set the owner of the NFT in the sale listing. 126
Now I'm going to call this function listItem and it's going to take two inputs. 127
One is the id which is going to be a principal type of the NFT that's being listed. 128
And secondly, it's the price as a Nat or a natural number, which will determine how much that price 129
is listed for, 130
so the two key pieces of information, 131
right? 132
And what this function is going to do is we're going to create a HashMap up here. 133
So we've got a map of NFTs, map of owners, 134
but we're going to create another one, which is going to keep track of all the listings. 135
And that function is going to add to that HashMap. 136
We're going to call this mapOfListings, and it's going to be a HashMap.HashMap. 137
Now, the data type of the key is going to be a principal because it's going to be the principal ID 138
of the listed NFT. 139
And the value that that is going to map to is going to be a custom type because we want to hold a bunch 140
of information. We want to hold the owner's ID, we want to hold the price, 141
and maybe in the future you'd even want to hold things like the historic record of sales, what price 142
it was when it first sold, what price it was when it was sold the next time, what date stamp that 143
happened on, and a whole bunch of complex things. 144
But for now, we're going to keep it quite simple just so that you can see how it's done. 145
So I'm going to create a private type. 146
So this is a new data type that I'm creating here, which is going to be called Listing. 147
And this is how you would create a new data type. 148
So you give it a name with a capital letter to begin with, equals, and then a set of curly braces. 149
Now, inside here, we get to define all of its properties. 150
So what is the Listing going to hold? 151
Well, it's going to hold the item owner, which is going to be of a principal type. 152
And then we close it off with a semicolon. 153
And then it's going to hold the item price as well, which is going to be a natural number. 154
And if we add a semicolon here, then that error will go away and we can add this listing as the data 155
type of the value. 156
Again, we're going to initialize our HashMap in the same way as before because our principal equal 157
checks for the key equality and principal hash will hash all keys for us when we create new items. Down 158
here in our listItem function, 159
we're going to get hold of the item, the actual NFT, from our map of NFTs. 160
And because we're not sure if a particular NFT actually exists, then we have to do what we did before, 161
which is use this switch statement to unwrap that optional. 162
So I'm going to create a new variable called item and it's going to have a data type of the NFTActor 163
Class.NFT, and it's going to be equal to a switch statement that's going to switch on, 164
looking through the map of NFTs, calling the .get function and then getting the NFT with this ID 165
that's passed in. 166
Now we get to specify in the case where it's null, where that ID doesn't exist as a key in the map 167
of NFTs, 168
well, in that case, we're actually going to return not an NFT, but we're actually going to use the 169
return keyword to output some piece of text. So we can make our function have a return in the form 170
of text. 171
And if in the case where we can't find the item with the id that was passed, then we're actually 172
going to exit the rest of the function. 173
We're not going to continue because there's no point. 174
And we're going to return a piece of text telling the frontend that the NFT does not exist. 175
That way we might be able to give that as feedback to the user or do something with it. 176
In the other case where we have the result existing, then we're going to do our usual thing where we 177
say case, 178
if it's a optional result and it does exist, then we're going to return the actual result. And we need 179
some semicolons at the end of all of these 180
lines. And it's angry with us because we don't have a return statement. 181
So for now, I'm just going to return the word Success just to stop the error messages from bugging 182
me. 183
So the next thing we want to do is we want to get hold of the owner of this NFT that we are pulling 184
up because we want to make sure that not just anyone is calling this list item method. 185
We have to check that the person who's calling it, the msg.caller, is the same person as the owner 186
of the item that's listed in our map of NFTs. 187
Now, once we've gotten hold of our item which is our NFTActorClass here, we can call that method 188
getOwner in order to check the stored owner of that NFT. So we can say let owner = item.get 189
Owner. 190
In our if statement, we can check to see if the owner is the same as the msg.caller. And using that 191
method from the principal data type equal, we can perform that check quite easily. So we can say principal 192
.equal and we can pass in two values. 193
First is the owner and second is the msg.caller. 194
And now, if this if statement is true, then that means it's the actual owner who is calling this, 195
which means this is allowed and we can list the item. 196
But if that statement is not true, then that means it's just some random person trying to list somebody 197
else's NFT, which should not be allowed. 198
So in that case, we're just simply going to return 199
'You don't own the NFT,' which sounds a little bit harsh, but I think it gets to the point. 200
Now, if this is the case, though, we're going to create that new listing inside our map of listings. 201
We can create the new listing item, newListing, 202
and this is going to be of data type Listing. 203
And we can set it equal to a new set of curly braces and we'll set each of these attributes, 204
so itemOwner and itemPrice. 205
So itemOwner is going to be equal to the owner that we got back just now, because we've checked that 206
these are the same, and the itemPrice, more importantly, is going to be the price that was passed 207
in over here. 208
And once we've created our Listing, we can then put it into the mapOfListings.put, and we're 209
going to put in the key as the id of the item that's being listed and the value being the newListing 210
that we created just now. 211
And we can cut this success return to below here, because that's really the only time when it should 212
be successful. 213
And if we add a semicolon here, then that error message should go away. 214
And here it's telling me expression of type async principal cannot produce expected type of principal, 215
which makes me think that this is currently still a promise that's not realized because we forgot, 216
I forgot to add the await keyword here. 217
So this should be the final code of our listing 218
and it does a couple of important things. 219
One is it checks that the person who's actually calling this method to list an NFT is the actual owner 220
of the NFT because the msg.caller has to contain your ID and we check it against the owner of the 221
NFT. 222
The second thing that it does is it creates this new listing, which seems a little bit overkill for 223
our two pieces of information. 224
But if you were to extend this project, then you would add in the timestamp of the sale or keep track 225
of the historical prices and a whole bunch of other things that you could do in here. 226
But now that we've created this function, let's go back to our frontend and actually call it. 227
So inside our sellItem function, we're going to call that method. 228
So we're going to say await, 229
and then we need to call the opend canister which we have yet to import. 230
So let's add that. 231
So import open curly braces, opend and it's going to be from three of these ../ declarations/ 232
opend. 233
Now we can call that method await opend.listItem, and we have to provide our two pieces of 234
information: 235
the id of the NFT and the price. 236
So the id is going to come from our props here, 237
so props.id. 238
And the price is of course going to come from our priceInput when we said it here. 239
Now I'm going to save the output as a listingResult, and for now I'm just going to console.log it. 240
Like 241
so. In order to test this out because we've changed the back end, we're going to have to redeploy 242
our canister. 243
I'm going to go ahead and go to my read me and scroll up to where I've got the deploy canisters code 244
and I'm going to copy all of this, 245
paste it in here and hit enter. 246
Now, once that's all deployed, if we head back and refresh our website, we should have nothing in 247
our collection because it's all reset. So we can mint a new NFT here, give it a name, and once it's 248
minted, we can go over to all my NFTs and click the sell button. 249
So I'm going to set a price on it, 250
click confirm and it tells me invalid Nat argument 12. And you see how it's got the quotation marks around 251
the 12, which gives me an idea of where the bug might be. 252
Now I hope you don't mind me showing you all these bugs, because from my experience of building, especially 253
this NFT project, which is really, really quite big, I've come across a lot of different error codes 254
and a lot of different problems and a lot of different bugs, which, you know, you'll probably make 255
as well. 256
And I think it's quite helpful to see the error messages and have me just sort of translate to what they 257
mean in case it happens to you as well when you're developing your own projects. 258
So in this case, it's saying this is not a Nat basically. This 12 is a string. 259
So if we go down to the part where we actually passed over the price, if instead of passing over as 260
just a string, we actually wrap it inside this number converter, then it's going to convert it into 261
a number data type. 262
So let's try that again. 263
And this time we're getting our listing success come back, which means that everything actually worked 264
and we've got our newly listed item into our map of listings. 265
Now, what else needs to happen when you sell an item? 266
Well, you need to transfer this item over to the platform, and the platform needs to hold on to it. 267
And the reason for this is because, similar to how we did this check here making sure that the person 268
who's calling this list item is the same person that owns the NFT, 269
we want to do the same thing in here when we do the transfer function. 270
We want to make sure that we only transfer the item if that transfer function is being called by the 271
owner of the NFT. 272
That's the next function that we have to write, and I'm going to do it at the bottom here. 273
Again, it's going to be a public shared function, 274
so that we can tap into the msg. 275
caller. And this function is going to be called transferOwnership. 276
Now it's going to take two inputs: 277
one is the new owner's id, so newOwner, 278
which is going to be a principal data type. 279
And this function is going to return a piece of text to let us know whether if the transfer went through 280
successfully. 281
Now, the first thing we're going to do is similar to what we did before, which is to check and make 282
sure that when we're performing this transfer, 283
this method is actually being called by the owner of the NFT. 284
To do that, all we have to do is say msg.caller and check that it's equal to the NFT owner, 285
which is set as the owner of this NFT. 286
Now, if this is not true, 287
so if there's an else statement, now means this person doesn't actually own the NFT. 288
So we're simply going to stop and return, 289
and because we've got this output text, we're going to give the frontend an error message and say that 290
'Not initiated by an NFT owner.' 291
But inside this if statement though, we're going to go ahead with the transfer. 292
So the transfer basically involves setting this NFT owner to the newOwner that's passed in, which 293
means we have to change this to a var. 294
Now, it's generally not a good idea to leave these global vars lying around where it could be changed 295
by other classes or other actors. 296
So we're going to make all of these properties private. 297
That way we can only update these properties within the NFT itself. 298
And in the case of the NFT owner, that means only through this transferOwnership function. 299
So we're going to set the NFT owner using that sort of weird syntax which is the := to the 300
new owner that's passed in. 301
And once this is done, we're going to return success so that our frontend knows that the transfer 302
went through successfully. 303
Now let's hit save and close down this file and go to our read me and go ahead and deploy and update all 304
our canisters using the command line code in here. 305
Coming back to our item, 306
once we've gotten a success back from the Listing result... 307
Now I'm just using text here just to make it easier for you to see what's actually going on so we can 308
follow it sort of roughly in English. 309
But of course, if you want to do this in terms of error codes or success codes or any other way of 310
checking the output, you can. 311
But here, it's pretty clear that if the listing process went through correctly, then we're going to 312
make the transfer. 313
The transfer is going to call a method, the transferOwnership, on the NFT itself. 314
And we already accessed the NFT itself up here when 315
we created that agent and then we loaded up the NFTActorClass through this line. 316
So we can simply piggyback off that by creating a new let variable called the NFTActor 317
and then over here we get rid of the const keyword and simply assign it to this variable. 318
So that means down here, we can then use it and use our await NFTActor.transferOwnership. 319
Now the input that our transferOwnership function takes is the principal ID of the new owner. 320
So who is going to be the new owner? 321
Well, it's going to be our OpenD canister. 322
So it'd be great if we get hold of its id. If we create a new function in here, which will be public 323
query func, 324
and we can call it 325
getOpenDCanisterID. 326
Quite wordy again, but good for readability. 327
And then we can return its ID as a principal. 328
Now, as you've seen before, we can use the principal.fromActor method in order to get the principal 329
ID of a particular actor. 330
Then we can simply just write principal.fromActor and pass in the name of this actor, which 331
is OpenD. 332
And if I add a semicolon, then I should get rid of my errors, 333
and I can now go over here and get hold of the OpenD 334
Id. 335
Which is going to be through await 336
opend. 337
get-- 338
getOpenDCanisterId. 339
I'm just going to copy and paste it. 340
And once we got hold of that, that's what we going to be passing in to our transferOwnership as the 341
new owner. 342
Now, finally, because we're going to get a output from this, we're going to create a new constant 343
called the transferResults to capture the text that gets sent back after this method is called. 344
And as usual, I'm just going to console.log it. 345
Now, before we can test our code, we're going to need to deploy again. 346
So make sure that you save bothyoru nft.mo file and your main.mo file using command + 347
s on Mac or control + s on Windows. 348
Then go ahead and redeploy your canisters. 349
So again, either heading into the read me or if you click up, 350
it should still be the last command that you had run. 351
Just simply run the deploy so that all our canisters get updated. 352
Now, once it's deployed, we can head over Mint a new item. 353
And if we try to sell it, you're going to see an error right now. 354
So when I click confirm and now when we try to sell this new item, then it tells you that the error 355
here is 'Fail to verify certificate.' 356
So I did quite a lot of digging around this error code, and it turns out that the issue is related 357
to our dfinity agent. 358
According to the documentation, by default, the agent is configured to talk to the main live internet 359
computer blockchain and verifies the response using a hardcoded public key. 360
But it's not going to work when we're doing it locally. 361
So in order to get around that issue when we're doing local deployment, because let's face it, none 362
of us have enough ICP tokens to be deploying these canisters on the Internet computer. 363
Because remember, every single time we're minting an NFT that is creating a new canister, and if we're 364
doing it live on the Internet computer, that's another $5 or $10 for every single time we're testing 365
it. 366
So luckily, we don't have to spend that money. 367
We're doing it locally and we can simply fix this by going over here and calling 368
agent.fetchRootKey 369
which is spelt like this. 370
And this is going to tell it that while we're working locally, simply just to fetch the route key and 371
ignore the situation. 372
But it's important to remember that when deploying this project live, we should remove the following 373
line. 374
So now let's try that again. 375
And because our transfer already went through, will have to mint another NFT to test it out. 376
And you can just imagine that ka-ching in the background as we've saved ourselves something like $5 377
or $10 from minting this NFT and creating this canister. 378
So let's try selling this one that we actually own. 379
And once we hit confirm, you should see listing success and transfer success. 380
So we've actually managed to put NFT onto our HashMap of listings and we've managed to transfer the 381
ownership over to the OpenD canister so that when somebody buys it, it'll be able to transfer it 382
to them. 383
Because remember, the whole point is that only the person who owns the NFT can make the transfer. 384
And because we're not going to be staying around, you know, waiting for somebody to buy it and then 385
calling this method, we have to transfer the ownership when we list it to our OpenD website. 386
So now, even though not much has happened, we've actually written quite a lot of code. 387
So in the next lesson, we're going to tidy things up a little bit 388
and we're also going to make sure that when we list an item, we're able to see all the items that are 389
listed for sale in the Discover section. 390
So for all of that and more, I'll see you on the next lesson.