> ## Documentation Index
> Fetch the complete documentation index at: https://daehan-base.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Coinbase Developer Platform

> Base Account와 CDP 임베디드 지갑을 모두 지원하는 온체인 앱 구축

# Base Account와 CDP 임베디드 지갑 통합

**기존 Base Account 사용자**와 CDP 임베디드 지갑을 통한 **신규 사용자** 모두를 원활하게 지원하는 온체인 앱을 구축하는 방법을 알아보세요. 통합 인증과 지갑 관리를 제공합니다.

## 개요

이 통합을 통해 앱은 두 가지 유형의 사용자를 지원할 수 있습니다:

* **기존 Base 사용자**: 친숙한 경험을 위해 Base Account로 연결
* **신규 온체인 사용자**: 이메일, 모바일, 또는 소셜 인증을 통해 CDP 임베디드 지갑 생성

두 유형의 사용자 모두 선호하는 지갑 유형을 사용하면서 동일한 앱 기능을 이용할 수 있습니다.

## 구축할 내용

* **통합 인증 플로우**: 두 가지 지갑 유형을 모두 지원하는 단일 로그인
* **자동 지갑 감지**: 사용자의 기존 지갑 상태에 따른 스마트 라우팅
* **일관된 사용자 경험**: 두 지갑 유형 모두 동일한 앱 기능에 접근

## 사전 요구 사항

* Node.js 18+ 설치
* React 애플리케이션 (Next.js 권장)
* Project ID가 있는 [CDP Portal 계정](https://portal.cdp.coinbase.com/)
* Wagmi 및 React hooks 기본 지식

## 설치

CDP 임베디드 지갑과 Base Account 지원에 필요한 패키지를 설치합니다:

```bash theme={null}
npm install @coinbase/cdp-core @coinbase/cdp-hooks @base-org/account @tanstack/react-query viem wagmi
```

## 단계별 구현

네이티브 CDP + Base Account 통합이 개발 중이므로, 이 가이드는 두 지갑 유형을 별도의 조율된 커넥터를 통해 지원하는 **이중 커넥터 방식**을 사용합니다.

Base Account Wagmi 커넥터와 CDP의 React 프로바이더 시스템을 함께 사용하면 두 지갑 유형의 지갑 지속성을 올바르게 처리하는 통합 경험을 만들 수 있습니다.

### 1단계: 환경 변수 설정

CDP 프로젝트의 환경 변수를 생성합니다:

```bash theme={null}
# .env.local
NEXT_PUBLIC_CDP_PROJECT_ID=your_cdp_project_id
NEXT_PUBLIC_APP_NAME="Your App Name"
```

CDP Project ID는 [CDP Portal](https://portal.cdp.coinbase.com/)에서 가져옵니다.

⚠️ **중요**: 유효한 `NEXT_PUBLIC_CDP_PROJECT_ID`가 없으면 앱이 "Project ID is required" 오류와 함께 로드되지 않습니다. 또한 CORS를 위해 CDP Portal → Wallets → Embedded Wallet 설정에서 도메인을 설정해야 합니다.

### 2단계: Base Account 지원을 위한 Wagmi 설정

Base Account 커넥터로 Wagmi를 설정합니다 (임베디드 지갑은 CDP React 프로바이더를 통해 별도로 처리됩니다):

```typescript theme={null}
// config/wagmi.ts
import { createConfig, http } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { baseAccount } from 'wagmi/connectors';

// Base Account 커넥터
const baseAccountConnector = baseAccount({
  appName: process.env.NEXT_PUBLIC_APP_NAME || 'Your App',
});

// Wagmi 설정 (Base Account만 해당 - 임베디드 지갑은 CDP React 프로바이더가 처리)
export const wagmiConfig = createConfig({
  connectors: [baseAccountConnector],
  chains: [baseSepolia, base], // 테스트를 위해 baseSepolia를 먼저 배치
  transports: {
    [base.id]: http(),
    [baseSepolia.id]: http(),
  },
});
```

### 3단계: 애플리케이션 프로바이더 설정

필요한 프로바이더로 애플리케이션을 래핑합니다. **중요**: 임베디드 지갑 인증 상태를 올바르게 관리하려면 `CDPHooksProvider`를 사용하세요:

```typescript theme={null}
// app/layout.tsx
'use client';

import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CDPHooksProvider } from '@coinbase/cdp-hooks';
import { wagmiConfig } from '../config/wagmi';

const queryClient = new QueryClient();

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <CDPHooksProvider 
          config={{
            projectId: process.env.NEXT_PUBLIC_CDP_PROJECT_ID!,
          }}
        >
          <WagmiProvider config={wagmiConfig}>
            <QueryClientProvider client={queryClient}>
              {children}
            </QueryClientProvider>
          </WagmiProvider>
        </CDPHooksProvider>
      </body>
    </html>
  );
}
```

### 4단계: 통합 인증 훅 생성

두 지갑 유형을 모두 관리하는 커스텀 훅을 구축합니다. `CDPHooksProvider`를 사용하면 사용자가 다시 로그인할 때 매번 새로운 지갑을 생성하는 대신 기존 임베디드 지갑을 가져올 수 있습니다.

```typescript theme={null}
// hooks/useUnifiedAuth.ts
import { useAccount, useConnect, useDisconnect } from 'wagmi';
import { useSignInWithEmail, useVerifyEmailOTP, useIsSignedIn, useEvmAddress, useSignOut } from '@coinbase/cdp-hooks';
import { useState, useEffect } from 'react';

export type WalletType = 'base_account' | 'embedded' | 'none';

export function useUnifiedAuth() {
  // Base Account를 위한 Wagmi 훅
  const { address: wagmiAddress, isConnected: wagmiConnected, connector } = useAccount();
  const { connect, connectors } = useConnect();
  const { disconnect: wagmiDisconnect } = useDisconnect();

  // 임베디드 지갑을 위한 CDP 훅 - CDPHooksProvider와 함께 동작
  const { signInWithEmail, isLoading: isSigningIn } = useSignInWithEmail();
  const { verifyEmailOTP, isLoading: isVerifying } = useVerifyEmailOTP();
  const { isSignedIn: cdpSignedIn } = useIsSignedIn();
  const { evmAddress: cdpAddress } = useEvmAddress();
  const { signOut } = useSignOut();

  const [walletType, setWalletType] = useState<WalletType>('none');
  const [flowId, setFlowId] = useState<string>('');

  // 활성 지갑 결정 및 우선순위 지정
  const address = wagmiConnected ? wagmiAddress : cdpAddress;
  const isConnected = wagmiConnected || cdpSignedIn;

  useEffect(() => {
    if (wagmiConnected && connector?.name === 'Base Account') {
      setWalletType('base_account');
    } else if (cdpSignedIn && cdpAddress) {
      setWalletType('embedded');
    } else {
      setWalletType('none');
    }
  }, [wagmiConnected, cdpSignedIn, connector, cdpAddress]);

  const connectBaseAccount = () => {
    const baseConnector = connectors.find(c => c.name === 'Base Account');
    if (baseConnector) {
      connect({ connector: baseConnector });
    }
  };

  const signInWithEmbeddedWallet = async (email: string) => {
    try {
      const response = await signInWithEmail({ email });

      // OTP 검증을 위한 flowId 캡처
      if (response && typeof response === 'object' && 'flowId' in response) {
        setFlowId(response.flowId as string);
      }

      return true;
    } catch (error) {
      console.error('Failed to sign in with email:', error);
      return false;
    }
  };

  const verifyOtpAndConnect = async (otp: string) => {
    try {
      // CDPReactProvider에서 verifyEmailOTP는 자동으로 사용자를 로그인시킴
      await verifyEmailOTP({ flowId, otp });
      return true;
    } catch (error) {
      console.error('Failed to verify OTP:', error);
      return false;
    }
  };

  const disconnect = async () => {
    if (wagmiConnected) {
      wagmiDisconnect();
    }

    if (cdpSignedIn || walletType === 'embedded') {
      try {
        await signOut();
      } catch (error) {
        console.error('CDP sign out failed:', error);
      }
    }
  };

  return {
    address,
    isConnected,
    walletType,
    connectBaseAccount,
    signInWithEmbeddedWallet,
    verifyOtpAndConnect,
    disconnect,
    isSigningIn,
    isVerifying,
  };
}
```

### 5단계: 인증 컴포넌트 구축

두 가지 인증 옵션을 제공하는 컴포넌트를 생성합니다:

```typescript theme={null}
// components/WalletAuthButton.tsx
'use client';

import { useState } from 'react';
import { useUnifiedAuth } from '../hooks/useUnifiedAuth';

export function WalletAuthButton() {
  const {
    address,
    isConnected,
    walletType,
    connectBaseAccount,
    signInWithEmbeddedWallet,
    verifyOtpAndConnect,
    disconnect,
    isSigningIn,
    isVerifying,
  } = useUnifiedAuth();

  const [authStep, setAuthStep] = useState<'select' | 'email' | 'otp'>('select');
  const [email, setEmail] = useState('');
  const [otp, setOtp] = useState('');

  // 연결된 상태
  if (isConnected && address) {
    const walletDisplay = {
      base_account: { name: 'Base Account', icon: '🟦' },
      embedded: { name: 'Embedded Wallet', icon: '📱' },
    }[walletType] || { name: 'Connected', icon: '✅' };

    return (
      <div className="flex items-center space-x-3 px-4 py-2 bg-green-50 border border-green-200 rounded-lg">
        <span>{walletDisplay.icon}</span>
        <div>
          <div className="font-medium text-green-800">{walletDisplay.name}</div>
          <div className="text-xs text-green-600 font-mono">
            {address.slice(0, 6)}...{address.slice(-4)}
          </div>
        </div>
        <button onClick={() => disconnect()} className="text-sm text-red-600">
          연결 해제
        </button>
      </div>
    );
  }

  // OTP 검증
  if (authStep === 'otp') {
    return (
      <div className="space-y-4 p-4 border rounded-lg">
        <div className="text-center">
          <h3 className="font-semibold">이메일을 확인하세요</h3>
          <p className="text-sm text-gray-600">{email}으로 발송된 코드를 입력하세요</p>
        </div>

        <input
          type="text"
          value={otp}
          onChange={(e) => setOtp(e.target.value)}
          placeholder="000000"
          maxLength={6}
          className="w-full px-3 py-2 border rounded text-center font-mono"
        />

        <div className="space-y-2">
          <button
            onClick={() => verifyOtpAndConnect(otp)}
            disabled={otp.length !== 6 || isVerifying}
            className="w-full px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
          >
            {isVerifying ? '계정 생성 중...' : '인증 및 계정 생성'}
          </button>

          <button
            onClick={() => setAuthStep('email')}
            className="w-full px-4 py-2 text-gray-600 hover:text-gray-800"
          >
            뒤로
          </button>
        </div>
      </div>
    );
  }

  // 이메일 입력
  if (authStep === 'email') {
    return (
      <div className="space-y-4 p-4 border rounded-lg">
        <h3 className="font-semibold text-center">계정 생성</h3>

        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="your@email.com"
          className="w-full px-3 py-2 border rounded"
        />

        <div className="space-y-2">
          <button
            onClick={async () => {
              const success = await signInWithEmbeddedWallet(email);
              if (success) setAuthStep('otp');
            }}
            disabled={!email || isSigningIn}
            className="w-full px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
          >
            {isSigningIn ? '코드 발송 중...' : '인증 코드 발송'}
          </button>

          <button
            onClick={() => setAuthStep('select')}
            className="w-full px-4 py-2 text-gray-600 hover:text-gray-800"
          >
            뒤로
          </button>
        </div>
      </div>
    );
  }

  // 초기 선택
  return (
    <div className="space-y-3">
      <h2 className="text-xl font-bold text-center mb-4">지갑 연결</h2>

      <button
        onClick={connectBaseAccount}
        className="w-full p-4 border-2 border-blue-200 rounded-lg hover:bg-blue-50"
      >
        <div className="flex items-center space-x-3">
          <span className="text-2xl">🟦</span>
          <div className="text-left">
            <div className="font-semibold">Base로 로그인</div>
            <div className="text-sm text-gray-600">Base Account가 있습니다</div>
          </div>
        </div>
      </button>

      <button
        onClick={() => setAuthStep('email')}
        className="w-full p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-50"
      >
        <div className="flex items-center space-x-3">
          <span className="text-2xl">📱</span>
          <div className="text-left">
            <div className="font-semibold">새 계정 생성</div>
            <div className="text-sm text-gray-600">이메일로 시작하기</div>
          </div>
        </div>
      </button>
    </div>
  );
}
```

### 6단계: 각 지갑 유형별 트랜잭션 처리

각 지갑 유형에 맞게 동작하는 트랜잭션 컴포넌트를 생성합니다:

```typescript theme={null}
// components/SendTransaction.tsx
import { useState } from 'react';
import { parseEther } from 'viem';
import { useSendTransaction, useWaitForTransactionReceipt, useAccount, useSwitchChain } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { useUnifiedAuth } from '../hooks/useUnifiedAuth';

export function SendTransaction() {
  const { address, walletType } = useUnifiedAuth();
  const { chain } = useAccount();
  const { switchChain } = useSwitchChain();
  const [amount, setAmount] = useState('');
  const [recipient, setRecipient] = useState('');

  const { data: hash, sendTransaction, isPending, error } = useSendTransaction();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });

  const handleTransaction = async () => {
    if (!address || !amount || !recipient) return;

    try {
      sendTransaction({
        to: recipient as `0x${string}`,
        value: parseEther(amount),
      });
    } catch (error) {
      console.error('Transaction failed:', error);
    }
  };

  // 지갑 유형에 따른 안내 표시
  const getTransactionGuidance = () => {
    switch (walletType) {
      case 'base_account':
        return {
          title: 'Base Account 트랜잭션',
          description: '패스키로 확인하라는 프롬프트가 표시됩니다',
          icon: '🔐'
        };
      case 'embedded':
        return {
          title: '임베디드 지갑 트랜잭션',
          description: '트랜잭션이 자동으로 서명됩니다',
          icon: '⚡'
        };
      default:
        return { title: '트랜잭션 발송', description: '', icon: '💸' };
    }
  };

  const guidance = getTransactionGuidance();

  if (!address) return null;

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
      <div className="text-center mb-6">
        <div className="text-3xl mb-2">{guidance.icon}</div>
        <h3 className="text-lg font-bold">{guidance.title}</h3>
        <p className="text-sm text-gray-600">{guidance.description}</p>

        {/* 네트워크 표시기 및 전환 */}
        <div className="mt-3 p-2 bg-gray-50 rounded border">
          <div className="flex items-center justify-between">
            <span className="text-sm">
              네트워크: <strong>{chain?.name || '알 수 없음'}</strong>
            </span>
            <div className="space-x-1">
              {chain?.id !== baseSepolia.id && (
                <button
                  onClick={() => switchChain({ chainId: baseSepolia.id })}
                  className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
                >
                  → Sepolia
                </button>
              )}
              {chain?.id !== base.id && (
                <button
                  onClick={() => switchChain({ chainId: base.id })}
                  className="px-2 py-1 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200"
                >
                  → Mainnet
                </button>
              )}
            </div>
          </div>
        </div>
      </div>

      <div className="space-y-4">
        <div>
          <label className="block text-sm font-medium mb-1">금액 (ETH)</label>
          <input
            type="number"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            placeholder="0.001"
            step="0.001"
            className="w-full px-3 py-2 border border-gray-300 rounded"
          />
        </div>

        <div>
          <label className="block text-sm font-medium mb-1">수신 주소</label>
          <input
            type="text"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
            placeholder="0x..."
            className="w-full px-3 py-2 border border-gray-300 rounded font-mono text-sm"
          />
        </div>

        {error && (
          <div className="p-3 bg-red-50 border border-red-200 rounded">
            <p className="text-sm text-red-800">오류: {error.message}</p>
          </div>
        )}

        <button
          onClick={handleTransaction}
          disabled={!amount || !recipient || isPending || isConfirming}
          className="w-full px-4 py-3 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
        >
          {isPending || isConfirming ? '처리 중...' : '트랜잭션 발송'}
        </button>

        {isSuccess && hash && (
          <div className="p-3 bg-green-50 border border-green-200 rounded text-center">
            <p className="text-green-800 font-medium mb-2">✅ 트랜잭션 확인됨!</p>
            <a
              href={`https://${chain?.id === baseSepolia.id ? 'sepolia.' : ''}basescan.org/tx/${hash}`}
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-600 hover:text-blue-800 text-sm underline"
            >
              {chain?.id === baseSepolia.id ? 'Sepolia ' : ''}Basescan에서 보기 →
            </a>
          </div>
        )}
      </div>
    </div>
  );
}
```

### 7단계: 앱 완성

메인 애플리케이션에 모든 것을 조합합니다:

```typescript theme={null}
// app/page.tsx
'use client';

import { WalletAuthButton } from '../components/WalletAuthButton';
import { SendTransaction } from '../components/SendTransaction';
import { useAccount } from 'wagmi';

export default function HomePage() {
  const { isConnected } = useAccount();

  return (
    <div className="min-h-screen bg-gray-50 py-12 px-4">
      <div className="max-w-2xl mx-auto">
        <div className="text-center mb-8">
          <h1 className="text-3xl font-bold mb-2">CDP + Base Account 데모</h1>
          <p className="text-gray-600">
            Base Account와 임베디드 지갑 사용자를 모두 지원하는 앱
          </p>
        </div>

        <div className="space-y-6">
          <WalletAuthButton />
          {isConnected && <SendTransaction />}
        </div>
      </div>
    </div>
  );
}
```

## 문제 해결

### 일반적인 문제

**Base Account 커넥터가 표시되지 않음**

* Base Account SDK(`@base-org/account`)가 설치되고 최신 상태인지 확인
* wagmi 설정에 Base Account 커넥터가 포함되어 있는지 확인
* 앱이 Base 또는 Base Sepolia 네트워크에서 실행 중인지 확인

**CDP 임베디드 지갑 인증 실패**

* 환경 변수에 CDP Project ID가 올바른지 확인
* **중요**: CDP Portal → Wallets → Embedded Wallet 설정 → Allowed domains에 도메인(예: `http://localhost:3000`, `http://localhost:3001`)을 추가해야 합니다
* 필요한 CDP 패키지(위 참조)가 모두 설치되어 있는지 확인

**로그인할 때마다 새 지갑이 생성됨**

* 레이아웃에 올바른 설정으로 `CDPHooksProvider`를 사용하고 있는지 확인
* CDP Project ID가 올바르게 설정되어 있는지 확인
* 훅이 `@coinbase/cdp-hooks`에서 일관되게 임포트되고 있는지 확인

**사용자가 지갑 유형 간 전환 불가**

* 다른 유형의 지갑을 연결하기 전에 올바른 연결 해제 플로우 구현
* 전환 시 캐시된 인증 상태 초기화
* 지갑 유형 선택을 위한 명확한 UI 안내 제공

## 향상된 통합 예정

Base Account와 CDP 임베디드 지갑의 네이티브 통합을 적극 개발 중이며, 다음 기능이 지원될 예정입니다:

* **통합 커넥터**: 두 지갑 유형을 원활하게 처리하는 단일 CDP 커넥터
* **스펜드 퍼미션**: 서브 어카운트가 상위 Base Account 잔액을 한도 내에서 접근 가능
* **서브 어카운트 생성**: Base Account 사용자가 앱별 서브 어카운트 생성 가능

## 리소스

* [CDP 임베디드 지갑 문서](https://docs.cdp.coinbase.com/embedded-wallets/)
* [CDP React 컴포넌트 문서](https://docs.cdp.coinbase.com/embedded-wallets/components)
* [Base Account Wagmi 설정](/base-account/framework-integrations/wagmi/setup)
* [CDP Portal](https://portal.cdp.coinbase.com/)
* [Wagmi 문서](https://wagmi.sh/)

향상된 임베디드 지갑 Base Account 통합 기능 업데이트를 위해 [CDP 문서](https://docs.cdp.coinbase.com/)를 주기적으로 확인하세요.
