Speeding Up dApp Interactions on Opera
TL;DR
- Avoid long block ranges or large expected response sizes.
- Avoid hammering broad queries. Ideally, use an API endpoint with request load balancing and high availability.
- If the transaction is supposed to execute a smart contract code, ensure the code emit relevant log records, which can be subscribed to via WebSocket and observed in near real time.
- For state-changing interactions, use a log filtering request (
eth_getLogs
) to query expected log records over the latest blocks. Be as specific as possible by including the contract address, event log topics, and block number for a fast response. - If the nature of the state-changing transaction cannot be determined, poll the signing account’s transaction index (
eth_getTransactionCount
).
With the Opera chain upgraded, the ecosystem should enjoy stable time to finality (TTF) and improved network throughput. Yet, the perceived responsiveness of the network highly depends on the real-life experience of our users. That makes the engineering decisions related to interactions with dApps one of the most important parts of the development process — we want to ensure dApp performance is as snappy as possible. Below are some insights:
If an interaction doesn’t change the network state, the response is usually very fast. You query an API endpoint and get a response with the data needed. The goal here is to be as specific as possible. But a long block range or large expected response size impacts user experience due to long-running API calls and/or response parsing time.
It’s okay to send several RPC calls per second to get a short and very specific answer, but avoid hammering broad queries. Ideally, the API endpoint used for your calls should provide request load balancing and high availability. You may want to consider a fallback API endpoint in case the primary one is not responding.
State-changing interactions require a transaction to be signed, submitted, and executed by the network. This is where the network TTF plays a critical role. Also, the user needs to be notified when the transaction is finalized. There are two distinct cases to be considered, as outlined below.
If the transaction is supposed to execute a smart contract code, we recommend the code emit relevant log records. The event log can be subscribed to via WebSocket and observed in near real time. Another option is to query the expected log record over the latest blocks using a log filtering request (eth_getLogs
). If the filter is tailored to be as specific as possible and include the contract address, the event log topics, and the block number, the response will be very fast.
If the nature of the transaction cannot be determined, the best approach is to poll the signing account’s transaction index (eth_getTransactionCount
). Each transaction sent to the network has a distinct nonce. Once the account transaction count reaches at least this nonce, you know the transaction in question is finalized. This is generally preferable to querying a transaction receipt. A receipt is a complex structure that requires multiple database interactions on the API side. It means you get the response later and it will take more time to process it. In contrast, the transaction count is a single number that’s pulled from a single data source and is probably already cached on the API.
Sample App — WrapFTM
We built a sample dApp to show you four ways of achieving fast connection with RPC.
Step-by-Step Approach
To implement the best practices mentioned above, follow this 5-step process to improve the time to finality of your dApp.
- Compare your dApp transaction confirmation speed with the sample WrapFTM dApp using a compatible wallet.
- Look at the 4 proposed ways of speeding up the confirmation times on the GitHub for the sample dApp.
- Select the most appropriate RPC call method for your dApp.
- Implement the selected method into your dApp.
- Enjoy higher user volume and satisfaction.
RPC Call Example
- Poll Tx Count
{"jsonrpc":"2.0","id": 1, "method":"ftm_getTransactionCount","params":["0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83", "latest"]}]}
- Poll Tx Receipt
{"jsonrpc":"2.0","id": 1, "method":"ftm_getTransactionReceipt","params":["0x99ff1643f3c74108478eee6277c4c47c1e6e6348732b4e735e8c4d6a44db6da1"]}]}
- Subscribe to an Event (ERC-20 Transfer on WFTM)
{"jsonrpc":"2.0","id": 1, "method": "ftm_subscribe", "params": ["logs", {"address":"0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}]}
- Query Log
{"jsonrpc":"2.0","id": 1, "method":"ftm_getLogs","params":[{"fromBlock":"0x51EA32B","toBlock":"0x51EA32B","address":"0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}]}
- Subscribe to Head (Option How to Get Impulse for Txcount/Txreceipt/Querylog Calls)
{"id":1,"jsonrpc":"2.0","method":"ftm_subscribe","params":["newHeads"]}
Web3 Call Examples
const {Web3} = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("https://rpcapi.fantom.network/"));
const address = "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83";
async function getTransactionCount() {
try {
const count = await web3.eth.getTransactionCount(address, "latest");
console.log("Transaction count:", count);
} catch (error) {
console.error("Error getting transaction count:", error);
}
}
getTransactionCount();
const {Web3} = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("https://rpcapi.fantom.network/"));
const txHash = "0x99ff1643f3c74108478eee6277c4c47c1e6e6348732b4e735e8c4d6a44db6da1";
async function getTransactionReceipt() {
try {
const receipt = await web3.eth.getTransactionReceipt(txHash);
if (receipt) {
console.log("Transaction receipt:", receipt);
} else {
console.log("Transaction receipt not found yet.");
}
} catch (error) {
console.error("Error getting transaction receipt:", error);
}
}
getTransactionReceipt();
const {Web3} = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider("wss://rpcapi.fantom.network/"));
async function subscribeToEvent() {
try {
const subscription = await web3.eth.subscribe("logs", {
address: "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83",
topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
})
subscription.on("data", (event) => {
console.log("New ERC20 transfer event:", event);
})
subscription.on("error", (error) => {
console.error("Subscription error:", error);
});
} catch (error) {
console.error("Error subscribing to event:", error);
}
}
subscribeToEvent();
const {Web3} = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("https://rpcapi.fantom.network/"));
const blockNumberFrom = "0x51EA32B";
const blockNumberTo = "0x51EA32B";
const address = "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83";
const topics = ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"];
async function queryLogs() {
try {
const logs = await web3.eth.getPastLogs({
fromBlock: blockNumberFrom,
toBlock: blockNumberTo,
address,
topics,
});
console.log("Queried logs:", logs);
} catch (error) {
console.error("Error querying logs:", error);
}
}
queryLogs();
const {Web3} = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider("wss://rpcapi.fantom.network/"));
async function subscribeToNewBlocks() {
try {
const subscription = await web3.eth.subscribe("newHeads")
subscription.on("data", (blockHeader) => {
console.log("New block:", blockHeader);
// You can trigger calls to getTransactionCount, getTransactionReceipt or queryLogs here based on the new block data.
})
subscription.on("error", (error) => {
console.error("Subscription error:", error);
});
} catch (error) {
console.error("Error subscribing to new blocks:", error);
}
}
subscribeToNewBlocks();