To date, we've been writing and testing contracts using JavaScript tests.
Those tests are a lot like our frontend code that we need to create in order to allow users of our decentralized applications (dapps) to talk to the Ethereum Blockchain.
However, there's a lot of initial setup and network configuration code that was automated (black boxed) by our test runner.
User experience, waiting for transactions and returning the updated state to the application and user interface are also a primary concern for developing these dapps.
There are 2 versions of Web3.js, a pre 1.0 release and a post 1.0 release in beta.
The pre-release will work with any version of TruffleContract, so it's fine to use the two interchangably. This release does not have promises and uses callbacks.
If you want to use promises with the pre-release, you need to use a tool like promisify and it's quite easy to do so.
The post 1.0 release is nice, but the interface for all functions has changed.
You will not be able to use TruffleContract.js with this release for now.
See the following to clarify this: https://github.com/trufflesuite/truffle-contract/issues/109
If you want to use Web3.js 1.0+ and connect to your contracts manually you can learn about this here:
https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html
The following notes will assume you are using TruffleContract so keep this in mind.
Now that you have Web3.js version < 1.0 and TruffleContract loaded into your web app, you will need to load your application binary interface (ABI).
The ABI can simply be the small JSON snippet that is produced when you compile on the remix editor for example.
However, when using Truffle a much larger JSON file is going to be found in your build/contracts
folder if you use migrate
from the truffle develop console.
This larger file allows TruffleContract to grab the latest deployed contract on the local development network, without the need to copy and paste addresses from your local Ethereum blockchain after deploying contracts. Making your development iteraction cycle from contract to frontend much faster.
You will need to use the fetch API or if you're using a build tool, import this JSON into your site.
The following is an example from a project. It is importing the generated JSON file from Truffle directly.
import * as DogeMemeToken from '../contracts/DogeMemeToken.json';
const dmtContract = window.TruffleContract(DogeMemeToken);
dmtContract.setProvider(window.web3.currentProvider);
// address is optional, if excluded you will return the latest contract on the network provider.
const dmt = await dmtContract.deployed(address);
From here, any contract functions can be called exactly how you would in your truffle tests.
Here is an example of using the dmt contract instance:
const from = 0x0; // from address
const to = 0x1; // to address
const value = 10000 // amount to transfer
//attempt to transfer
try {
await dmt.transfer(to, value, { from });
} catch(e) {
console.log('ERROR', e);
}
// get updated balance
const balance = (await dmt.balanceOf.call(from, { from })).toString();
const withdrawn = await dmt.withdrawn.call(from, { from });
Here is a sample from a project of detecting web3 and explicitely setting the network to 'development' with truffle develop.
const networkId = 99; // force development network
const { Web3 } = window;
const host = 'http://127.0.0.1:9545/'; // fallback host if network is 99
let web3Provider;
if (typeof window.web3 !== 'undefined' && networkId !== 99) {
web3Provider = window.web3.currentProvider;
} else {
web3Provider = new Web3.providers.HttpProvider(host);
}
window.web3 = new Web3(web3Provider); // overwrite the provided web3 context (in case MetaMask was injected)
// calling web3.eth.getAccounts using promisify
const accounts = await promisify(cb => window.web3.eth.getAccounts(cb));
When the user decides to make a transaction, typically this will pop up a dialog where the user takes control over some important properties of the transaction:
- ETH value to be sent
- gas price per unit gas (typically measured in gwei) https://etherconverter.online/
- gas limit, the upper bound of gas units the user will allow for this transaction
- data payload (function arguments)
If the user rejects the transaction an exception is thrown. Hence the try catch
block above.
If the user accepts the transaction, a tx hash is returned in order to track the transaction.
Now the frontend app has 2 options:
- poll the blockchain using the tx hash to see if the block is mined and the state was updated, this can be costly for mobile devices but has the highest success rate of being able to notify the UI when it's time to update.
- listen for events from the blockchain through the user's client and hope to catch the event when it's fired.
The second approach has issues with connectivity and your application will need to store some reference of the last transaction made in either scenario if the user decides to close the app and re-open it at a later date.
You can store transactions locally in localStorage until the events / blocks mined in question resolve with the expected new state and then clear them.
Or this could be handled by a backend microservice with a registry of user transactions as well, especially if your application stores other user data that cannot / does not need to be on the blockchain.
When a transaction is accepted you will want to store the following information immediately in your code:
- current state
- current block
- transaction details, function name, arguments etc...
- optimistic new state
Using the returned tx hash from a TruffleContract call you can use the following method to get information about the transaction: https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethgettransaction
The transaction object will tell you whether the transaction has been mined or not.
Comparing block numbers when the transaction was accepted and broadcast with the block number of the mined transaction is not ideal, but fairly fault tolerant provided you keep a backup of the information listed above in case of network connection / closed app.
Subsequently, checking for the new expected state, provided the transaction does not fail, can work in most instances, but it's a lot of back and forth to the blockchain.
The following article does a great job explaining how to listen for events: https://coursetro.com/posts/code/100/Solidity-Events-Tutorial---Using-Web3.js-to-Listen-for-Smart-Contract-Events
Instead of creating a new event watch function for every event, you can throw the contract event function into this function to generate a watch that will log all the events.
const logEvent = (func, fromBlock) => {
const event = func({ _from: web3.eth.coinbase }, { fromBlock, toBlock: 'latest' });
event.watch(function(error, result){
console.log('*** EVENT ***' + result.event);
result.args.forEach((arg) => console.log(arg));
});
}
Usages: logEvent(contract.MyEvent);
Standard UI / UX practice is to avoid letting your user error in the first place. So before you go implementing snack bars and alert dialogs / modals for error handling. Ask yourself if you can avoid all but the wrong gas limit / transaction data errors prior to showing the user errors.
Scan the blockchain prior to allowing the user to send the transaction and provided no one else's transactions changes state causing their transaction to fail, or the gas limit isn't hit, things should go as expected.
This would be called optimistic UI updating if you were to show the new state prior to the blockchain committing it.
You must also keep track of the pending transaction and prevent your user from doing another transaction while this one is pending, since they could conflict with each other based on the result of the first transaction.
In any event, attempt to prevent the user from seeing errors and limit + explain their limitations in the UI prior to making transactions.
Each time the user's transaction fails, they lose Ether in gas costs.