During the last couple of months, I have been developing a sample web3 dApp project to learn:
- How to develop Smart Contracts as backend for web3 dApp?
- How to develop a VueJS based web3 dApp to interact with MetaMask wallet and Smart Contracts?
- How to deploy Smart Contracts to multiple EVM-compliant blockchains?
- What are the differences between different blockchains?
Use Cases
The sample dApp is called "Decentralized Bookstore", allowing anyone to sell/buy books using cryptocurrencies. A NFT is minted at the same time as the book is purchased. The user can import the NFT into MetaMask wallet, shown in tab “My Books”. Here are the main use cases and the related UX screenshots:
- Sell Book: powered by Bookstore smart contract's sellBook method, a book owner sells "Harry Potter" at the website, setting the price at 0.05 ETH.
- Buy Book: powered by Bookstore contract's buyBook method, another user purchases the book "Harry Potter", paying 0.05 ETH to the seller and receives an NFT as proof of purchase:
- Import NFT: the buyer imports the minted NFT into MetaMask wallet:
Application Architecture
The front-end is a Vue 3 based Single Page Application (SPA) which can be deployed to any server or IPFS as a static web site. After the SPA is loaded in browser, it interacts with the backend Bookstore smart contract which has been deployed to following blockchains:
- Layer 1 (L1)
- Ropsten
- Rinkeby
- Kovan
- Goerli
- Layer 2 (L2)
- Arbitrum Rinkeby
- Optimism Kovan
- Sidechain
- Poloygon Mumbai
- Meter
Learnings & Know-hows
Comparison of Transaction Speed & Fee on Different Blockchains
As shown in the screenshots above, the dApp captures the transaction times and displays Etherscan links from which we can find out transaction fees. Here is a summary table to compare transaction speed & fee on different blockchains:
For any blockchain network, the transaction confirmation times and fees vary from time to time, depending on how busy the network is. The above table only provides some sense on the transaction speeds in different networks. During the development, I have observed the following:
- Among tested L1 networks, Rinkeby and Kovan are generally faster than Ropsten and Goeli.
- As a PoW L1 network, Ropsten is usually slow. Sometimes the transaction confirmation can take more than 200 seconds.
- Among tested L2 networks, Arbitrum Rinkeby testnet delivers very fast transaction speed, usually transaction can be confirmed within 1 second; while Optimism Kovan's transaction speed is similar to Kovan's, ie, 10+ seconds, its transaction fee is much lower than that of Arbitrum's (~ 1/56K)
- L1/L2 ethers can be exchanged at Arbitrum Bridge, Optimism Bridge or third party bridges.
- Deposit from L1 to L2 balance takes only couple of minutes, while withdraw from L2 to L1 balance will take ~7 days to process
- This withdrawal waiting period is to allow time for fraud proofs to be submitted and maintain security of the network.
- To receive the funds on L1, the user must send a transaction on L1 which will incur a second transaction fee - see below a screenshot captured at Optimism Bridge:
- Both sidechains (Ploygon Mumbai and MTR) deliver transaction speeds similar to L1 PoS blockchains
How to Deploy Smart Contract to Different Blockchains?
With an Infura developer account properly set up, we can set up package.json and truffle-config as below to deploy smart contracts to different blockchains:
How Can Front-end Interact with Smart Contract in Different Blockchains with JavaScript code?
Once a smart contract is deployed to all supported blockchain networks, the compiled contract JSON files can be copied to front end codebase and then deployed along with other assets:
At runtime, after web3.js (Ethereum JavaScript API) is properly initialized, the front-end will be able to retrieve public testnet contract address from the compiled contract JSON:
getContractInfo(contractJson) {
this.initBcInfo()
const currencySymbol = this.bcInfo.currencySymbol
const contractName = contractJson.contractName
const envKey = 'VITE_BOOKSTORE_CONTRACT_ADDRESS'
let contractAddr = import.meta.env[envKey + '_' + currencySymbol]
if (!contractAddr || contractAddr == '') {
const network = contractJson.networks[this.bcInfo.networkId]
contractAddr = network ? network.address : import.meta.env[envKey]
}
const result = { contractAddr, contractName }
console.log(
`getContractInfo(${currencySymbol}): ${JSON.stringify(result)}`,
)
return result
},
Then it can create web3.eth.Contract object as below, after which the front-end will be able to call any public method defined in the smart contract:
initContractJson(compiledContractJson, contractInfo) {
var abiArray = compiledContractJson['abi']
if (abiArray == undefined) {
const error = 'BcExplorer error: missing ABI in the compiled Truffle JSON.'
console.error(error)
throw new Error(error)
}
const { contractAddr, contractName } = contractInfo
if (!this.web3().utils.isAddress(contractAddr)) {
const error = `wrong contract address - contractAddr: ${contractAddr}`
console.error(error)
throw new Error(error)
}
const contract = new this.web3inst.eth.Contract(abiArray, contractAddr)
contract.currencySymbol = this.info.currencySymbol
this.contractInst[contractName] = contract
this.contractAddr[contractName] = contractAddr
console.log(`contract with name ${contractName} initialized`)
}
As different blockchains require different gas fees, instead of suggesting them from the code, it would be better to let them handled by MetaMask by setting both maxPriorityFeePerGas and maxFeePerGas options to null:
invokeSmartContract(
contractName,
method,
getContractInfo,
updateTransactionStatus,
) {
const contractInfo = getContractInfo(contractName, method)
const option = {
from: contractInfo.address,
maxPriorityFeePerGas: null,
maxFeePerGas: null,
}
if (contractInfo.value) {
option.value = contractInfo.value
}
contractInfo.method
.send(option, (error, txHash) =>
this.sendTransactionCallback(
contractName,
method,
error,
txHash,
updateTransactionStatus,
),
)
.then((txReceipt) =>
this.handleTransactionReceipt(
contractName,
method,
contractInfo.bookId,
txReceipt,
updateTransactionStatus,
),
)
.catch((error) => {
this.userInteractionCompleted({ result: 'error', error })
if (updateTransactionStatus) {
updateTransactionStatus(error, null)
}
})
},
},
}
Where to find the sample dApp?
- GitHub repo for both Solidity-based Smart contracts and Vue3-based SPA:
https://github.com/inflaton/decentralized-bookstore.git
- dApp deployed at Netlify:
https://decentralized-bookstore.netlify.app/
-
dApp deployed at IPFS:
https://dweb.link/ipfs/QmXtNEZm6nMoH5y8J3pBf5gdepsL9z1737GVK3YKsQcsNq/
Hope you have enjoyed the reading and will find this sample dApp useful. If you have any question/comment on the code, feel free to reach out to me via twitter: @inflaton_sg. Have a good one!