Quick Start

Instantiate smart account & sending an user operation

This guide walks you through the bare essentials of initializing a Startale smart account and sending a basic user operation. It’s the fastest way to get started with account abstraction using the @startale-scs/aa-sdk.

Setup

Create a new work directory and initialise a NodeJs project:

mkdir aa_test
cd aa_test
npm init -y

Install the dependencies:

npm i viem @startale-scs/aa-sdk typescript ts-node dotenv

Dependency explanation:

  • viem and @startale-scs/aa-sdk are used fo setting up and interacting with the smart contract account
  • typescript and ts-node compile and run the script
  • dotenv parses the .env file and makes the variables available

Create and fill the .env file

touch .env
echo "MAINNET_BUNDLER_URL=<your bundler URL> \
OWNER_PRIVATE_KEY=<your TEST private key>" > .env

Open the file and replace the text in <> angle brackets with your data.

  • You can obtain the Bundler url and api key by registering on the SCS portal.
  • Use a NON-PRODUCTION private key, or generate one only for this purpose.

You can use this command to obtain the key (in the project directory):

node -e "import('viem/accounts').then(({generatePrivateKey}) => console.log(generatePrivateKey()))"

Add Typescript configuration

Create a tsconfig.json and add the configuration.

Here's an example, but you can modify it to your needs:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": {
      "@startale-scs/aa-sdk": ["./node_modules/@startale-scs/aa-sdk/dist/_types"]
    },
    "typeRoots": [
      "./node_modules/@types",
      "./node_modules/@startale-scs/aa-sdk/dist/_types",
      "./src/types"
    ]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Create the script

Create a new file:

touch main.ts

Add imports

import "dotenv/config";
import { createPublicClient, http, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { soneiumMinato } from "viem/chains";
import { createSmartAccountClient, toStartaleSmartAccount } from "@startale-scs/aa-sdk";

Define variables

const bundlerUrl = process.env.MAINNET_BUNDLER_URL;
const privateKey = process.env.OWNER_PRIVATE_KEY;

if (!bundlerUrl || !privateKey) {
  throw new Error("Missing environment variables");
}

const chain = soneiumMinato;
const publicClient = createPublicClient({ chain, transport: http() });
const signer = privateKeyToAccount(privateKey as Hex);

Instantiate the smart account and client


const startaleSmartAccount = await toStartaleSmartAccount({
  signer,
  chain,
  transport: http(),
  index: BigInt(0)
});

console.log("Smart account address: ", startaleSmartAccount.address);

const smartAccountClient = createSmartAccountClient({
  account: startaleSmartAccount,
  transport: http(bundlerUrl),
  client: publicClient,
});

📘

Note

Same signer and same index value will always yield the same Smart Account address.


Construct and send the user operation

First we construct the call. In this case we're only sending 0 ETH back to the signer address.

This can be any contract call, but in that case the data needs to be constructed using viem's encodeFunctionData().

const COUNTER_CONTRACT_ADDRESS = "0x2cf491602ad22944D9047282aBC00D3e52F56B37";
const counterCall = {
  to: signer.address,
  value: 0n,
data: "0x",
}

Next, send that call as a user operation:

const hash = await smartAccountClient.sendUserOperation({
  calls: [
    {
      to: signer.address,
      value: 0n,
      data: "0x",
    },
  ],
});

console.log("UserOp hash:", hash);

Running your script

The above code should be wrapped in an async function and called.


async function main() {
  ...
}

main();

You can now run the script:

npx ts-node main.ts

📘

Note

First run will fail with the following message:

"sender balance and deposit together is 0 but must be at least XXXXX to pay for this operation"

That is expected, as your smart account doesn't have any funds.

You can now either send some ETH to the address logged (remember, same signer and same index will always instantiate the same account), or add a paymaster to sponsor the account.

To add a paymaster, follow the paymaster tutorial.


Full code example

In the full example we've added some UI libraries to make the output nicer.

To run it, you'll need to install them:

npm i ora chalk

Here's the main.ts example:


import "dotenv/config";
import ora from "ora";
import chalk from "chalk";
import { createPublicClient, encodeFunctionData, http, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { soneiumMinato } from "viem/chains";
import { createSmartAccountClient, toStartaleSmartAccount } from "@startale-scs/aa-sdk";

const bundlerUrl = process.env.MAINNET_BUNDLER_URL;
const privateKey = process.env.OWNER_PRIVATE_KEY;

if (!bundlerUrl || !privateKey) {
  throw new Error("Missing environment variables");
}

const chain = soneiumMinato;
const publicClient = createPublicClient({ chain, transport: http() });
const signer = privateKeyToAccount(privateKey as Hex);

const main = async () => {
  console.log(chalk.blue("Startale Smart Account Example"));
  const spinner = ora("Setting up...").start();
  try {
    const smartAccountClient = createSmartAccountClient({
      account: await toStartaleSmartAccount({ signer, chain, transport: http(), index: BigInt(0) }),
      transport: http(bundlerUrl),
      client: publicClient,
    });

    const address = smartAccountClient.account.address;
    spinner.succeed(`Smart Account initialized: ${address}`);

    console.log("Make sure this account is funded before sending transactions!");

    spinner.start("Sending basic User Operation...");
    const hash = await smartAccountClient.sendUserOperation({
      calls: [
        {
          to: signer.address,
          value: 0n,
          data: "0x",
        },
      ],
    });

    console.log("UserOp hash:", hash);
  } catch (err) {
    spinner.fail(chalk.red(`Error: ${(err as Error).message}`));
  }
  process.exit(0);
};

main().catch((err) => {
  console.error(chalk.red(`Unexpected error: ${(err as Error).message}`));
  process.exit(1);
});


What’s Next