Skip to main content
Version: SDK V3

Quickstart: Smart Account Native Transfer ⚡️

In this guide, we will create a basic Node.js script using TypeScript with an implementation of the Smart Account Package from the Biconomy SDK. You will learn how to create a Smart Account and perform user operations by sending a native transfer of tokens.

info

Please note that this tutorial assumes you have Node JS installed on your computer and have some working knowledge of Node. 🧠

Environment set up 🛠️

We will clone a preconfigured Node.js project with TypeScript support to get started. Follow these steps to clone the repository to your local machine using your preferred command line interface:

  1. Open your command line interface, Terminal, Command Prompt, or PowerShell.
  2. Navigate to the desired directory where you would like to clone the repository.
  3. Execute the following command to clone the repository from the provided GitHub link
git clone https://github.com/bcnmy/quickstart.git

Once you have the repository on your local machine, install all dependencies using your preferred package manager. In this tutorial, we will use yarn.

yarn install
yarn dev

After running these two commands you should see the printed statement Hello World! in your terminal. Any changes made to the index.ts file in the src directory should now automatically run in your terminal upon saving.

All packages you need for this guide are configured and installed for you. Check out the package.json file if you want to explore the dependencies.

Click to learn more about the packages
  • The Account package will help you with creating Smart Account and an interface with them to create transactions.
  • The Bundler package allows you to interact with our bundler or any other bundler of your choice.
  • The Paymaster package can be used with our paymaster or any other of your choice, similar to the bundler package.
  • The Core Types package will give us Enums for the proper ChainId we may want to use.
  • The Modules package provides access to the different modules that are published for the biconomy SDK.
  • The Common package is needed by our accounts package as another dependency.
  • Finally, with version 5.7.2 of the ethers package, we can set our EOA as the account owner.

Let’s first set up a .env file in the root of our project, this will need a Private Key of any Externally Owned Account (EOA) you would like to serve as the owner of the Smart Account we create. This is a private key you can get from wallets like MetaMask, TrustWallet, Coinbase Wallet, etc. Each of these wallets will include step-by-step guides on how to export the Private key. 🔑

PRIVATE_KEY = "<your_private_key>"

Let’s give our script the ability to access this environment variable. Delete the console log inside of src/index.ts and replace it with the code below. All of our work for the remainder of the tutorial will be in this file.

import { config } from "dotenv";

config();

Now our code is configured to access the environment variable as needed.

Initialization 🌟

Start by importing necessary packages for the bundler:

import { IBundler, Bundler } from "@biconomy/bundler";
import { DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account";
import { ethers } from "ethers";

IBundler is the typing for the Bundler class that we will create a new instance of.

Initial Configuration

const bundler: IBundler = new Bundler({
bundlerUrl:
"https://bundler.biconomy.io/api/v2/80002/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44",
chainId: 80002,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
});
  • Now we have created an instance of our Bundler with the following:
    • A bundler URL which you can retrieve from the Biconomy Dashboard
    • Chain ID, in this case, we’re using Polygon Amoy
    • Default entry point address imported from the Account package
import {
BiconomySmartAccountV2,
DEFAULT_ENTRYPOINT_ADDRESS,
} from "@biconomy/account";

Update your import from the Account package to also include BiconomySmartAccountV2 which is the class we will be using to instantiate our smart account.

const provider = new ethers.providers.JsonRpcProvider(
"https://rpc-amoy.polygon.technology/"
);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", provider);
  • Our provider is configured with Ankr's public RPC endpoint; alternatives like Infura or Alchemy are also viable.
  • Following this, we initiate a wallet instance linked to our Private Key.

Before proceeding, we need to integrate the ECDSA module for our Smart Account. For details on modules, visit Biconomy's page. We start by importing the ECDSA module:

import {
ECDSAOwnershipValidationModule,
DEFAULT_ECDSA_OWNERSHIP_MODULE,
} from "@biconomy/modules";

Now let's initialize the module and pass it to our Account Creation Config:

async function initializeSmartAccount() {
const module = await ECDSAOwnershipValidationModule.create({
signer: wallet,
moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE,
});

let biconomyAccount = await BiconomySmartAccountV2.create({
chainId: 80002,
bundler: bundler,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
defaultValidationModule: module,
activeValidationModule: module,
});

// Log the EOA owner's address and the Smart Account address
console.log("EOA Owner Address:", wallet.address);
console.log(
"Smart Account Address:",
await biconomyAccount.getAccountAddress()
);

return biconomyAccount;
}

The initializeSmartAccount() function initiates the creation of a new Smart Account using the BiconomySmartAccount class. This setup process involves configuring the account with predefined settings. Upon successful initialization, the function logs:

  1. The owner of the Smart Account, identified as an Externally Owned Account (EOA).
  2. The unique address of the newly created Smart Account.

These steps prepare the Smart Account for use and make its details easily accessible.

info

Smart accounts are designed with a pre-determined address known prior to deployment, making them counterfactual. The Smart Account (contract) is created automatically during the first transaction, with the gas needed for deployment included.

caution

Before continuing, now that we have our Smart Account address, we need to fund it with some test network tokens! Since we are using the Polygon Amoy network head over to the Polygon Faucet and paste in your smart account address and get some test tokens! If you skip this step, you might run into the AA21 didn't pay prefund error! 💸

Once you have tokens available, it is time to start constructing our first userOps for a native transfer.

Build you first userOp (Transaction) 🧱

Let's create your first user operation (userOp) for the transaction. Here are the details it needs:

  • to: Smart Contract your Smart Account will interact with. (Feel free to change to your own or send me more test tokens 😉)
  • data: We are defaulting to 0x as no specific data is needed for native transfers.
  • value: We need to indicate the amount we want to transfer and format it correctly using the parseEther utility function.

Now we'll build the userOp. You can log the partial userOp if you want. We'll add 0x to the paymasterAndData value to make it a regular transaction where the end user pays for the gas.

async function buildUserOp(smartAccount: BiconomySmartAccountV2) {
try {
const transaction = {
to: "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD",
data: "0x",
value: ethers.utils.parseEther("0.01"),
};

const userOp = await smartAccount.buildUserOp([transaction]);
userOp.paymasterAndData = "0x";
return userOp;
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Error building user operation:", error.message);
}
}
}

Execute your first userOp 🚀

With our buildUserOp function set to create user operations, we'll now use it inside the submitUserOp function.

async function submitUserOp() {
try {
// Initialize your Smart Account
const smartAccount = await initializeSmartAccount();

// Build the user operation
const userOp = await buildUserOp(smartAccount);
if (!userOp) {
console.error("Error: Could not create the user operation.");
return;
}

// Send the user operation and wait for the transaction to complete
const userOpResponse = await smartAccount.sendUserOp(userOp);
const transactionDetails = await userOpResponse.wait();

console.log("See your transaction details here:");
console.log(
`https://www.oklink.com/amoy/tx/${transactionDetails.receipt.transactionHash}`
);
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Transaction Error:", error.message);
}
}
}
  1. We send the userOp to our bundler.
  2. We save the response in a variable called userOpResponse.
  3. We retrieve the transaction detail by calling userOpResponse.wait().

To wait for a specific number of network confirmations before getting the value, use wait() with a number argument. For instance, userOpResponse.wait(5) waits for 5 confirmations before returning the value.

Check out the long transaction details available now in your console! You just created and executed your first userOps using the Biconomy SDK!

Well done! The entire Biconomy crew is sending you a big round of applause! 👏👏🏻👏🏼👏🏽👏🏾👏🏿

View Complete Code
import { config } from "dotenv";
import { IBundler, Bundler } from "@biconomy/bundler";
import {
BiconomySmartAccountV2,
DEFAULT_ENTRYPOINT_ADDRESS,
} from "@biconomy/account";
import { ethers } from "ethers";
import {
ECDSAOwnershipValidationModule,
DEFAULT_ECDSA_OWNERSHIP_MODULE,
} from "@biconomy/modules";
config();

const bundler: IBundler = new Bundler({
bundlerUrl:
"https://bundler.biconomy.io/api/v2/80002/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44",
chainId: 80002,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
});

const provider = new ethers.providers.JsonRpcProvider(
"https://rpc-amoy.polygon.technology/"
);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", provider);

async function initializeSmartAccount() {
const module = await ECDSAOwnershipValidationModule.create({
signer: wallet,
moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE,
});

let biconomyAccount = await BiconomySmartAccountV2.create({
chainId: 80002,
bundler: bundler,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
defaultValidationModule: module,
activeValidationModule: module,
});

// Log the EOA owner's address and the Smart Account address
console.log("EOA Owner Address:", wallet.address);
console.log(
"Smart Account Address:",
await biconomyAccount.getAccountAddress()
);

return biconomyAccount;
}

async function buildUserOp(smartAccount: BiconomySmartAccountV2) {
try {
const transaction = {
to: "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD",
data: "0x",
value: ethers.utils.parseEther("0.0001"),
};

const userOp = await smartAccount.buildUserOp([transaction]);
userOp.paymasterAndData = "0x";
return userOp;
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Error building user operation:", error.message);
}
}
}

async function submitUserOp() {
try {
// Initialize your Smart Account
const smartAccount = await initializeSmartAccount();

// Build the user operation
const userOp = await buildUserOp(smartAccount);
if (!userOp) {
console.error("Error: Could not create the user operation.");
return;
}

// Send the user operation and wait for the transaction to complete
const userOpResponse = await smartAccount.sendUserOp(userOp);
const transactionDetails = await userOpResponse.wait();

console.log("See your transaction details here:");
console.log(
`https://www.oklink.com/amoy/tx/${transactionDetails.receipt.transactionHash}`
);
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Transaction Error:", error.message);
}
}
}

submitUserOp();

If you are facing any error, do checkout the troubleshooting for common errors.

🎉 Congratulations on completing the quickstart!

To dive deeper, check out more use cases in our Quick Explore guide or explore our Node.js guides for additional insights.