During the last couple of months, I have been developing a sample web3 dApp project to learn:
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:
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:
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:
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:
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)
}
})
},
},
}
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!