Wallets

Dynamic Integration Tutorial

This tutorial shows you how to integrate Dynamic as an Externally Owned Account (EOA) signer for Circle Smart Accounts using the Modular Wallets SDK. You'll learn how to connect Dynamic's authentication system with Circle's Smart Accounts. You'll also send USDC as a user operation using the viem account abstraction utilities, enabled by the Modular Wallets SDK.

Before you begin, make sure you have:

  • A Circle Developer Console account.
  • A Dynamic Dashboard account.
  • Node.js installed for local testing. Circle recommends Node 16 or higher.
  • Testnet funds in your wallet:
    • Testnet USDC: Use the Circle Faucet to mint USDC on supported testnets (for example, USDC on Polygon Amoy).
    • Native testnet tokens: Use a Public Faucet to get native testnet tokens (for example, MATIC for Polygon Amoy). You'll need these to pay for transaction fees when gas sponsorship isn't available.
  1. In the Circle Developer Console, complete the setup below by following the steps in the Modular Wallets Console Setup section:

    • Create a Client Key for the Modular Wallets SDK.
    • Retrieve the Client URL.
  2. In the Dynamic Dashboard, do the following:

    • Obtain your Environment ID and store it in the project's .env file along with other credentials, to be accessed later using import.meta.env.
    • Set the default network to one of Circle's supported networks. In this example, we use Polygon Amoy.
    • Alternatively, configure a custom list of supported networks using Dynamic's network overrides.

    Here's how to override the default EVM networks in Dynamic:

    Web

    Tsx
    const evmNetworks = [
      {
        chainId: polygonAmoy.id,
        networkId: polygonAmoy.id,
        name: polygonAmoy.name,
        nativeCurrency: polygonAmoy.nativeCurrency,
        rpcUrls: [...polygonAmoy.rpcUrls.default.http],
        iconUrls: [],
        blockExplorerUrls: [polygonAmoy.blockExplorers.default.url],
      },
    ]
    
    function App() {
      return (
        <DynamicContextProvider
          settings={{
            environmentId,
            walletConnectors: [EthereumWalletConnectors],
            overrides: { evmNetworks },
          }}
        >
          <DynamicWidget variant="modal" />
          <Example />
        </DynamicContextProvider>
      )
    }
    

Follow the steps below to integrate Dynamic as an EOA signer for Circle Smart Accounts. You'll start by installing the necessary dependencies, then configure your application to wrap Dynamic's context, create a Circle Smart Account, and send a user operation.

Install the required Dynamic SDK packages, depending on your package manager:

npm install @dynamic-labs/ethereum @dynamic-labs/sdk-react-core

Wrap your app with DynamicContextProvider from the Dynamic SDK to enable wallet authentication and connection.

Web

Tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'

import { EthereumWalletConnectors } from '@dynamic-labs/ethereum'
import {
  DynamicContextProvider,
  DynamicWidget,
} from '@dynamic-labs/sdk-react-core'

import { Example } from '.'

const environmentId = import.meta.env.VITE_DYNAMIC_ENV_ID as string

function App() {
  return (
    <DynamicContextProvider
      settings={{
        environmentId,
        walletConnectors: [EthereumWalletConnectors],
      }}
    >
      <DynamicWidget variant="modal" />
      <Example />
    </DynamicContextProvider>
  )
}

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <App />,
)

Use the Dynamic context and Circle's Modular Wallets SDK to create a Smart Account. Here's the full working example:

Web

Tsx
import React, { useEffect } from 'react'
import { createPublicClient, Hex, parseUnits } from 'viem'

import { isEthereumWallet } from '@dynamic-labs/ethereum'
import { useDynamicContext } from '@dynamic-labs/sdk-react-core'

import {
  toCircleSmartAccount,
  toModularTransport,
  walletClientToLocalAccount,
  encodeTransfer,
} from '@circle-fin/modular-wallets-core'

import { createBundlerClient, SmartAccount } from 'viem/account-abstraction'

import { polygonAmoy } from 'viem/chains'

const clientKey = import.meta.env.VITE_CLIENT_KEY as string
const clientUrl = import.meta.env.VITE_CLIENT_URL as string

// Create Circle transports
const modularTransport = toModularTransport(
  `${clientUrl}/polygonAmoy`,
  clientKey,
)

// Create a public client
const client = createPublicClient({
  chain: polygonAmoy,
  transport: modularTransport,
})

// Create a bundler client
const bundlerClient = createBundlerClient({
  chain: polygonAmoy,
  transport: modularTransport,
})

export const Example = () => {
  const { primaryWallet } = useDynamicContext() // Get the wallet information from the Dynamic context provider
  const [account, setAccount] = React.useState<SmartAccount>()
  const [hash, setHash] = React.useState<Hex>()
  const [userOpHash, setUserOpHash] = React.useState<Hex>()

  useEffect(() => {
    async function setSigner() {
      if (!primaryWallet) {
        setAccount(undefined) // Reset the account if the wallet is not connected
        return
      }

      if (!isEthereumWallet(primaryWallet)) {
        throw new Error('Wallet is not EVM-compatible.')
      }

      const walletClient = await primaryWallet.getWalletClient() // Dynamic provider

      const smartAccount = await toCircleSmartAccount({
        client,
        owner: walletClientToLocalAccount(walletClient), // Transform the wallet client to a local account
      })

      setAccount(smartAccount)
    }

    setSigner()
  }, [primaryWallet])

  const sendUserOperation = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    if (!account) return

    const formData = new FormData(event.currentTarget)
    const to = formData.get('to') as `0x${string}`
    const value = formData.get('value') as string

    const USDC_CONTRACT_ADDRESS = '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582' // Polygon Amoy testnet
    const USDC_DECIMALS = 6 // Used for parseUnits
    const callData = encodeTransfer(
      to,
      USDC_CONTRACT_ADDRESS,
      parseUnits(value, USDC_DECIMALS),
    )

    const opHash = await bundlerClient.sendUserOperation({
      account,
      calls: [callData],
      paymaster: true, // Enable gas sponsorship if supported
    })

    setUserOpHash(opHash)

    const { receipt } = await bundlerClient.waitForUserOperationReceipt({
      hash: opHash,
    })
    setHash(receipt.transactionHash)
  }

  if (!primaryWallet) return null

  return (
    <div>
      {!account ? (
        <p>Loading...</p>
      ) : (
        <>
          <p>
            <strong>Address:</strong> {account.address}
          </p>
          <h2>Send User Operation</h2>
          <form onSubmit={sendUserOperation}>
            <input name="to" placeholder="Address" required />
            <input name="value" placeholder="Amount (USDC)" required />
            <button type="submit">Send</button>
          </form>
          {userOpHash && (
            <p>
              <strong>User Operation Hash:</strong> {userOpHash}
            </p>
          )}
          {hash && (
            <p>
              <strong>Transaction Hash:</strong> {hash}
            </p>
          )}
        </>
      )}
    </div>
  )
}

Web

Tsx
const { primaryWallet } = useDynamicContext()

if (primaryWallet && isEthereumWallet(primaryWallet)) {
  const walletClient = await primaryWallet.getWalletClient()
  const smartAccount = await toCircleSmartAccount({
    client,
    owner: walletClientToLocalAccount(walletClient),
  })
}

In the above code, if the primaryWallet is not null or undefined, you can transform it into a wallet client, convert it to a local account, and then pass it to toCircleSmartAccount() to create a Circle Smart Account. This account can then be used to send user operations.

Start your app, click Login or Sign Up using Dynamic, and you'll be connected to the blockchain through Circle's Modular Wallets SDK.

Once logged in, you'll see the UI for sending a user operation:

In this tutorial, you integrated Dynamic as an EOA signer for Circle Smart Accounts using the Modular Wallets SDK. You:

  • Set up credentials in both Circle and Dynamic.
  • Installed required dependencies.
  • Wrapped your app with DynamicContextProvider.
  • Created a smart account using toCircleSmartAccount().
  • Sent a user operation using viem bundler client.

This integration enables a seamless, passwordless Web3 onboarding experience and allows you to build advanced features like gas sponsorship and session keys using Circle's modular wallets framework.

Did this page help you?
© 2023-2025 Circle Technology Services, LLC. All rights reserved.