Creating a Gasless Transaction
Now with our SDK integration set up let's render our UI and execute some transactions! First let's create a new import in our App.tsx file:
import Counter from "./Components/Counter";
In the return for this component lets add the following JSX:
<div>
<h1> Biconomy Smart Accounts using social login + Gasless Transactions</h1>
{!smartAccount && <button onClick={login}>Login</button>}
{loading && <p>Loading account details...</p>}
{!!smartAccount && (
<div className="buttonWrapper">
<h3>Smart account address:</h3>
<p>address}</p>
<Counter smartAccount={smartAccount} provider={provider} />
</div>
)}
<p>
Edit <code>src/App.tsx</code> and save to test
</p>
<a
href="https://docs.biconomy.io/docs/overview"
target="_blank"
className="read-the-docs"
>
Click here to check out the docs
</a>
</div>
Now lets create our Counter component!
If you do not already have a Components folder go ahead and create one within source and create a new file called Counter.tsx
We will also add react-toastify
for a nice toast to update our users about their transactions.
yarn add react-toastify
Let's add our imports for this file:
import React, { useState, useEffect } from "react";
import { BiconomySmartAccount } from "@biconomy/account";
import {
IHybridPaymaster,
SponsorUserOperationDto,
PaymasterMode,
} from "@biconomy/paymaster";
import abi from "../utils/counterAbi.json";
import { ethers } from "ethers";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
Make sure to get the abi from the smart contract we deployed in the first step. You can create utils folder and add the ABI json file there.
Here is an interface we will use for the props of this component:
interface Props {
smartAccount: BiconomySmartAccount
provider: any
}
Now we can start building out this Counter Component:
TotalCountDisplay will be our functional component where the value of count will be passed to the component when it's used.
const TotalCountDisplay: React.FC<{ count: number }> = ({ count }) => {
return <div>Total count is {count}</div>;
};
Counter is react function component. This component takes two props smartAccount and provider, which are expected to be passed when the component is used. The component uses React's useState hook to manage three states: count, counterContract, and isLoading. Additionally, the counterAddress variable is set to the value of VITE_COUNTER_CONTRACT_ADDRESS
.
const Counter: React.FC<Props> = ({ smartAccount, provider }) => {
const [count, setCount] = useState<number>(0);
const [counterContract, setCounterContract] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const counterAddress = import.meta.env.VITE_COUNTER_CONTRACT_ADDRESS;
With this done let's go into the two functions this component will have:
useEffect(() => {
setIsLoading(true);
getCount(false);
}, []);
const getCount = async (isUpdating: boolean) => {
const contract = new ethers.Contract(counterAddress, abi, provider);
setCounterContract(contract);
const currentCount = await contract.count();
setCount(currentCount.toNumber());
if (isUpdating) {
toast.success("Count has been updated!", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
});
}
};
We use UseEffect hook so that it sets the isLoading state to true initially and then calls the getCount(false).
The getCount
function is an asynchronous function responsible for fetching the current value of the counter from a smart contract on a blockchain and setting the state of the contract and count in the component.
- Create Contract Instance: It first creates an instance of the smart contract using the
ethers.Contract
constructor. It takes three arguments - the contract address, the contract ABI (Application Binary Interface), and the Web3 provider. This contract instance allows you to interact with the contract on the blockchain. - Set Contract State: It then calls
setCounterContract(contract)
to set the state of counterContract to the instance of the contract it just created. - Fetch Current Count: It fetches the current value of the count from the smart contract by calling the
count()
function on the contract instance. Since this function interacts with the blockchain, it returns a Promise, hence theawait
keyword. - Set Count State: It then calls
setCount(currentCount.toNumber())
to set the state of count to the current value it just fetched. It converts the value to a number since the value returned from the contract is a BigNumber. - Display Toast Notification: If the
isUpdating
parameter istrue
, it displays a toast notification indicating that the count has been updated. This notification includes configuration for its position, auto-close time, behavior on click and hover, and theme.
In summary, the getCount
function creates a contract instance, sets the state of the contract, fetches the current value of the count from the contract, sets the state of the count, and optionally displays a toast notification.
const incrementCount = async () => {
try {
toast.info('Processing count on the blockchain!', {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
});
The incrementCount
function is an asynchronous function that increments the count on the smart contract by sending a transaction to the blockchain.
- Display Initial Toast Notification: The function begins by displaying a toast notification to inform the user that the count is being processed on the blockchain.
- Create Transaction: It then creates a transaction object using the counterContract.
populateTransaction.incrementCount()
function. This function prepares the data needed to call theincrementCount
function on the smart contract but does not send the transaction. The returned transaction object includes the data needed to call the function. - Define Transaction Parameters: It defines the parameters for the transaction in
tx1
. This includes the address of the contract in to and the data needed to call the function indata
. - Send Transaction: It sends the transaction to the blockchain using the
smartAccount.sendTransaction()
function. This function takes an object with the transaction parameters and sends the transaction. Since this function interacts with the blockchain, it returns a Promise, hence the await keyword. - Wait for Transaction Confirmation: It then calls
txResponse.wait()
to wait for the transaction to be confirmed on the blockchain. This function also returns a Promise, hence theawait
keyword. - Log Transaction Hash: Once the transaction is confirmed, the transaction hash is logged to the console with
console.log(txHash)
. - Update Count: It then calls
getCount(true)
to fetch the updated count from the smart contract and update the state in the component. The true argument means that a toast notification will be displayed to indicate that the count has been updated. - Error Handling: If an error occurs during this process, it is caught by the
catch
block. The error is logged to the console withconsole.log({error})
and a toast notification is displayed to inform the user that an error occurred.
In summary, the incrementCount
function sends a transaction to a smart contract on a blockchain to increment a count, waits for the transaction to be confirmed, fetches the updated count, and handles any errors that occur during this process.
Now, let's work on our transaction data :
const incrementTx = new ethers.utils.Interface(["function incrementCount()"]);
const data = incrementTx.encodeFunctionData("incrementCount");
const tx1 = {
to: counterAddress,
data: data,
};
const userOp = await smartAccount.buildUserOp([transaction], {
paymasterServiceData: {
mode: PaymasterMode.SPONSORED,
},
});
Now that the userOp is built and will be sponsored, lets send the final userOp.
Now, let's build try and catch block :
try {
const userOpResponse = await smartAccount.sendUserOp(partialUserOp);
const transactionDetails = await userOpResponse.wait();
console.log("Transaction Details:", transactionDetails);
console.log("Transaction Hash:", transactionDetails.receipt.transactionHash);
toast.success(`Transaction Hash: ${transactionDetails.receipt.transactionHash}`, {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
});
getCount(true);
} catch (e) {
console.error("Error executing transaction:", e);
// ... handle the error if needed ...
}
} catch (error) {
console.error("Error executing transaction:", error);
toast.error('Error occurred, check the console', {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "dark",
});
}
};
Now, let's break down what's happening above :
-
const userOpResponse
= await smartAccount.sendUserOp(partialUserOp); : The partialUserOp containing the transaction details and paymaster information is sent as a user operation (sendUserOp) to the smartAccount. The smartAccount handles the meta-transaction and submits it to the blockchain. -
const transactionDetails
= await userOpResponse.wait(); : The wait() function is called on the userOpResponse, which awaits the completion of the user operation transaction. It returns the transaction details once the transaction is confirmed on the blockchain.
Logging and displaying transaction information:
a. console.log("Transaction Details:", transactionDetails);
: The transaction details are logged to the console.
b. console.log("Transaction Hash:", userOpResponse.userOpHash);
: The transaction hash (identifier) is logged to the console.
c. toast.success(...);
: A toast notification is displayed to the user to indicate that the transaction was successful. The transaction hash is displayed in the notification.
getCount(true);
: Calls the getCount function with true as an argument. It's likely used to fetch and update the latest count value from the smart contract after the successful transaction.
Error handling:
a. If any error occurs in the try block of code, the catch block will catch the error and log it to the console. Additionally, a toast notification is displayed to the user indicating that an error occurred. The details of the error are logged to the console for further investigation.
b. If any error occurs in the inner try block (getPaymasterAndData, sendUserOp, etc.), it will be caught in the corresponding catch block, and an error toast notification is displayed to the user. The error message will be logged to the console for debugging purposes.
Finally we round all this up by displaying the UI for our Toast and button:
return (
<>
<TotalCountDisplay count={count} />
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={true}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
<br></br>
<button onClick={() => incrementCount()}>Increment Count</button>
</>
);
};
export default Counter;
Congratulations you just created your first AA powered dApp. Users can now log in, have a smart account created for them, and interact with a smart contract without needing to pay gas fees.