We've already managed to tackle two-thirds of our website, the fauceting, the ability to check for
balance.
The final section we want to tackle is to transfer some sort of amount from our account, so the user
who's either logged in or using the website, to another account.
And that's what we're going to be doing in this lesson. Heading back into our main.mo,
we're going to create a new public function and this one is going to be called transfer.
Again, let's make sure that we cap off the previous functions with any semicolons that we need.
And the first thing I want to show you within this is a bit of a demo, because we know that when we
use that shared message, we can get hold of the caller of the function,
but who is the caller when a function is being called from another function?
If I go ahead and just comment out all of this apart from a single return so that it doesn't get angry
at me, and I'm going to Debug.print who the message caller is in this case. From the transfer function,
I'm going to call the payOut() function.
What do you think will happen?
What do you think will actually get printed?
Let's hit Save.
Let's deploy our canister.
And let's go ahead and check out what we're going to get printed in the console from our payOut() function.
As we saw previously, we could call that function from the command line, in which case the msg.caller
is going to be our user Principal ID.
So let's try that again.
I'm going to call, "dfx canister call token payOut",
and again, notice that you've got the correct spelling for your method.
In my case, I have a capital 'O' here for payOut.
So when I go ahead and hit Enter, it's going to call this function and that msg.caller is going
to be logged here.
And as you can see, that is my user Principal.
This is my Principal ID for the user that controls this command line.
Now, the other way we saw that we could call this function was from our frontend.
We could go ahead and click on this button,
and when that's pressed, this function also gets triggered
but the caller in this case is the user that is using this frontend website, which is this ID.
So now we've got the final one,
what if we call this transfer() method, which then calls the payOut() method?
So in this case, who is the message caller?
Well, let's try that here.
So we're going to say, "dfx canister call token...", which is the name of our canister, and then we're going
to call the name of the method, which is "...transfer".
Now when I go ahead and hit enter, it's going to trigger this function which is going to trigger
this function, which is going to print out the message
caller.
Now, I want you to just think for a moment, what is it going to print?
Who will be the caller?
Let's go ahead and hit Enter and find out.
Well, you can see that this ID is neither the frontend Principal ID when we clicked the button to call
that method, nor is it the user Principal that we called previously from our command line,
instead, it's this. And you might be a little bit familiar with this because every time we do dfx deploy,
you'll see it.
It says, "Deploying all canisters. All canisters have been created."
And then it says something about, "Upgrading code for canister token, with the canister_id", and there it
is.
It matches this.
Essentially, what's happening here is when you call a method from another method, it's this canister
that's actually calling this method.
So in this case, the msg.caller is this entire actor, this token canister.
That's why we get that canister ID being printed here.
This is really important in order for us to make our transfer function work.
Let's go ahead and restore some of this code for our Faucet function, and let's work on our transfer
function for real this time.
Firstly, we're also going to make this a public
shared function with the parameter, msg,
and we're also going to give it an async return, which is going to be a piece of text similar to before.
Now, what do we want to do when we transfer some token from one account to another?
Well, we want to subtract the amount that they want to transfer from the account that's being transferred
from,
and then we want to add that amount to the account that's being transferred to.
Let's delete this line which we don't need.
That was just for testing
to show you what actually happens. In this transfer() function, we need to add two parameters and that
is the to,
so who are we going to transfer to, which is going to be a Principal ID type,
and then we also need to have the amount which is going to be the amount that we're going to transfer
to that particular account.
Now you'll notice that I don't have the transfer from, in this function.
Why is that?
Well, the transfer from is going to be the message
caller.
So whoever triggers this function is going to be the one where the money is going to be taken from.
Because if you think about it, when we called this method from the frontend, when we clicked on that
button, the debug_show, showed us that the msg.caller is the Principal ID of the frontend user.
When we do that here, when we click on this button, it's also going to take the from account as the
principal of the person using the website and then they're going to provide the Principal ID of the
account they want to transfer to, and the amount they want to transfer.
So then it's just a matter of a little bit of math.
The first thing we can figure out is what is the fromBalance?
So the person who is going to be doing the transfer.
Well, we can work that out by using our balanceOf() method and we can pass in the msg.caller
as the principal.
Now, if you're worried about these error messages, it's because we're expecting a return here of type
text.
If we just add a, return "Success", at the very bottom, then we can actually write our code in peace.
Once we've gotten hold of the balance of the account we're transferring from, well then we can do
a quick check to see if the fromBalance is greater than the amount that we want to transfer because
otherwise we can't really do the transfer, right?
If they have enough money, we're going to return "Success",
but otherwise we're going to return "Insufficient funds".
And remember that this balanceOf returns asynchronously.
So in order to fix this error, we have to make sure we have the await in front here.
What we're doing here is we're going to get the balance of the caller of this transfer method, and we're
going to wait for it until it's done.
And then we're going to check to see if they actually have enough money, if they have more money than
they want to transfer.
And if they do, then we're going to actually make the transfer go through.
So what does that actually mean in terms of math?
Well, we're going to subtract this amount from their balance and add this amount to the person they
want to transfer to.
It's pretty simple.
Just a bit of addition and subtraction.
So let's create a new constant.
Let newFromBalance = fromBalance - amount.
And because Motoko thinks that this can go below zero, even though we definitely know it can't, we
have to add a data type explicitly here and that will make that warning go away.
Now, once we figured out the new balance of the account that we're transferring from, then we can
update our ledger of balances to this new amount for this user, which is msg.caller.
So we can say balances.put, and we're basically going to replace the entry here.
So we're going to put in the msg.caller's ID and then this is the key.
And then for them we're going to update their value to the newFromBalance.
And this basically just does the subtraction from the account.
The other aspect is to add the amount to the account that is being transferred to.
But let's first figure out what is the balance of the owner that we're transferring the money to. So we can
say, let toBalance = balanceOf, and then we pass in the Principal ID, which comes from this, to,
here, which basically the user is actually going to type in or paste in. And it's going to be the long
form sort of principal ID of the account that they want to transfer to.
Now, once we figured out their balance, and you'll notice that in our balanceOf method, we've already
taken care of the case where that user doesn't actually exist.
Then we're actually just going to assign them a balance of zero.
Once we've done that, the next step is to add the amount that we want to transfer,
so the newtoBalance, which is the toBalance + the amount that we want to transfer.
And again, we have to await for the results of balanceOf before we can execute this line,
so adding the await keyword before the balanceOf.
And finally, we're going to put into our HashMap the new updated balance.
The key is the Principal ID of the to account, and the value is the newToBalance. In those few lines
of code
we've just done a little bit of simple math.
We've subtracted the amount from the balance of the sender, and we have added the amount to the balance
of the recipient.
And then we've basically updated our ledger with this new information. And then we returned "Success"
if they had enough balance, which means all of this should succeed, but otherwise we return "nsufficient
funds".
So now let's make sure that we hook this up with our frontend and that we actually call that method
in our handleClick(). When the user presses the transfer button,
we want to call that method in our token canister, so let's import our token canister.
So ../, three of them /declarations/token.
And then we can call, "await token.transfer", and I believe it is spelt lowercase. And then we have
to provide the two inputs that are required.
So the first one is the to. That is going to come from the first input field where we've got the transfer
to, which is this one here, and we're going to do this in the same way that we did for our balance
checking.
So we had a value and then we had an onChange listener.
So the first thing we need to use our useState Hook in order to hook up our input so that it has a value
and it has an onChange listener.
The value is going to be tied to a constant, which I'm going to call the recipientId
,
and I'm going to have a setter called setId.
And this is going to of course, useState.
And we're going to set it to an initial value of an empty string.
So now we can set this value to the recipientId constant,
and when that gets changed, we're going to use that event listener to update the ID by using our
setId setter.
And we're going to set it to the "e.target.value", which will basically give us the value
of the input at the moment when it changes.
It's going to keep updating this recipientId
and then in the moment when we click on the button, then we'll be able to take the final value of the
recipientId and pass it over to that token.transfer() method.
The other thing that we need to set up is the amount.
We're going to do something pretty similar.
We're going to have our value and we're going to have our onChange.
That means that we again have to set up a new constant.
So I'm going to call that the amount and setAmount.
And it's going to start off with a value of, again, an empty string.
And we can update this in here in the same way that we've done before.
And I've just noticed that I've misspelt my Amount, so I have to make sure it's correct in both places,
otherwise it won't work.
All right.
So now we should be able to bring the values inside these inputs in, and I should be able to get hold
of it here and use it in my transfer() function.
Now, there's a couple of problems with using the recipientId or the amount straight from the inputs,
and this is because the amount is going to come in as a piece of text and when we call the transfer() function,
we actually need it to be a number.
And similarly, the recipientId needs to be a valid Principal type rather than a simple piece of text.
Let's go ahead and fix that by importing the Principal data type.
And let's go ahead and create the recipient as the Principal.fromText,
and then we're going to pass in the recipientId that we got from our state Hook.
And similarly, we're going to convert our amount as a new constant amountToTransfer, and we're going
to convert it into a number from the string.
So now we can replace this recipientId with our new recipient const and the amount with our new
amountToTransfer const.
And now, finally, we can test our code.
Make sure you run, "dfx deploy".
Let's copy the principal ID of the frontend.
If I first check my sender balance, you can see I've got zero DANG because we did dfx deploy,
so the ledger has been reset.
So let's just reclaim it from the faucet.
And now if we check it, you can see we have 10,000 DANG. And I'm going to transfer it to my user's Principal
ID and I'm going to transfer 50 DANG.
Now if I click Transfer and if I check my balance again, you can see 50 has now been subtracted, and
if I check the balance of the recipient, you can see that 50 has been added to its balance.
So our transfer method finally works.
Now that we've got the functionality down, let's go ahead and add some of the user experience touches
that'll improve the way that our website works.
Now the first thing we're going to do is we're going to make this button disabled when the transfer
is happening. And once we get the results back, then we can enable it again.
So we'll go ahead and set the disabled attribute to a new constant.
We'll call it isDisabled and we can create our constant up here.
And we'll also add a setter for setDisable.
And we'll start it out with, false, so that we can actually tap on the button, and then as soon as the button
gets clicked, we'll set it to true.
And once the process has completed, then we can set it back to false again.
In addition, when we call our token.transfer, we actually get a result that comes back in the form
of text.
And if you remember, it can either be, "Success", or it can be "Insufficient funds".
Now we want to go ahead and capture that.
So let me create a const called result and set it equal to the result or the output of this transfer
function call.
Now we want to display that in a paragraph element.
So just above the last two divs, let's go ahead and add a paragraph element.
And inside this paragraph, we're going to add some feedback.
Now, this feedback is going to be a new constant,
starting out with an empty string,
but then as soon as we get the results back from our token.transfer, then we're going to set the feedback
to whatever that result is.
This feedback probably shouldn't exist to begin with, so we can create another const, which is called
isHidden, and we can use the setHidden setter to hide that paragraph element.
And it's going to start out with true.
So we're going to start out by having this paragraph element have a hidden attribute set to the constant
we've got, isHidden.
And then as soon as we get our result back and we set our feedback, then we can go ahead and set the
hidden to false.
And the next time the user clicks on the button, we're going to set the hidden to true again.
So we can copy our Principal ID, go back and try to make another transfer.
Click Transfer and you can see this time the transfer button goes gray and becomes disabled when that
process is going through,
and once it's done, it gives us, "Success".
And if I try to transfer some amount that my account doesn't have, because remember, I only have 10,000,
and I try to transfer, say 100,000, then it's going to think a little bit and then it's going to tell
me "Insufficient funds".