To access the current wallet, use the useDynamicContext()
hook provided by
DynamicContextProvider
and get the primaryWallet
. You can then convert it
to a local account and use it to generate a Circle Smart Account.
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:
In the Circle Developer Console, complete the setup below by following the steps in the Modular Wallets Console Setup section:
In the Dynamic Dashboard, do the following:
.env
file
along with other credentials, to be accessed later using import.meta.env
.Here's how to override the default EVM networks in Dynamic:
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.
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:
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>
)
}
To access the current wallet, use the useDynamicContext()
hook provided by
DynamicContextProvider
and get the primaryWallet
. You can then convert it
to a local account and use it to generate a Circle Smart Account.
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:
DynamicContextProvider
.toCircleSmartAccount()
.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.