Skip to main content

Overview

Magic provides wallet infrastructure with two integration paths:
  • Embedded Wallets: Client-side Magic SDK handles authentication and signing. Keys are managed by Magic and signing happens through their RPC provider.
  • Server Wallets: Backend-managed wallets via Magic Express API. Keys live in Magic’s TEE (Trusted Execution Environment) and signing happens through server-side API calls.
Both paths produce a viem-compatible account that becomes the owner of a Rhinestone smart account, enabling cross-chain functionality.

Integration

Prerequisites

  • A Magic account and publishable API key
  • Rhinestone API key
  • React application setup
1

Install Dependencies

npm install magic-sdk @rhinestone/sdk viem
2

Set Up Magic SDK

Initialize the Magic SDK in your application:
import { Magic } from 'magic-sdk'

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

Authenticate and Get Wallet Address

Use Magic’s client-side SDK for authentication:
// Login with Magic email OTP
await magic.auth.loginWithEmailOTP({ email: "user@example.com" })

// Get the wallet address
const accounts = (await magic.rpcProvider.request({
  method: "eth_accounts",
})) as string[]

const address = accounts[0]
Magic also supports SMS, social login, WebAuthn, and more. See Magic’s authentication overview for all options.
4

Create Rhinestone Account

Create a viem account from Magic’s provider and pass it to Rhinestone:
import { RhinestoneSDK, walletClientToAccount } from "@rhinestone/sdk"
import { createWalletClient, custom } from "viem"
import { toAccount } from "viem/accounts"
import type { SignableMessage, TypedDataDefinition } from "viem"

// Magic RPC requires BigInts as decimal strings
function serializeBigInts(obj: unknown): unknown {
  if (obj === null || obj === undefined) return obj
  if (typeof obj === "bigint") return obj.toString(10)
  if (Array.isArray(obj)) return obj.map(serializeBigInts)
  if (typeof obj === "object") {
    const result: Record<string, unknown> = {}
    for (const key in obj) {
      result[key] = serializeBigInts((obj as Record<string, unknown>)[key])
    }
    return result
  }
  return obj
}

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

const wrappedWalletClient = walletClientToAccount(walletClient)

// 2. Wrap with toAccount to handle BigInt serialization
//    for signTypedData (required by Magic's RPC)
const account = toAccount({
  address: wrappedWalletClient.address,
  async signMessage({ message }: { message: SignableMessage }) {
    return wrappedWalletClient.signMessage({ message })
  },
  async signTransaction(transaction: any) {
    return wrappedWalletClient.signTransaction(transaction)
  },
  async signTypedData(typedData: TypedDataDefinition) {
    const serialized = serializeBigInts(typedData)
    return wrappedWalletClient.signTypedData(serialized as any)
  },
})

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

Cross-Chain Transactions

Once initialized, both wallet types use the same Rhinestone API:
async function handleCrossChainTransfer() {
  const transaction = await rhinestoneAccount.sendTransaction({
    sourceChains: [baseSepolia],
    targetChain: arbitrumSepolia,
    calls: [
      {
        to: "USDC",
        data: encodeFunctionData({
          abi: erc20Abi,
          functionName: "transfer",
          args: ["0xrecipient", parseUnits("10", 6)],
        }),
      },
    ],
    tokenRequests: [
      {
        address: "USDC",
        amount: parseUnits("10", 6),
      },
    ],
  })
}

Next Steps