Omni Account
1: Create a new Omni Account

Tutorial 1: Create a new Omni Account

In this tutorial, we will walk through creating a new Omni Account. What this means is that you will create a smart account, in this case a Safe with the Safe7579 module, and add the required modules to transform it into an Omni Account. This will enable the user to lock funds and instantly spend them on any other chain. To send the first intent, check out tutorial 2.

For this tutorial, we are using @rhinestone/module-sdk at the latest version to set up the required modules. We are also using @rhinestone/orchestrator-sdk to interact with the Omni Account modules and the backend service, the Orchestrator. Finally, we use permissionless@^0.2.22 and viem@^2.21.51 for their account abstraction features. See the full source code for this tutorial in our module-sdk-tutorials repo (opens in a new tab).

Install the packages

First, install the required dependencies:

npm i @rhinestone/module-sdk @rhinestone/orchestrator-sdk permissionless viem

Import the required functions and constants

import {
  getOwnableValidator,
  RHINESTONE_ATTESTER_ADDRESS,
} from "@rhinestone/module-sdk";
import { createSmartAccountClient } from "permissionless";
import {
  toSafeSmartAccount,
  ToSafeSmartAccountParameters,
} from "permissionless/accounts";
import {
  Chain,
  createPublicClient,
  createWalletClient,
  encodeAbiParameters,
  encodeFunctionData,
  erc20Abi,
  Hex,
  http,
  zeroAddress,
} from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import {
  entryPoint07Address,
} from "viem/account-abstraction";
import {
  getHookAddress,
  getOrchestrator,
  getSameChainModuleAddress,
  getTargetModuleAddress,
  getTokenAddress,
} from "@rhinestone/orchestrator-sdk";
import { erc7579Actions } from "permissionless/actions/erc7579";
import { createPimlicoClient } from "permissionless/clients/pimlico";

Create the owner for the smart account

Before creating a new smart account, we will set up the owner. In this example, we just generate a new private key and use the Ownable Validator, but you could use an existing user account, for example using wagmi, or a different authentication method such as passkeys through the Webauthn Validator.

const owner = privateKeyToAccount(generatePrivateKey());
 
const ownableValidator = getOwnableValidator({
  owners: [owner.address],
  threshold: 1,
});

Create the source chain clients

Next, we will create the source chain clients. Since we are interacting with multiple chains, we need to create clients for each of them. First, we will create some for the source chain, ie the chains on which the funds will reside.

const sourcePublicClient = createPublicClient({
  chain: sourceChain,
  transport: http(),
});
 
const sourcePimlicoClient = createPimlicoClient({
  transport: http(
    `https://api.pimlico.io/v2/${sourceChain.id}/rpc?apikey=${pimlicoApiKey}`,
  ),
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
});

Create the source chain smart account client

Now we will create the smart account client for the source chain. This client will be used to interact with the smart account on the source chain and calculates the counterfactual address.

When creating the smart account, we pass a few initial modules. The first of these is the Ownable Validator that we set up before. The remaining ones are the executors and fallback required for Omni Account. In the future, this will be moved into the sdk, but for now we do it manually.

const smartAccountConfig: ToSafeSmartAccountParameters<
  "0.7",
  "0x7579011aB74c46090561ea277Ba79D510c6C00ff"
> = {
  client: sourcePublicClient,
  owners: [owner],
  version: "1.4.1",
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
  safe4337ModuleAddress: "0x7579EE8307284F293B1927136486880611F20002",
  erc7579LaunchpadAddress: "0x7579011aB74c46090561ea277Ba79D510c6C00ff",
  attesters: [
    RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
    "0x6D0515e8E499468DCe9583626f0cA15b887f9d03", // Mock attester for omni account
  ],
  attestersThreshold: 1,
  validators: [
    {
      address: ownableValidator.address,
      context: ownableValidator.initData,
    },
  ],
  executors: [
    {
      address: getSameChainModuleAddress(targetChain.id),
      context: "0x",
    },
    {
      address: getTargetModuleAddress(targetChain.id),
      context: "0x",
    },
    {
      address: getHookAddress(targetChain.id),
      context: "0x",
    },
  ],
  hooks: [
    {
      address: getHookAddress(targetChain.id),
      context: encodeAbiParameters(
        [
          { name: "hookType", type: "uint256" },
          { name: "hookId", type: "bytes4" },
          { name: "data", type: "bytes" },
        ],
        [
          0n,
          "0x00000000",
          encodeAbiParameters([{ name: "value", type: "bool" }], [true]),
        ],
      ),
    },
  ],
  fallbacks: [
    {
      address: getTargetModuleAddress(targetChain.id),
      context: encodeAbiParameters(
        [
          { name: "selector", type: "bytes4" },
          { name: "flags", type: "bytes1" },
          { name: "data", type: "bytes" },
        ],
        ["0x3a5be8cb", "0x00", "0x"],
      ),
    },
  ],
};
 
const sourceSafeAccount = await toSafeSmartAccount(smartAccountConfig);
 
const sourceSmartAccountClient = createSmartAccountClient({
  account: sourceSafeAccount,
  chain: sourceChain,
  bundlerTransport: http(
    `https://api.pimlico.io/v2/${sourceChain.id}/rpc?apikey=${pimlicoApiKey}`,
  ),
  paymaster: sourcePimlicoClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      return (await sourcePimlicoClient.getUserOperationGasPrice()).fast;
    },
  },
}).extend(erc7579Actions());

Create the orchestrator client

Next, we will create a client to interact with the Orchestrator service that will receive the intents and broadcast them to solvers.

const orchestrator = getOrchestrator(orchestratorApiKey);

Fund the smart account on the source chain

Now we will fund the smart account on the source chain. In our case, we will use USDC. Later, we will use the funds from source chain to instantly spend on the target chain.

const fundingAccount = privateKeyToAccount(fundingPrivateKey);
const sourceWalletClient = createWalletClient({
  chain: sourceChain,
  transport: http(),
});
 
const fundingTxHash = await sourceWalletClient.sendTransaction({
  account: fundingAccount,
  to: getTokenAddress("USDC", sourceChain.id),
  data: encodeFunctionData({
    abi: erc20Abi,
    functionName: "transfer",
    args: [sourceSafeAccount.address, 10000000n],
  }),
});
 
await sourcePublicClient.waitForTransactionReceipt({
  hash: fundingTxHash,
});

Deploy the source account

Finally, we deploy the account on source chain. This is currently required to ensure that the resources are locked, but this assumption will be relaxed in the future. To deploy easily, we simply send a mock transaction.

const opHash = await sourceSmartAccountClient.sendTransaction({
  to: zeroAddress,
  data: "0x11111111",
});
 
await sourcePublicClient.waitForTransactionReceipt({
  hash: opHash,
});

Now that the resource lock hook is installed, the account can use any funds on the source chain instantly on a different chain. In the next tutorial, we will send the first intent.