Skip to main content

Overview

Magic provides passwordless authentication and embedded wallet infrastructure that enables seamless user onboarding. This guide shows you how to integrate Magic signers with Rhinestone smart accounts to create a unified, cross-chain wallet experience. How it works: Magic handles user authentication and provides wallet clients. We use Magic’s authenticated user data to create Rhinestone smart accounts, which enables cross-chain functionality with a single wallet experience.

Prerequisites

  • A Magic account and API key
  • Rhinestone API key
  • React/Next.js application setup

Installation

Install the required dependencies:
npm install magic-sdk @rhinestone/sdk viem

Setup Magic Provider

First, set up the Magic provider in your React application:
import { Magic } from 'magic-sdk'
import { OAuthExtension } from '@magic-ext/oauth'

const magic = new Magic(process.env.NEXT_PUBLIC_MAGIC_API_KEY, {
  network: {
    rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/your-api-key',
    chainId: 11155111, // Sepolia
  },
  extensions: [new OAuthExtension()],
})

function App() {
  return (
    <MagicProvider value={{ magic }}>
      {/* Your app components */}
    </MagicProvider>
  )
}

Create a Magic Integration Hook

Create a custom hook that integrates Magic authentication with Rhinestone accounts. This demonstrates the core pattern: get the wallet client from Magic, then pass it to Rhinestone.
Magic provides a direct RPC provider that we convert to a viem wallet client compatible with Rhinestone.
import { useState, useEffect, useCallback } from "react"
import { useMagic } from "./MagicProvider"
import { RhinestoneSDK, walletClientToAccount } from "@rhinestone/sdk"
import { createWalletClient, custom } from "viem"

export function useMagicWallet() {
  const { magic } = useMagic()
  const [rhinestoneAccount, setRhinestoneAccount] = useState(null)
  const [magicAddress, setMagicAddress] = useState(null)
  const [isConnected, setIsConnected] = useState(false)
  const [isLoading, setIsLoading] = useState(false)

  const initializeRhinestoneAccount = useCallback(async () => {
    if (!magic) return

    setIsLoading(true)

    try {
      // Check if user is logged in with Magic
      const isLoggedIn = await magic.user.isLoggedIn()
      if (!isLoggedIn) {
        setIsConnected(false)
        return
      }

      // Get user's Magic wallet address
      const userMetadata = await magic.user.getMetadata()
      const address = userMetadata.publicAddress

      if (!address) {
        throw new Error("No Magic wallet address found")
      }

      // Create a viem wallet client using Magic's provider
      const walletClient = createWalletClient({
        account: address as `0x${string}`,
        transport: custom(magic.rpcProvider as any),
      })

      // Ensure the wallet client has the address property for Rhinestone
      const wrappedWalletClient = walletClientToAccount(walletClient)
      }

      // Pass the Magic wallet client to Rhinestone
      // Rhinestone wraps it with cross-chain transaction capabilities
      const rhinestone = new RhinestoneSDK({
        apiKey: process.env.NEXT_PUBLIC_RHINESTONE_API_KEY,
      })
      const account = await rhinestone.createAccount({
        owners: {
          type: "ecdsa",
          accounts: [wrappedWalletClient],
        },
      })

      setRhinestoneAccount(account)
      setMagicAddress(address)
      setIsConnected(true)
    } catch (error) {
      console.error("Failed to initialize Rhinestone account:", error)
      setIsConnected(false)
    } finally {
      setIsLoading(false)
    }
  }, [magic])

  useEffect(() => {
    initializeRhinestoneAccount()
  }, [initializeRhinestoneAccount])

  return {
    rhinestoneAccount,
    magicAddress,
    isConnected,
    isLoading,
    reconnect: initializeRhinestoneAccount,
  }
}

Usage

Basic Authentication Flow

Use the hook to handle Magic authentication and smart account creation:
import { useMagicWallet } from './hooks/useMagicWallet'
import { useMagic } from './hooks/MagicProvider'

function MagicWalletDashboard() {
  const { magic } = useMagic()
  const { rhinestoneAccount, magicAddress, isConnected, isLoading } = useMagicWallet()

  const handleLogin = async () => {
    try {
      await magic.auth.loginWithEmailOTP({ email: 'user@example.com' })
      // Account will be automatically initialized via the hook
    } catch (error) {
      console.error('Login failed:', error)
    }
  }

  if (isLoading) {
    return <div>Setting up your global wallet...</div>
  }

  if (!isConnected) {
    return (
      <div>
        <h2>Welcome to Rhinestone</h2>
        <button onClick={handleLogin}>Sign in with Magic</button>
      </div>
    )
  }

  return (
    <div>
      <h2>Connected!</h2>
      <p>Magic Address: {magicAddress}</p>
      <p>Smart Account: {rhinestoneAccount?.getAddress()}</p>
    </div>
  )
}

Cross-Chain Transactions

Send transactions using the Magic-connected wallet:
async function handleCrossChainTransfer() {
  const transaction = await rhinestoneAccount.sendTransaction({
    sourceChains: [{ chainId: 42161 }], // Arbitrum
    targetChain: { chainId: 8453 }, // Base
    calls: [
      {
        to: "USDC",
        data: encodeFunctionData({
          abi: erc20Abi,
          functionName: "transfer",
          args: ["0xrecipient", parseUnits("10", 6)],
        }),
      },
    ],
    tokenRequests: [
      {
        address: "USDC",
        amount: parseUnits("10", 6),
      },
    ],
  })
}

Authentication Methods

Magic supports multiple authentication methods:
  • Email OTP: magic.auth.loginWithEmailOTP({ email })
  • SMS: magic.auth.loginWithSMS({ phoneNumber })
  • Social: magic.oauth.loginWithRedirect({ provider: 'google' })
  • WebAuthn: magic.webauthn.loginWithWebAuthn({})
Each method provides the same wallet client that can be used with Rhinestone.

Environment Variables

Make sure to set the following environment variables:
NEXT_PUBLIC_MAGIC_API_KEY=pk_live_your_magic_publishable_key
NEXT_PUBLIC_RHINESTONE_API_KEY=your_rhinestone_api_key

Complete Example

Here’s a minimal working example that demonstrates the integration:
import { useState, useEffect } from 'react'
import { Magic } from 'magic-sdk'
import { RhinestoneSDK, walletClientToAccount } from '@rhinestone/sdk'
import { createWalletClient, custom } from 'viem'

const magic = new Magic(process.env.NEXT_PUBLIC_MAGIC_API_KEY)

export function MagicDemo() {
  const [account, setAccount] = useState(null)
  const [email, setEmail] = useState('')

  const login = async () => {
    try {
      // 1. Login with Magic
      await magic.auth.loginWithEmailOTP({ email })
      
      // 2. Get user address
      const { publicAddress } = await magic.user.getMetadata()
      
      // 3. Create wallet client
      const walletClient = createWalletClient({
        account: publicAddress as `0x${string}`,
        transport: custom(magic.rpcProvider),
      })

      const wrappedWalletClient = walletClientToAccount(walletClient)

      // 4. Create Rhinestone account
      const rhinestone = new RhinestoneSDK({
        apiKey: process.env.NEXT_PUBLIC_RHINESTONE_API_KEY,
      })
      const rhinestoneAccount = await rhinestone.createAccount({
        owners: {
          type: "ecdsa",
          accounts: [wrappedWalletClient],
        },
      })

      setAccount(rhinestoneAccount)
    } catch (error) {
      console.error('Setup failed:', error)
    }
  }

  return (
    <div>
      {!account ? (
        <div>
          <input 
            value={email} 
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Enter email"
          />
          <button onClick={login}>Login with Magic</button>
        </div>
      ) : (
        <div>
          <p>Global Wallet: {account.getAddress()}</p>
          <p>Ready for cross-chain transactions!</p>
        </div>
      )}
    </div>
  )
}

Next Steps