Scaling Your DApp with QuickNode: Tips and Strategies for High-Performance Applications

Scaling Your DApp with QuickNode: Tips and Strategies for High-Performance Applications

Unlocking the Full Potential of Your DApps with QuickNode

Achieving scalability for your DApp involves a delicate balance between security, decentralization, and performance, which requires careful trade-offs. To begin the process, you must decide which blockchain to build on and how to connect your smart contract to the frontend application. QuickNode provides support for various chains and offers a rapid connection between your DApp and the blockchain.

This blog post explores ways to enhance your DApps using QuickNode services. We'll demonstrate how to create a straightforward ERC20 token information tracker that displays essential token details. Then, we'll utilize QuickNode services to scale up the tracker for maximum efficiency.

Register for a free account if you haven't already on QuickNode.

D_D Newsletter CTA

Functionalities

In this post, we'll develop small-scale applications to demonstrate how you can scale your DApps. The tips and tricks we explore can be applied to your real-world applications. We'll delve into how to connect your DApps to the blockchain using a custom QuickNode RPC URL and use the QuickNode Token API to obtain token contract information with ease.

Additionally, we'll share information on how to compress blockchain response data received during queries to enhance performance. Lastly, we'll cover the QuickAlert service provided by QuickNode and show how to use it to scale up your DApps.


The Tech Stack

  • RPC Provider: QuickNode

  • Frontend: Nextjs, Javascript


The Prerequisites

This article assumes the reader is familiar with creating a smart contract and connecting same to a front end.


Code Setup

We will take an overview approach to improve the scalability and functionality of our small token information DApps by leveraging the services offered by QuickNode. We will manually build and retrieve information about a token from the Ethereum blockchain and show how easily we could do the same using a QuickNode service. We will also discuss the difficulties we might encounter when retrieving historical data about a Token and show how easy the same task is when using QuickNode.

Connecting to a Node

We must first connect to a remote procedure call node (RPC) to interact with the blockchain network. QuickNode offers this service, so we can use a custom RPC URL to connect to a QuickNode node.

We should use a custom RPC provider rather than the public one for a production-grade application because a custom provider can scale and be well-tailored to your application's needs. Public RPCs are shared between multiple applications, and you might experience rate-limiting and dropped connections.

After obtaining an endpoint URL from QuickNode, the following step will demonstrate how to connect a frontend application.

To create a Next.js project, run the following command:

$ npx create-next-app@latest

RainbowKit provides a simple drop-in solution for wallet management for your DApp. It handles the connection and disconnection of wallets and supports multiple wallets, while the WAGMI package provides React hooks for wallet account management.

Open the directory of our Next.js app in the command line, and install the packages with this command:

$ npm install @rainbow-me/rainbowkit wagmi ethers

At this point, we have a simple Next.js application, which we are ready to configure. Navigate into the src/pages/_app.js, where we will configure wagmi and the rainbow-kit package.

Add the following code to the file:

File src/pages/_app.js:

import "@/styles/globals.css";
import "@rainbow-me/rainbowkit/styles.css";
import { getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { chain, configureChains, createClient, WagmiConfig } from "wagmi";
import { mainnet, polygonMumbai } from 'wagmi/chains';
import { jsonRpcProvider } from "wagmi/providers/jsonRpc";
import { publicProvider } from 'wagmi/providers/public'

const { chains, provider } = configureChains(
  [mainnet,polygonMumbai],
  [
    jsonRpcProvider({
      rpc: () => ({
        http: `replace-with-your-quicknode-rpc-url`,
      }),
      priority: 0
    }),
    publicProvider({ priority: 1 }),
  ]
);

const { connectors } = getDefaultWallets({
  appName: "QuickNode App Scaling",
  chains,
});

const wagmiClient = createClient({
  autoConnect: true,
  connectors,
  provider,
});

export default function App({ Component, pageProps }) {
  return (
    <WagmiConfig client={wagmiClient}>
      <RainbowKitProvider chains={chains}>
        <Component {...pageProps} />
      </RainbowKitProvider>
    </WagmiConfig>
  );
}

We insert our RPC URL in the jsonRpcProvider function.

We only want to connect to the Ethereum Mainnet and the Polygon Mumbai Testnet network. We also define a publicProvider with a priority of 1 so our DApp can fall back to it if our private provider fails.

QuickNode provides features that ensure our RPC URL is secure. Exposing your RPC URL as done here in the frontend application can have severe consequences, as it could fall into the wrong hands and be used in nefarious ways to pile up bills. QuickNode allows us to set a referrer header policy, ensuring that the RPC only answers requests from the domain we specify. It also offers token authorization.

Functional Steps

In this section, we will set up a small application that displays meta-data about the USDT Token. We will also leverage QuickNode services to display information about any ERC20 token. Finally, we will show how to retrieve transaction information of any wallet in any ERC20 token, made possible by the QuickNode Token API.

Setting a Referrer Header

Login into your account and click the "Endpoints" button on the left-hand side of the screen. This will open up your available endpoints. Click on the three horizontal dots/points to open up the sub-menu below.

Click on the "Security" button to open the page. Scroll down and look for "Referrer Whitelist."

Enter the URL of the domain you want to send the request from. Assuming our domain name is https://example.com, we will enter it like this on the form; "example.com" without the https or http part. Click "add" to add the domain to the allowed referrers. We can add as many domains as we want.

Connecting to a contract

We are now ready to send requests from our frontend dapps. Next, we will set up a connection to a contract and attempt to read information from this contract. We will read data from the USDT ERC20 contract deployed on the Ethereum Mainnet. You can find the contract here.

Create a new folder under the src folder directory called utils and create two files inside the newly created utils folder called abi.json and connectContract.js.

Your directory structure will look like this after creating these files.

Theabi.json file will contain the ERC20 contract ABI.

Add the below code to the connectContract.js file.

File connectContract.js:

import abiJSON from "./abi.json";
import { ethers } from "ethers";

function connectContract() {
  const contractAddress = "0xf80C30B568E43aD5C7C2767958098B7dB366C56e";
  const contractABI = abiJSON;
  let contract;
  try {
    const connectionInfo = {
      url: "YOUR-RPC-URL-FROM-QUICKNODE",
      headers: { referer: "DOMAIN-WHITELISTED-ON-QUICKNODE" },
    };
    const provider = new ethers.providers.JsonRpcProvider(connectionInfo);
    contract = new ethers.Contract(contractAddress,contractABI,provider);
  } catch (error) {
    console.log("ERROR:", error);
  }
  return contract;
}

export default connectContract;

We configure the contract we want to interact with this file. At the top of the file, we imported the contract ABI and the ethers library. We created a function connectContract; inside the function body, we created a connection info object containing our custom RPC URL and the referer header with a domain value allowed on QuickNode.

Setting the referrer header is required for QuickNode to accept a request!

We create a provider by passing the connection info object to ethers.providers.JsonRpcProvider(connectionInfo).

Then, we create a contract instance by passing in the contract address, contract ABI and the provider to the function ethers.Contract, which creates a read-only contract instance.

Displaying token information

Open the src/pages/index.js file and replace the content with the code below. This file renders the Home component when we run the application.

File src/pages/index.js:

import Head from "next/head";
import styles from "@/styles/Home.module.css";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import connectContract from "../utils/connectContract";
import { useEffect, useState } from "react";

export default function Home() {
  const [totalSupply, setTotalSupply] = useState("0");
  const [name, setName] = useState("");
  const [symbol, setSymbol] = useState("");

  async function getTokenInformation() {
    const contract = connectContract();
    if (contract) {
      const tokenName = await contract.name();
      const totalSupply = await contract.totalSupply();
      const tokenSymbol = await contract.symbol();
      setName(tokenName);
      setTotalSupply(totalSupply.toString() / 10 ** 6); //divided by the         decimal
      setSymbol(tokenSymbol);
    }
  }

  useEffect(() => {
    getTokenInformation();
  }, []);

  return (
    <>
      <Head>
        <title>Using QuickNode Services</title>
        <meta name="description" content="Tips on using QuickNode Services"     />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <nav>
        <ul>
          <li>
            <ConnectButton />
          </li>
        </ul>
      </nav>
      <main className={styles.main}>
        <div className={styles.center}>
          <div className={styles.card}>
            <p>Token Name : {name}</p>
            <br />
            <p>Total Supply: {totalSupply}</p>
            <br />
            <p>Token Symbol: {symbol}</p>
          </div>
        </div>
      </main>
    </>
  );
}

At the top of the file, we imported Head from next/head to add meta-data to a webpage. We imported the ConnectButton from "@rainbow-me/rainbowkit". We are not signing any transaction; the button displays wallet options for users to interact with our DApp.

useEffect and usestate are imported from react. Our home page displays the total token supply, name, and symbol. We save this information in the React state. We defined a function getTokenInformation; inside this function, we retrieve our contract instance using the imported connectContract function. Then we check to see if we have a contract instance; if we do, we retrieve the token name, total supply, and symbol from the contract using the instance.

const tokenName = await contract.name();

The values are set in the React state using the useState setters functions. We call the getTokenInformation function inside a React useEffect, which has an empty array as a dependency to ensure it only gets executed once when the page renders and displays information about the USDT Token.

Run the application using the following command:

$ npm run dev

Retrieving token metadata is relatively easy, but we may have scenarios where we may want to query the blockchain for historical data about token transactions performed by a particular wallet.

This is more complex, as we must gather such information manually. It requires a time-consuming and technical process of scraping, decoding, and storing data from millions of blocks and billions of logs made by token events.

Caching of data

QuickNode has made retrieving information from an ERC20 token as seamless as possible. It has done the heavy lifting from the blockchain and saved this data in performant databases, which we can query via an API called the Token API. There is no wait time or indexing of data before retrieving the data you want and helping to scale our DApps and make data more readily available for our users.

Retrieving token data via QuickNode Token API

Quickly getting your information will positively affect the user experience of your DApp. This is made possible by leveraging QuickNode APIs to build and scale our products.

Open the folder src/pages/api and create a new file called get-meta-data.js. Next.js executes the files inside the api folder on the backend.

Copy and paste the code below into the created file.

File src/pages/api/get-meta-data.js:

const ethers = require("ethers");

export default async function handler(req, res) {
  if (req.method === "POST") {
    const body = req.body;
    const responseData = await retrieveTokenMetadateUsingQuickNodeAPI(body);
    return res.status(200).json({ success: true, data: responseData });
  } else {
    return res
      .status(405)
      .json({ message: "Method not allowed", success: false });
  }
}

async function retrieveTokenMetadateUsingQuickNodeAPI(contractAddress) {
  const connectionInfo = {
    url: "YOUR-RPC-URL",
  };
  let provider = new ethers.providers.JsonRpcProvider(connectionInfo);
  const response = await provider.send("qn_getTokenMetadataByContractAddress",       {
    contract: contractAddress,
  });
  console.log("response data : ", response);
  return response;
}

This file contains a request handler, which only accepts the POST method and throws an error if the request method is not POST. The request's body contains the contract address we want to retrieve its metadata.

The retrieveTokenMetadateUsingQuickNodeAPI function calls the QuickNode Token API method qn_getTokenMetadataByContractAddress to retrieve the metadata of a token contract address.

If the API call succeeds, we return a status code of 200 and a JSON object with "success" set to true and the contract's metadata.

Open the index.js page and let us consume our API. We will create a simple input box and a button. We enter the contract address for the token we want to check in the textbox and click the button.

We call the API in the get-meta-data.js file created above.

File src/pages/index.js

  //index.js
  //new state information added  
  const [contractAddress, setContractAddress] = useState("");
  const [contractMetaData, setContractMetaData] = useState(null);

  const submitAddress = async () => {
    try {
      setLoading(true);
      const response = await fetch("/api/get-meta-data", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-qn-api-version": 1,
          referrer: "YOUR-REFERRER-HEADER",
        },
        body: JSON.stringify(contractAddress),
      });
      if (response.status !== 200) {
        alert("Oops! Something went wrong. Please refresh and try again.");
        setLoading(false);
      } else {
        let responseJSON = await response.json();
        const { contract } = responseJSON.data;
        const metaData = {
          ...contract,
        };
        setLoading(false);
        setContractMetaData(metaData);
        console.log("json response ", responseJSON);
      }
    } catch (error) {
      setLoading(false);
      alert(
        `Oops! Something went wrong. Please refresh and try again. Error                ${error}`
      );
    }
  };
return (
    <main>
       <div className={styles.center}>
          <input
            type="text"
            onChange={(e) => setContractAddress(e.target.value)}
            className={styles.input}
            value={contractAddress}
          />
          <button
            onClick={submitAddress}
            className={styles.button}
            disabled={loading}
          >
            Retrieve Token Metadata
          </button>
        </div>
        {loading && <p>Getting data...............</p>}
        <div className={styles.center}>

          {contractMetaData && (
            <div className={styles.card}>
              <p>Token Name : {contractMetaData.name}</p>
              <p>Token Symbol : {contractMetaData.symbol}</p>
              <p>Token Decimal : {contractMetaData.decimals}</p>
              <p>Genesis Block : {contractMetaData.genesisBlock}</p>
              <p>
                Genesis Transactions : {contractMetaData.genesisTransaction}
              </p>
            </div>
          )}
        </div>
    </main>
)

We have added code on the index.js page that we will use to call our API. The textbox is used to enter the contract address. Clicking the "Retrieve Token Metadata" button makes a call to the function callMetaDataApi which communicates with our API via a POST method. We set the Headers to the QuickNode API. We are concerned about two of the headers, which are:

 "x-qn-api-version": 1,
 "referrer": "YOUR-REFERRER-HEADER",

The header x-qn-api-version tells QuickNode the version of the API we are targeting, and the header referrer is set to the domain we have allowed in the QuickNode dashboard.

The contract address is stringified and sent in the body of the HTTP request object. If the request status code returned from the server is not 200, we know we have encountered an error and alerted our users. Yet, if all goes fine, we save the response data using the function setContractMetaData in state and display the data subsequently to the user.

Verdict on retrieving token contract metadata

We have seen two approaches we could take to retrieve the metadata of a token contract. First, we saw the manual approach where we have to set up everything like the contract ABI, connecting it to a provider to call the token contract methods. The second method was connecting to QuickNode API to retrieve data about any token contract stored on the Ethereum blockchain.

Going further, it will be difficult to retrieve the past transactions of a wallet using this manual approach because we will have to shift through millions of blocks and logs to pull the transaction data needed, but this is not the case when we use the QuickNode token API. The code sample below retrieves information about a wallet transaction using the Token API.

const ethers = require("ethers");
export default async function handler(req, res) {
  if (req.method === "POST") {
    const {contractAddress, walletAddress } = req.body;
    const responseData = await retrieveTransactionDataQuickNodeAPI(contractAddress, walletAddress);
    return res.status(200).json({ success: true, data: responseData });
  } else {
    return res
      .status(405)
      .json({ message: "Method not allowed", success: false });
  }
}

async function retrieveTransactionDataQuickNodeAPI(contractAddress, walletAddress) {
  const connectionInfo = {
    url: "YOUR-QUICKNODE-RPC-URL",
  };
  let provider = new ethers.providers.JsonRpcProvider(connectionInfo);
  const response = await provider.send("qn_getWalletTokenTransactions", {
    address: walletAddress,
    contract: contractAddress,
    page: 1,
    perPage: 10,
  });
   console.log("response data : ", response);
   return response;
}

This code above retrieves transaction information about a wallet address in an ERC20 token contract. Calling the function gives the following sample response:

  token: {
    address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
    decimals: 6,
    genesisBlock: 4634748,
    genesisTransaction: '0x2f1c5c2b44f771e942a8506148e256f94f1a464babc938ae0690c6e34cd79190',
    name: 'Tether USD',
    symbol: 'USDT'
  },
  transfers: [
    {
      amount: '379607522',
      blockNumber: '16653789',
      date: '2023-02-18T06:36:59.000Z',
      from: '0x8C8D7C46219D9205f056f28fee5950aD564d7465',
      to: '0x998DA4F8510A24e3bfd74aD8BEfc22430A6bdE6d',
      logIndex: 397,
      txHash: '0x3f69551132413b195f16753e145f5c0220a7a473731c5fe156f7658473bd3fbe',
      valueSent: '0'
    },
    {
      amount: '45627000',
      blockNumber: '16653771',
      date: '2023-02-18T06:33:23.000Z',
      from: '0x8C8D7C46219D9205f056f28fee5950aD564d7465',
      to: '0xf53C3C0C009dDb3dbdfea4D9C31899aCea07165B',
      logIndex: 830,
      txHash: '0xccc0a05484103edca6fbd572bf4e87a87911bc0881770d84c4fff598d4e929d6',
      valueSent: '0'
    },
  ],
  pageNumber: 1,
  totalPages: 13017,
  totalItems: 130164
}

We use the pageNumber property to retrieve different data pages; this particular wallet is active in 130,164 transactions.

Having this information by the query of an API will help in scaling your product, for example, if you are building an analytics dashboard to track the performance or popularity of a token.

Scaling by compressing blockchain data

Data requests from the blockchain can be massive. QuickNode allows the compression of data sent to a requesting client. If we want to retrieve extensive data from the blockchain, we can use the QuickNode API compression functionality.

We add a key in the connection info object passed to the provider constructor. Compression of the data by the QuickNode server makes onward transmission of data to the client faster because of the reduced size of the data.

const connectionInfo = {
    allowGzip: "true",
    url: "YOUR-RPC-URL",
  };
  let provider = new ethers.providers.JsonRpcProvider(connectionInfo);
  //you can call any QuickNode API

What's Next

Scaling a DApp can be a challenge. Decentralized systems are abundant, and choosing the suitable blockchain to build on goes a long way in your ability to scale your system to meet the needs of your users. We have shown some QuickNode services that can help scale your application. Here are some other strategies employed by QuickNode to make your application scale and available to a billion users;

  • QuickNode offers services that cover the major blockchain, so from the onset, you are not restricted by the blockchain you need to build your solution on. QuickNode have you covered in that aspect, as their coverage transcends all popular blockchain solution.

  • QuickNode has servers in different geographical locations across the globe, so your application load is evenly distributed across its load balancers. There is no single point of failure that guarantees you 100% uptime.

  • QuickNode provides real-time alert services from your smart contract; you write simple expressions. When events occur in your contract, you get a notification via a custom webhook setup by you.

In conclusion, scaling a DApp project is a complex process, but with the help of QuickNode's services, you can leverage the best strategies and best practices to ensure its success. Whether you are building a new DApp or looking to scale an existing one, QuickNode can help you achieve your goals and deliver a high-performance decentralized application.


D_D Newsletter CTA

Additional Resources

The code for this simple sample can be found on GitHub