Solidity Workshop Guide - Pocked Edition - Part 2
Solidity Workshop Guide - Pocked Edition - Part 2
This is the part 2 of Solidity Workshop Guide, now tackling communication between JavaScript and a smart contract.
Let's start!
We'll use the already deployed contract Dask to play around with it.
Dask is deployed to Ropsten test network at the address 0x991238107f1823de55c6fb21162b059532c72496
. You can check it out on EtherScan.
To recap, Dask is a contract that allows anyone to ask a question and send bounty with it that will be rewarded to anyone who answers the question. The answers are a number between 1 and 5, so we can imagine someone asking a question like "How much did you like my blog post about Solidity?" and send some ether with it. Then the questions would come rushing in, with people answering with a number, so I would take the average of the answers and find out how much did people like my blog post on a scale from 1 to 5 :)
Interface
Dask has several methods that we can call:
getPricePerAnswer()
This method simply returns the price per one answer (view only). This is the amount of ether that will be rewarded to anyone who answers a question, and is defined in the contract. It is initially set to 10000 wei, a totally arbitrary number. But it can also be changed, by the creator of the contract, by calling the setPricePerAnswer method. You can try calling it, but it won't work, as it will allow only the address that created the contract to execute it.
askQuestion(string text, uint maxAnswers)
This method creates a new question that can be later answered by anyone. It changes state, so it will have a transaction fee associated with it. The first argument is text, so it would just be a string like "How much did you like my blog post". The second parameter, maxAnswers puts a limit on how many people can answer. I could say that I want 100 answers. Then if someone tries to answer the question after 100 answers, his will be rejected. Finally, along with the call to this method, I need to send ether that will be distributed to those who answer the question. I must send *maxAnswers* * *pricePerAnswer*
ether. If I send less than that, the call will be rejected.
The questions are indexed first by address and then by the text of the question, which seems reasonable, as it allows every address to ask a question once.
This method also fires an event called QuestionAsked, so anyone can listen to that event and know when a new question has been asked.
answerQuestion(address askedBy, string text, uint answer)
This method allows us to answer a question. It is a state chaning method, so it has a transaction fee associated with it. We must provide the address that created the question, as well as the question's text. We can get this info from the events fired by askQuestion method. Last argument we provide is our answer, and it has to be a number from 1 to 5. Every address can only answer the question once, therefore disabling people from trying to get the answer reward multiple times for one question. The second requirement is that there is less than maxAnswers answers already associated with the question.
If all requirements are met, this method records the answer, and transfers the reward in ether to you. Finally, it emits the QuestionAnswered event, so that the user who asked the question can be notified and follow the answers as they come in.
Interacting with the contract via JavaScript
We will use Metamask extension to manage our accounts and inject the web3
library into our page. If you're not familiar with Metamask, check out this tutorial and in the Part 3, instead of Mainnet, choose Ropsten test net, and you're all set!
Create a new html file, and let's get rolling! One note though, you'll need a local webserver to serve your html page in order for this to work, I recommend NodeJS package HttpServer. You can get it by running npm i -g http-server
, and then just go into the directory where you've got your html file, and run http-server
. Then visit http://localhost:8080
and you'll see your webpage!
We need to wait for the document to load, to give Metamask time to initialize:
document.body.onload = () => {
// your code here
}
Now, to interact with our smart contract from JavaScript, we need two things - its abi and its address. The address we already know (0x991238107f1823de55c6fb21162b059532c72496
), and the ABI you can find in this Gist.
let abi = [ { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "price", "type": "uint256" } ], "name": "PricePerAnswerSet", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "askedBy", "type": "address" }, { "indexed": false, "name": "text", "type": "string" } ], "name": "QuestionAsked", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "askedBy", "type": "address" }, { "indexed": false, "name": "text", "type": "string" }, { "indexed": false, "name": "answer", "type": "uint256" } ], "name": "QuestionAnswered", "type": "event" }, { "constant": false, "inputs": [ { "name": "price", "type": "uint256" } ], "name": "setPricePerAnswer", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "getPricePerAnswer", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "text", "type": "string" }, { "name": "maxAnswers", "type": "uint256" } ], "name": "askQuestion", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "askedBy", "type": "address" }, { "name": "text", "type": "string" }, { "name": "answer", "type": "uint256" } ], "name": "answerQuestion", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" } ]
let address = '0x991238107f1823de55c6fb21162b059532c72496'
let daskContract = web3.eth.contract(abi).at(address)
Okay! Now we can use our daskContract to ask a question. Let's hardcode the parameters for brewity, but in real life you'd have an input field and use the value of it when calling a method.
let question = 'How are you today?'
let maxAnswers = 100
let value = 10000 * maxAnswers
daskContract
.askQuestion(question, maxAnswers, { value }, (error, response) => {
console.log(error, response)
})
After reloading the page, Metamask will ask you to confirm the transaction. When you confirm it, it will send it to the Ethereum Ropsten network! And in the browser console, you'll see a transaction hash printed out as response
. This means that the transaction of this method call has been sent to the network, and now it's waiting to be mined (included in a block). You can open EtherScan and paste the transaction hash to watch it execute.
After some time it will be executed, which means that it is now officially included in the Ropsten network, and everybody can see it!
But, there's a problem with our code. We calculate value
by multipling by 10000
, as we know that is the initial price per answer. What if at some point the contract creator (me) decides to change that price? Your code won't work, the transactions for askQuestion will be rejected. So we need a way to first ask the contract what is the current price per answer, and then use that price when asking the question.
daskContract
.getPricePerAnswer((error, price) => {
let question = 'How are you today?'
let maxAnswers = 100
let value = price * maxAnswers
daskContract
.askQuestion(question, maxAnswers, { value }, (error, response) => {
console.log(error, response)
})
})
Events
Great! Now, let's create an event listener to load all questions that have ever been asked!
daskContract
.QuestionAsked(null, { fromBlock: 0, toBlock: 'latest' })
.watch((error, event) => {
let { askedBy, text } = event.args
console.log(text)
})
We can see that QuestionAsked
accepts two parameters when we want to start watching it. Both are filters - the first one (the one we set to null
) is for filtering events based on its indexed values. Since we didn't index any parameters for events in the Dask contract, we leave this empty. The second filter argument allows us to specify fromBlock
and toBlock
. By setting the value 0 for fromBlock
, we say that we want to get all QuestionAsked
events from the beginning of the blockchain history. If we set 'latest'
as the value for fromBlock
, we would not get any past events, but only those that happen after we first load the page. On the other hand, setting 'latest'
as the value for toBlock
allows us to keep listening for events as new blocks are added to the blockchain. Conversly, if we set a fixed value for toBlock
, we would only get QuestionAsked
events that happend up to that block number.
Next steps
Whoah! We took a look at interacting with our contract by calling methods that modify the state and those that just read from it. We saw how we can use events to keep track of what's happening on the blockchain in real time. Now, you can try to implement the answerQuestion
functinality on your own!
If you get stuck, or have any questions, feel free to ask them in the comments below. I'd love to help!
Cheers! 🎉🎉🎉