Now, let's write some JavaScript and make use of the web3 library. Don't worry if you are not familiar with web3, you will see more of that in Chapter 2, Web3 and Solidity in Truffle. For now, you will interact with web3 just enough to get a fully working Dapp.
In app.js, paste the following code:
import { default as Web3} from 'web3';
import { default as contract } from 'truffle-contract'
import taskMasterArtifacts from '../../build/contracts/TaskMaster.json'
By pasting in the preceding code, you just imported the following:
- Your CSS file, for Webpack bundling and build purposes (not important for now)
- web3—this is to connect with Ethereum blockchains
- truffle-contract—an Ethereum contract abstraction for the browser that allows you to invoke public smart contract methods from plain old JavaScript objects
- A JSON representation of your TaskMaster.sol contract file
Why do we import a JSON file and not a Solidity file?
Before you deploy a contract to a blockchain network, your contract needs to be compiled into bytecode understood by the EVM (Ethereum Virtual Machine), and provide an ABI (application binary interface). The ABI is the process for encoding your Solidity contract to be understood by the EVM, and this file is in a JSON format. When you run compile, you will notice that a build folder is created along with JSON data of your contracts.
Under our import statements, let's declare and initialize a few variables:
var TaskMaster = contract(taskMasterArtifacts);
var ownerAccount;
We use contract to get the JavaScript abstraction of our contract. This is necessary to call public functions such as getBalance and reward. As you can see, contract is a function that accepts your contract's ABI JSON file.
Next, we need to tell web3 which blockchain network to connect to:
window.addEventListener('load', function() {
window.web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:9545"));
});
On the load event of the window, we:
- Set web3 as a window object and give it our localhost (http://127.0.0.1:9545) as the blockchain network provider
- You will learn more about web3 providers in subsequent chapters
The moral of this story is that web3 requires a blockchain provider to allow us to interact with the said blockchain on the frontend. Notice that when you enter truffle develop, it takes you to a console that looks like this:
In particular, notice the statement at the top: Truffle Develop started at http://localhost:9545/. This is the same URL and port that we specified in the following code block.
Also, notice that in our local blockchain we have nine accounts with their respective private keys. These nine accounts are all the accounts in our mini, private, and local blockchain:
window.web3 = new Web3(
new Web3.providers.HttpProvider("http://127.0.0.1:9545")
);
Essentially, we are using a local copy of a mock blockchain.
Next, let's create a global object called TaskMasterApp to encapsulate logic involving listening for click events and performing DOM updates. Above the web3 code, paste the following code:
window.TaskMasterApp = {};
Our TaskMaster object also needs to be made aware of the current web3 provider. Let's add a method to TaskMasterApp:
setWeb3Provider: function() {
TaskMaster.setProvider(web3.currentProvider);
}
We'll call this method setWeb3Provider, because we want to set a provider for web3.
You can now paste the following as the final step inside the load callback, after setting a provider for web3:
TaskMasterApp.setWeb3Provider();
Go to https://localhost:8080/#/. You should see no errors, and nothing in the console.
Next, we should get all accounts associated with the current web3 provider, and set our ownerAccount variable to the first account. Let's add another method to TaskMasterApp:
getAccounts: function () {
var self = this;
web3.eth.getAccounts(function(error, accounts) {
if (error != null) {
alert("Sorry, something went wrong. We couldn't fetch your accounts.");
return;
}
if (!accounts.length) {
alert("Sorry, no errors, but we couldn't get any accounts - Make sure your Ethereum client is configured correctly.");
return;
}
ownerAccount = accounts[0];
})
},
As you can see, we are getting the first account and setting it to our ownerAccount variable. Also, you'll notice that web3.eth.getAccounts is not the most elegantly designed, as it requires a callback function. In later chapters, we will work with promises to avoid callback hell.
You'll notice that we don't use ES6 syntax here (var instead of const, and so forth). This is because we are still writing in vanilla JavaScript.
Remember, we are only focusing on one thing at a time. Once we get the fundamentals of Truffle, web3, and Solidity down solidly, we'll start looking into modern JavaScript technologies such as Angular, React, and Node.
Inside of the window load function, after we call TaskMasterApp.setWeb3Provider(), let's make a call to get all accounts:
TaskMasterApp.getAccounts();
Now that we have accounts, we can find a way to populate our status div. Let's create a method on TaskMasterApp called refreshAccountBalance. We are calling it refreshAccountBalance because we'll invoke this method again when we send ether to a doer to see the DOM update instantly. For this reason, we opt for a more descriptive name than, say, setAccountBalance:
refreshAccountBalance: function() {
var self = this;
TaskMaster.deployed()
.then(function(taskMasterInstance) {
return taskMasterInstance.getBalance.call(ownerAccount, {
from: ownerAccount
});
}).then(function(value) {
document.getElementById("accountBalance").innerHTML =
value.valueOf();
document.getElementById("accountBalance").style.color =
"white";
}).catch(function(e) {
console.log(e);
});
}
Notice how we call TaskMaster.deployed(). This returns a promise that resolves to a usable instance.
Then, we can get the owner's balance by calling taskMasterInstance.getBalance.call(account, {from: account}).
In this case, msg.sender is the owner's account.
Let's see it in action. Inside TaskMasterApp.getAccounts, add the following line of code at the very end:
self.refreshAccountBalance();
Now, you should see the balance updated on the DOM:
Now, let's create a method to reward a doer:
rewardDoer: function() {
var self = this;
var todoCoinReward = +document.getElementById("todoCoinReward").value;
var doer = document.getElementById("doer").value;
TaskMaster.deployed()
.then(function(taskMasterInstance) {
return taskMasterInstance.reward(doer, todoCoinReward, {
from: ownerAccount
});
}).then(function() {
self.refreshAccountBalance();
}).catch(function(e) {
console.log(e);
});
}
We invoke rewardDoer and pass in a receiver and amount, and the account that initiates this call is ownerAccount. That's msg.sender. Once the promise resolves, we refresh the owner's balance.
We need to attach a click handler on our button too:
<button
class="white pa3 ph4 bg-green br2 f3 b pointer"
onclick="TaskMasterApp.rewardDoer()">→
</button>
Enter any reward amount you wish, and see the balance decrease by that amount. Also, make sure you enter a valid Ethereum address.
I transferred 22 TodoCoin to 0x85db1e131b6c5c0c7eec98fed091a441ed856424. Here's what my screen looks like:
Once I submitted, my balance refreshed:
Now, let's see one last method involving TaskMasterApp to update our status div:
updateTransactionStatus: function(statusMessage) {
document.getElementById("transactionStatus").innerHTML = statusMessage;
}
Now, you can view the transaction status on the UI. Send some more TodoCoin to a doer, and look at the bottom of the screen. You should see the following words:
Transaction complete!