Skip to main content

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