Overview
Dynamic provides embedded wallet infrastructure that enables seamless user onboarding and wallet management. This guide shows you how to integrate Dynamic signers with Rhinestone smart accounts to create a unified, cross-chain wallet experience.
How it works: Dynamic handles user authentication and wallet connections, providing wagmi-compatible clients. We use wagmi hooks to access these clients and pass them to Rhinestone’s SDK, which wraps them with cross-chain capabilities.
Prerequisites
- A Dynamic account and project
- Dynamic API key
- React application setup
Installation
Install the required dependencies:
npm install @dynamic-labs/sdk-react @rhinestone/sdk viem wagmi
Setup Dynamic Provider
First, set up the Dynamic provider in your React application:
import { DynamicContextProvider } from '@dynamic-labs/sdk-react'
function App() {
return (
<DynamicContextProvider
settings={{
environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID,
walletConnectors: ['metamask', 'coinbase', 'walletconnect'],
}}
>
{/* Your app components */}
</DynamicContextProvider>
)
}
Create a Dynamic Integration Hook
Create a production-ready hook that integrates Dynamic wallets with Rhinestone accounts. This demonstrates the core pattern: get the wallet client from wagmi, then pass it to Rhinestone.
Dynamic automatically populates wagmi’s useAccount() and useWalletClient() hooks when users connect their wallets.
import { useState, useEffect } from 'react'
import { useAccount, useWalletClient } from "wagmi"
import { RhinestoneSDK, walletClientToAccount } from "@rhinestone/sdk"
interface GlobalWalletState {
rhinestoneAccount: any | null
address: string | null
isLoading: boolean
error: string | null
isConnected: boolean
}
export function useGlobalWallet(): GlobalWalletState & {
reconnect: () => Promise<void>
} {
const { address, isConnected } = useAccount()
const { data: walletClient } = useWalletClient()
const [state, setState] = useState<GlobalWalletState>({
rhinestoneAccount: null,
address: null,
isLoading: false,
error: null,
isConnected: false,
})
const initializeAccount = async () => {
if (!isConnected || !address || !walletClient) {
setState(prev => ({
...prev,
rhinestoneAccount: null,
address: null,
isConnected: false,
error: null,
}))
return
}
setState(prev => ({ ...prev, isLoading: true, error: null }))
try {
// Ensure wallet client has address property for Rhinestone compatibility
const wrappedWalletClient = walletClientToAccount(walletClient)
const apiKey = process.env.NEXT_PUBLIC_RHINESTONE_API_KEY
if (!apiKey) {
throw new Error('NEXT_PUBLIC_RHINESTONE_API_KEY is not configured')
}
// Create Rhinestone account using Dynamic's wallet client
const rhinestone = new RhinestoneSDK({
apiKey,
})
const account = await rhinestone.createAccount({
owners: {
type: "ecdsa",
accounts: [wrappedWalletClient],
},
})
setState(prev => ({
...prev,
rhinestoneAccount: account,
address,
isConnected: true,
isLoading: false,
}))
} catch (error) {
console.error('Failed to initialize Rhinestone account:', error)
setState(prev => ({
...prev,
error: error instanceof Error ? error.message : 'Failed to initialize account',
isLoading: false,
isConnected: false,
}))
}
}
useEffect(() => {
initializeAccount()
}, [isConnected, address, walletClient])
return {
...state,
reconnect: initializeAccount,
}
}
Usage
Basic Component Integration
Use the enhanced hook in your React components with proper loading and error states:
import { useGlobalWallet } from './hooks/useGlobalWallet'
import { useDynamicContext } from '@dynamic-labs/sdk-react'
function WalletDashboard() {
const { setShowAuthFlow } = useDynamicContext()
const {
rhinestoneAccount,
address,
isLoading,
error,
isConnected,
reconnect
} = useGlobalWallet()
// Handle loading state
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2">Setting up your global wallet...</span>
</div>
)
}
// Handle error state
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h3 className="text-red-800 font-medium">Wallet Setup Error</h3>
<p className="text-red-600 text-sm mt-1">{error}</p>
<button
onClick={reconnect}
className="mt-2 px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
>
Try Again
</button>
</div>
)
}
// Handle disconnected state
if (!isConnected || !rhinestoneAccount) {
return (
<div className="text-center p-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Connect Your Wallet
</h2>
<p className="text-gray-600 mb-6">
Connect with Dynamic to access cross-chain functionality
</p>
<button
onClick={() => setShowAuthFlow(true)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Connect Wallet
</button>
</div>
)
}
// Connected state
return (
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
<h2 className="text-lg font-semibold text-green-900 mb-2">
Wallet Connected
</h2>
<div className="space-y-2 text-sm">
<p><strong>EOA Address:</strong> {address}</p>
<p><strong>Smart Account:</strong> {rhinestoneAccount.getAddress()}</p>
<p className="text-green-700">Ready for cross-chain transactions!</p>
</div>
</div>
)
}
Cross-Chain Transactions
Send transactions using the Dynamic-connected wallet with proper error handling:
import { useState } from 'react'
import { encodeFunctionData, parseUnits } from 'viem'
import { erc20Abi } from 'viem/chains'
import { baseSepolia, arbitrumSepolia } from 'viem/chains'
function CrossChainTransfer({ rhinestoneAccount }) {
const [isTransacting, setIsTransacting] = useState(false)
const [txResult, setTxResult] = useState(null)
const [error, setError] = useState(null)
const handleCrossChainTransfer = async () => {
if (!rhinestoneAccount) return
setIsTransacting(true)
setError(null)
setTxResult(null)
try {
const transaction = await rhinestoneAccount.sendTransaction({
sourceChains: [baseSepolia],
targetChain: arbitrumSepolia,
calls: [
{
to: "USDC", // This resolves to the USDC address on the target chain
data: encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: ["0xrecipient", parseUnits("10", 6)],
}),
},
],
tokenRequests: [
{
address: "USDC",
amount: parseUnits("10", 6),
},
],
})
// Wait for transaction execution
const result = await rhinestoneAccount.waitForExecution(transaction)
setTxResult({
id: transaction.id,
hash: result.transactionHash,
status: 'success',
})
} catch (err) {
console.error('Transaction failed:', err)
setError(err instanceof Error ? err.message : 'Transaction failed')
} finally {
setIsTransacting(false)
}
}
return (
<div className="space-y-4">
<button
onClick={handleCrossChainTransfer}
disabled={isTransacting || !rhinestoneAccount}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isTransacting ? 'Sending...' : 'Send 10 USDC (Base → Arbitrum)'}
</button>
{error && (
<div className="text-red-600 text-sm">
Error: {error}
</div>
)}
{txResult && (
<div className="text-green-600 text-sm">
Transaction successful!
<a
href={`https://arbiscan.io/tx/${txResult.hash}`}
target="_blank"
rel="noopener noreferrer"
className="underline ml-1"
>
View on Arbiscan
</a>
</div>
)}
</div>
)
}
Environment Variables
Make sure to set the following environment variables:
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your_dynamic_environment_id
NEXT_PUBLIC_RHINESTONE_API_KEY=your_rhinestone_api_key
Complete Example
Try the full integration in our example repository:
git clone https://github.com/rhinestonewtf/e2e-examples.git
cd e2e-examples/dynamic
npm install && npm run dev
The example demonstrates:
- Dynamic wallet connection and authentication
- Rhinestone smart account creation
- Cross-chain transactions
- Error handling and best practices
Next Steps