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 { createRhinestoneAccount, 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 account = await createRhinestoneAccount({
        owners: {
          type: "ecdsa",
          accounts: [wrappedWalletClient],
        },
        rhinestoneApiKey: apiKey,
      })

      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