Let’s deploy a Safe account on top of any wallet through Dynamic! First we’ll make sure signup/login are enabled, then we’ll fetch the walletClient once logged in before deploying the Safe account.

Enable signup/login

Once you’ve signed up for Dynamic, integrating it is as easy as either npx create-dynamic-app, or simply following the Quickstart guide.

When that’s done you should have an app which is wrapped with the DynamicContextProvider.

import { DynamicContextProvider } from '@dynamic-labs/sdk-react-core';
import Main from './Main';

const App = () => {
  return (
    <DynamicContextProvider
        settings={{
            environmentId: 'YOUR_ENV_ID'
            ...
        }}
    >
      <Main />
    </DynamicContextProvider>
  );
}

Before we work on Main, let’s declare a function that can help us deploy the Safe account, taking a walletClient and wallet address as arguments:

We’re using a few dependancies below, you might want to first install them:

npm i viem permissionless @dynamic-labs/wallet-connector-core
// src/utils/safe.ts
import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { WalletClient, PublicClient, http } from "viem";
import { ENTRYPOINT_ADDRESS_V07, createSmartAccountClient, walletClientToSmartAccountSigner } from "permissionless";
import { createPimlicoBundlerClient, createPimlicoPaymasterClient } from "permissionless/clients/pimlico";
import { signerToSafeSmartAccount } from "permissionless/accounts";
import { Wallet } from '@dynamic-labs/wallet-connector-core';

const transportUrl = (chain: Chain) =>
  `https://api.pimlico.io/v2/${chain.id}/rpc?apikey=${process.env.NEXT_PUBLIC_PIMLICO_API_KEY}`;

export const pimlicoBundlerClient = (chain: Chain) =>
  createPimlicoBundlerClient({
    transport: http(transportUrl(chain)),
    entryPoint: ENTRYPOINT_ADDRESS_V07,
});

export const paymasterClient = (chain: Chain) =>
  createPimlicoPaymasterClient({
    transport: http(transportUrl(chain)),
    entryPoint: ENTRYPOINT_ADDRESS_V07,
});

export const getPimlicoSmartAccountClient = async (
  wallet: Wallet
) => {
  if (!isEthereumWallet(wallet)) return;

  const address = wallet.address as `0x${string}`;
  const publicClient = await wallet.getPublicClient() as PublicClient;
  const walletClient = wallet.getWalletClient() as WalletClient;
  const chain = walletClient.chain;

  const signer = walletClientToSmartAccountSigner(walletClient);

  const safeSmartAccountClient = await signerToSafeSmartAccount(publicClient(chain), {
    entryPoint: ENTRYPOINT_ADDRESS_V07,
    signer: signer,
    safeVersion: "1.4.1",
  });

  return createSmartAccountClient({
    account: safeSmartAccountClient,
    entryPoint: ENTRYPOINT_ADDRESS_V07,
    chain,
    bundlerTransport: http(transportUrl(chain)),
    middleware: {
      gasPrice: async () => (await pimlicoBundlerClient(chain).getUserOperationGasPrice()).fast, // use pimlico bundler to get gas prices
      sponsorUserOperation: paymasterClient(chain).sponsorUserOperation, // optional
    },
  });
};

Let’s walk through what we just wrote step by step:

1

Transport URL

This is the RPC URL for the Pimlico API which we will use in multiple places, so better to extract it. It depends on your Pimlico API key so make sure you have that set under NEXT_PUBLIC_PIMLICO_API_KEY in your .env file.

2

pimlicoBundlerClient

The Bundler is the node that can help us batch our user operations and submit them as a single transaction to the blockchain. The client we are creating here is an interface to that Bundler, and it comes with a load of helpful methods related to those userops, such as Gas estimations. Specifically, we are going to need getUserOperationGasPrice later.

You can see that as well as the transport (i.e. how it should communicate which in this case is via HTTP using the Transport URL we defined earlier), it needs something called the entrypoint address. This is imported as a static variable via ENTRYPOINT_ADDRESS_V07 from the permissionless package. This is an address of a smart contract (deployed on every chain) that acts as a gateway for user operations. This article helps describe what the V07 version is compared to previous.

You can learn more about the Bundler Client here.

3

pimlicoPaymasterClient

This Paymaster is where we handle the sponsoring of user operations. It’s optional, but we are going to use it in our middleware later.

This client also needs the transport method and entrypoint address as we described in the previous step.

You can learn more about the Paymaster Client here.

4

Viem Clients & Chain

We are going to need to make some read actions and write actions onchain, and Viem provides a Public Client (for read actions) and Wallet Client (for write actions). We are going to need both of these, so we are going to extract them from the wallet object we get passed in as an argument.

Dynamic makes it super easy to return these clients from the wallet object. We can use the getPublicClient and getWalletClient methods on the wallet connector respectively.

You can learn more about interacting with Wallets (covers all kinds of actions) here.

5

walletClientToSmartAccountSigner

Something needs to sign the user operations we are going to send to the Bundler. This function takes the Wallet Client and returns a signer that can be used to sign those operations.

In normal transactions, the walletClient is enough to sign with, and you can use methods like signTransaction or signMessage directly on the walletClient. But for Smart Accounts, we need a specific type of signer that can handle the extra complexity of Smart Accounts.

You can learn more about this specific Signer type here.

6

signerToSafeSmartAccount

While we now have something that can sign for user operations, we don’t have a Smart Account yet to actually use it with. That’s the purpose of signerToSafeSmartAccount. It takes the Public Client, the signer we just created, and some other options, and returns a Safe Account.

You can learn more about this method here.

7

createSmartAccountClient

With our signer and Safe account now in hand, the last step is to create an interface where we can start to take actions on the Safe Account. This is what createSmartAccountClient does.

You can see we are defining some extra middleware here, such as the gas price and the sponsorUserOperation. These are optional but can be helpful in certain situations.

In our case we’re going to use the paymaster we created earlier to sponsor the user operations, and the bundler to get the gas prices.

You can learn more about the signerToSafeSmartAccount method here.

In Main.js, we can now fetch the walletClient once logged in and then create that Safe account using the function we just created. To learn how to handle loading states, visit the Loading States guide.

import { useState } from 'react';
import { useDynamicContext, primaryWallet } from '@dynamic-labs/sdk-react-core';
import { getOrMapViemChain } from "@dynamic-labs/ethereum-core";
import { getPimlicoSmartAccountClient } from '../utils/safe';

const Main = () => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [safeAddress, setSafeAddress] = useState(null);
  const [safeDeployed, setSafeDeployed] = useState(false);

  const { user } = useDynamicContext();

  const handleDeploySafe = async () => {
    setLoading(true);
    setError(null);

    try {
      if(!primaryWallet) {
        setError("No wallet found");
        return;
      };

      if (!process.env.NEXT_PUBLIC_PIMLICO_API_KEY) {
        console.error("Please set NEXT_PUBLIC_PIMLICO_API_KEY in .env file and restart");
        return;
      }

      const { account } = await getPimlicoSmartAccountClient(primaryWallet);
      setSafeAddress(account.address);
      setSafeDeployed(true);
    } catch (err) {
      setError("Failed to deploy Safe account.");
      console.error(err);
    } finally {
      setLoading(false);
    }
  };
  
  if (!user) {
    return <div>Loading...</div>;
  } else if(user && !safeDeployed) {
    return (
      <div>
        <h1>Welcome</h1>
        <button onClick={handleDeploySafe}>Deploy Safe</button>
      </div>
    );
  } else if (user && safeDeployed) {
    return (
      <div>
        <h1>Safe Account Deployed</h1>
        <p>Safe Address: {safeAddress}</p>
      </div>
    );
  }

}

export default Main;

That’s it! You now have the ability to create a Safe account on top of any wallet used with Dynamic, including embedded wallets! To learn how to interact with the Safe account, check out our Hackathon starter kit which handles ERC20 token transfers and cross chain ERC20 token transfers.