Skip to main content

Documentation Index

Fetch the complete documentation index at: https://daehan-base.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

서브 어카운트란?

서브 어카운트를 통해 애플리케이션에 직접 임베드된 앱별 지갑 계정을 사용자에게 프로비저닝할 수 있습니다. 생성 후에는 지갑 프로바이더나 wagmi, viem과 같은 인기 온체인 라이브러리를 통해 다른 지갑과 동일하게 상호작용할 수 있습니다.
완전한 구현을 찾고 계신가요? 완전한 통합 예제로 바로 이동하세요.
동영상 콘텐츠를 선호하시나요?이 페이지의 마지막 섹션에 구현을 자세히 다루는 동영상 가이드가 있습니다.

주요 이점

  • 마찰 없는 트랜잭션: 고빈도 및 에이전틱 사용 사례를 위한 반복적인 서명 프롬프트를 제거하거나 트랜잭션 플로우를 완전히 제어합니다.
  • 자금 조달 플로우 불필요: 스펜드 퍼미션을 통해 서브 어카운트가 범용 Base Account의 잔액에서 직접 지출할 수 있습니다.
  • 사용자 제어: 사용자는 account.base.app에서 모든 서브 어카운트를 관리할 수 있습니다.
서브 어카운트의 라이브 데모를 보려면 서브 어카운트 데모를 확인하세요.
스펜드 퍼미션서브 어카운트는 앱이 사용자의 기존 Base Account 잔액을 활용할 수 있도록 스펜드 퍼미션과 함께 사용하도록 최적화되어 있습니다. 작동 방식에 대한 자세한 내용은 스펜드 퍼미션 가이드를 참조하세요.

설치

Base Account SDK를 설치합니다:
npm install @base-org/account

빠른 시작

서브 어카운트를 채택하는 가장 빠른 방법은 SDK 설정에서 creationon-connect로, defaultAccountsub로 설정하는 것입니다.
page.tsx
const sdk = createBaseAccountSDK({
  // ...
  subAccounts: {
    creation: 'on-connect',
    defaultAccount: 'sub',
  }
});
이렇게 하면 사용자가 Base Account를 연결할 때 자동으로 서브 어카운트가 생성되고, from 파라미터를 범용 계정 주소로 지정하지 않는 한 트랜잭션은 자동으로 서브 어카운트에서 전송됩니다. 앱이 필요할 때 서브 어카운트에 대한 스펜드 퍼미션도 자동으로 요청됩니다. 자동 서브 어카운트가 활성화된 상태에서 사용자가 Base Account를 연결할 때 표시되는 화면:
서브 어카운트 생성 플로우
서브 어카운트 통합 시 최상의 사용자 경험을 위해 페이마스터를 사용하여 가스를 스폰서하는 것을 권장합니다. SDK 설정에서 paymasterUrls 파라미터를 구성하여 모든 트랜잭션에 페이마스터를 사용하도록 설정할 수 있습니다. 이 옵션은 createBaseAccount SDK 초기화 설정에서 함께 구성할 수 있습니다.
동영상 콘텐츠를 선호하시나요?이 페이지의 마지막 섹션에 이 특정 구현을 다루는 동영상 가이드가 있습니다.

서브 어카운트 사용

SDK 초기화

먼저 Base Account SDK를 설정합니다. appNameappLogoUrl을 앱에 맞게 커스터마이징하세요. 이는 지갑 연결 팝업과 account.base.app 대시보드에 표시됩니다. 또한 앱이 지원하는 체인으로 appChainIds를 커스터마이징할 수 있습니다.
page.tsx
import { createBaseAccountSDK, getCryptoKeyAccount } from '@base-org/account';
import { base } from 'viem/chains';

// 서브 어카운트 설정으로 SDK 초기화
const sdk = createBaseAccountSDK({
  appName: 'Base Account SDK 데모',
  appLogoUrl: 'https://base.org/logo.png',
  appChainIds: [base.id],
});

// EIP-1193 프로바이더 가져오기
const provider = sdk.getProvider()

서브 어카운트 생성

서브 어카운트를 생성하기 전에 사용자를 Base Account로 인증해야 합니다. 다음 중 하나를 선택할 수 있습니다:
  • 사용자 인증 가이드를 따르세요
  • 간단한 지갑 연결을 위해 provider.request({ method: 'eth_requestAccounts' });를 사용하세요
프로바이더의 wallet_addSubAccount RPC 메서드를 사용하여 애플리케이션을 위한 서브 어카운트를 생성합니다. publicKey 파라미터가 제공되지 않으면 추출 불가능한 브라우저 CryptoKey가 생성되어 서브 어카운트를 대신해 서명합니다.
page.tsx
// 서브 어카운트 생성
const subAccount = await provider.request({
  method: 'wallet_addSubAccount',
  params: [
    {
      account: {
        type: 'create',
      },
    }
  ],
});

console.log('Sub Account created:', subAccount.address);
또는 SDK 편의 메서드를 사용할 수 있습니다:
page.tsx
const subAccount = await sdk.subAccount.create();

console.log('Sub Account created:', subAccount.address);
서브 어카운트 생성 프롬프트가 표시될 때 사용자에게 보이는 화면:
서브 어카운트 생성 플로우

기존 서브 어카운트 가져오기

프로바이더의 wallet_getSubAccounts RPC 메서드를 사용하여 기존 서브 어카운트를 조회합니다. 앱의 도메인과 연결된 서브 어카운트를 반환하며, 사용자에게 이미 서브 어카운트가 존재하는지 확인하는 데 유용합니다.
page.tsx
// 범용 계정 가져오기
const [universalAddress] = await provider.request({
  method: "eth_requestAccounts",
  params: []
})

// 범용 계정의 서브 어카운트 가져오기
const { subAccounts: [subAccount] } = await provider.request({
  method: 'wallet_getSubAccounts',
  params: [{
    account: universalAddress,
    domain: window.location.origin,
  }]
})

if (subAccount) {
  console.log('Sub Account found:', subAccount.address);
} else {
  console.log('이 앱에 대한 서브 어카운트가 없습니다');
}
또는 SDK 편의 메서드를 사용할 수 있습니다:
page.tsx
const subAccount = await sdk.subAccount.get();

console.log('Sub Account:', subAccount);

트랜잭션 전송

연결된 서브 어카운트에서 트랜잭션을 전송하려면 EIP-5792 wallet_sendCalls 또는 eth_sendTransaction을 사용합니다. from 파라미터를 서브 어카운트 주소로 지정해야 합니다.
서브 어카운트가 연결되면 eth_requestAccounts 또는 eth_accounts가 반환하는 배열의 두 번째 계정이 됩니다. wallet_addSubAccount는 서브 어카운트를 사용하기 전에 각 세션에서 호출되어야 합니다. 이미 서브 어카운트가 있는 경우 새로운 서브 어카운트가 생성되지 않습니다.mode: 'auto'를 사용하는 경우 서브 어카운트가 배열의 첫 번째 계정이 됩니다.
먼저, 서브 어카운트가 두 번째 계정이 되는 모든 사용 가능한 계정을 가져옵니다:
page.tsx
const [universalAddress, subAccountAddress] = await provider.request({
  method: "eth_requestAccounts", // 이미 연결된 경우 "eth_accounts"
  params: []
})
그런 다음 서브 어카운트에서 트랜잭션을 전송합니다: wallet_sendCalls
page.tsx
const callsId = await provider.request({
  method: 'wallet_sendCalls',
  params: [{
    version: "2.0",
    atomicRequired: true,
    from: subAccountAddress, // 서브 어카운트 주소 지정
    calls: [{
      to: '0x...',
      data: '0x...',
      value: '0x...',
    }],
    capabilities: {
      // https://docs.cdp.coinbase.com/paymaster/introduction/welcome
      paymasterUrl: "https://...",
    },
  }]
})

console.log('Calls sent:', callsId);
eth_sendTransaction
page.tsx
const tx = await provider.request({
  method: 'eth_sendTransaction',
  params: [{
    from: subAccountAddress, // 서브 어카운트 주소 지정
    to: '0x...',
    data: '0x...',
    value: '0x...',
  }]
})

console.log('Transaction sent:', tx);
최상의 사용자 경험을 위해 페이마스터와 함께 wallet_sendCalls를 사용하는 것을 권장합니다. 자세한 내용은 페이마스터 가이드를 참조하세요.

고급 사용법

기존 계정 임포트

이미 배포된 스마트 컨트랙트 계정이 있고 이를 연결된 Base Account의 서브 어카운트로 만들고 싶다면 프로바이더 RPC 메서드를 사용하여 임포트할 수 있습니다:
page.tsx
const subAccount = await provider.request({
  method: 'wallet_addSubAccount',
  params: [
    {
      account: {
        type: 'deployed',
        address: '0xYourSmartContractAccountAddress',
        chainId: 8453 // 계정이 배포된 체인
      },
    }
  ],
});

console.log('Sub Account added:', subAccount.address);
서브 어카운트를 임포트하기 전에 Base Account 주소를 서브 어카운트의 소유자로 추가해야 합니다. 현재 이것은 임포트하려는 서브 어카운트의 스마트 컨트랙트에서 addOwnerAddress 또는 addOwnerPublicKey 함수를 수동으로 호출하고 Base Account 주소를 소유자로 설정해야 합니다.또한 현재 Coinbase 스마트 지갑 컨트랙트만 Base Account의 서브 어카운트로 임포트하는 것을 지원합니다.Coinbase 스마트 지갑 컨트랙트 ABI는 GitHub에서 확인할 수 있습니다.

소유자 계정 추가

서브 어카운트는 서명이 필요할 때 소유권 업데이트가 필요한 경우 자동으로 감지하여 서명 전에 사용자에게 업데이트를 승인하도록 프롬프트합니다. 하지만 SDK 편의 메서드를 사용하여 서브 어카운트에 소유자를 수동으로 추가할 수도 있습니다:
page.tsx
const ownerAccount = await sdk.subAccount.addOwner({
  address: subAccount?.address,
  publicKey: cryptoAccount?.account?.publicKey,
  chainId: base.id,
});

console.log('서브 어카운트에 소유자 추가됨');
이는 서브 어카운트의 스마트 컨트랙트에서 addOwnerAddress 또는 addOwnerPublicKey 함수를 호출하여 소유자를 추가하는 트랜잭션을 생성합니다.
사용자가 새 기기나 브라우저에서 앱에 로그인하면 소유권 변경이 예상됩니다.서버(예: Node.js)에서 SDK를 사용할 때 앱의 서브 어카운트 서명자 키를 잃지 않도록 주의하세요. 소유자를 업데이트하려면 사용자의 서명이 필요하며, 이는 서버 컨텍스트에서 요청할 수 없습니다.

자동 스펜드 퍼미션

자동 스펜드 퍼미션을 통해 서브 어카운트는 트랜잭션 잔액이 부족할 때 상위 Base Account에서 자금을 접근할 수 있습니다. 이 기능은 또한 지속적인 스펜드 퍼미션을 설정하여 미래 트랜잭션이 사용자 승인 프롬프트 없이 실행될 수 있게 하여 앱의 트랜잭션 플로우의 마찰을 줄입니다. 이 기능은 서브 어카운트 사용 시 기본적으로 활성화됩니다.

작동 방식

첫 번째 트랜잭션 플로우: 서브 어카운트가 첫 번째 트랜잭션을 시도하면 Base Account가 사용자 승인을 위한 팝업을 표시합니다. 이 승인 과정에서 Base Account는:
  • 트랜잭션에 필요한 누락된 토큰(네이티브 또는 ERC-20)을 자동으로 감지합니다
  • 현재 트랜잭션을 이행하기 위해 상위 Base Account에서 서브 어카운트로 필요한 자금 전송을 요청합니다
  • 사용자가 선택적으로 해당 토큰에 대한 미래 트랜잭션을 위한 지속적인 스펜드 퍼미션을 부여할 수 있도록 합니다
이후 트랜잭션: 사용자가 스펜드 퍼미션을 부여한 경우, 미래 트랜잭션은 다음 우선순위를 따릅니다:
  1. 먼저 기존 서브 어카운트 잔액과 부여된 스펜드 퍼미션 사용 시도
  2. 부족한 경우 Base Account에서 추가 전송 및/또는 스펜드 퍼미션 승인을 위해 사용자에게 프롬프트 표시
스펜드 퍼미션 요청은 단일 트랜잭션에 여러 전송이 필요한 경우 첫 번째 토큰으로 제한됩니다. 추가 토큰은 별도의 승인이 필요합니다.

설정

사용자의 서브 어카운트가 수동으로 자금이 조달될 경우 SDK 설정에서 fundingmanual로 설정하여 자동 스펜드 퍼미션을 비활성화할 수 있습니다:
page.tsx
const sdk = createBaseAccountSDK({
  appName: 'Base Account SDK 데모',
  appLogoUrl: 'https://base.org/logo.png',
  appChainIds: [base.id],
  subAccounts: {
    funding: 'manual', // 자동 스펜드 퍼미션 비활성화
  }
});

기술적 세부사항

Base Account의 자기 보관 설계는 트랜잭션이나 메시지 서명과 같은 각 지갑 상호작용마다 사용자 패스키 프롬프트를 요구합니다. 이를 통해 모든 지갑 상호작용에 대한 사용자 인식과 승인이 보장되지만, 빈번한 지갑 상호작용이 필요한 애플리케이션에서는 사용자 경험에 영향을 줄 수 있습니다. 개발자가 지갑 상호작용을 더 많이 제어할 수 있는 사용자 경험으로 Base Account를 지원하기 위해, 우리는 ERC-7895와 함께 서브 어카운트를 구축했습니다. 이는 지갑 계정 간 계층적 관계를 생성하는 새로운 지갑 RPC입니다. 이 서브 어카운트들은 온체인 관계를 통해 최종 사용자의 Base Account와 연결됩니다. 스펜드 퍼미션 기능과 결합하면 앱 계정을 안전하게 프로비저닝하고 자금을 조달하는 강력한 기반을 만들면서, 애플리케이션에 가장 적합한 사용자 경험을 구축하는 데 충분한 제어권을 제공합니다.

완전한 통합 예제

서브 어카운트 생성과 사용을 보여주는 전체 React 컴포넌트:
page.tsx
import { createBaseAccountSDK } from "@base-org/account";
import { useCallback, useEffect, useState } from "react";
import { baseSepolia } from "viem/chains";

interface SubAccount {
  address: `0x${string}`;
  factory?: `0x${string}`;
  factoryData?: `0x${string}`;
}

interface GetSubAccountsResponse {
  subAccounts: SubAccount[];
}

interface WalletAddSubAccountResponse {
  address: `0x${string}`;
  factory?: `0x${string}`;
  factoryData?: `0x${string}`;
}

export default function SubAccountDemo() {
  const [provider, setProvider] = useState<ReturnType<
    ReturnType<typeof createBaseAccountSDK>["getProvider"]
  > | null>(null);
  const [subAccount, setSubAccount] = useState<SubAccount | null>(null);
  const [universalAddress, setUniversalAddress] = useState<string>("");
  const [connected, setConnected] = useState(false);
  const [loadingSubAccount, setLoadingSubAccount] = useState(false);
  const [loadingUniversal, setLoadingUniversal] = useState(false);
  const [status, setStatus] = useState("");

  // SDK 및 크립토 계정 초기화
  useEffect(() => {
    const initializeSDK = async () => {
      try {
        const sdkInstance = createBaseAccountSDK({
          appName: "서브 어카운트 데모",
          appChainIds: [baseSepolia.id],
        });

        // 프로바이더 가져오기
        const providerInstance = sdkInstance.getProvider();
        setProvider(providerInstance);

        setStatus("SDK 초기화됨 - 연결 준비 완료");
      } catch (error) {
        console.error("SDK initialization failed:", error);
        setStatus("SDK 초기화 실패");
      }
    };

    initializeSDK();
  }, []);

  const connectWallet = async () => {
    if (!provider) {
      setStatus("프로바이더가 초기화되지 않았습니다");
      return;
    }

    setLoadingSubAccount(true);
    setStatus("지갑 연결 중...");

    try {
      // 지갑 연결
      const accounts = (await provider.request({
        method: "eth_requestAccounts",
        params: [],
      })) as string[];

      const universalAddr = accounts[0];
      setUniversalAddress(universalAddr);
      setConnected(true);

      // 기존 서브 어카운트 확인
      const response = (await provider.request({
        method: "wallet_getSubAccounts",
        params: [
          {
            account: universalAddr,
            domain: window.location.origin,
          },
        ],
      })) as GetSubAccountsResponse;

      const existing = response.subAccounts[0];
      if (existing) {
        setSubAccount(existing);
        setStatus("연결됨! 기존 서브 어카운트 발견");
      } else {
        setStatus("연결됨! 기존 서브 어카운트 없음");
      }
    } catch (error) {
      console.error("Connection failed:", error);
      setStatus("연결 실패");
    } finally {
      setLoadingSubAccount(false);
    }
  };

  const createSubAccount = async () => {
    if (!provider) {
      setStatus("프로바이더가 초기화되지 않았습니다");
      return;
    }

    setLoadingSubAccount(true);
    setStatus("서브 어카운트 생성 중...");

    try {
      const newSubAccount = (await provider.request({
        method: "wallet_addSubAccount",
        params: [
          {
            account: {
              type: 'create',
            },
          }
        ],
      })) as WalletAddSubAccountResponse;

      setSubAccount(newSubAccount);
      setStatus("서브 어카운트가 성공적으로 생성됨!");
    } catch (error) {
      console.error("Sub Account creation failed:", error);
      setStatus("서브 어카운트 생성 실패");
    } finally {
      setLoadingSubAccount(false);
    }
  };

  const sendCalls = useCallback(
    async (
      calls: Array<{ to: string; data: string; value: string }>,
      from: string,
      setLoadingState: (loading: boolean) => void
    ) => {
      if (!provider) {
        setStatus("프로바이더를 사용할 수 없습니다");
        return;
      }

      setLoadingState(true);
      setStatus("콜 전송 중...");

      try {
        const callsId = (await provider.request({
          method: "wallet_sendCalls",
          params: [
            {
              version: "2.0",
              atomicRequired: true,
              chainId: `0x${baseSepolia.id.toString(16)}`, // 16진수로 변환
              from,
              calls,
              capabilities: {
                // https://docs.cdp.coinbase.com/paymaster/introduction/welcome
                // paymasterUrl: "your paymaster url",
              },
            },
          ],
        })) as string;

        setStatus(`콜 전송됨! 콜 ID: ${callsId}`);
      } catch (error) {
        console.error("Send calls failed:", error);
        setStatus("콜 전송 실패");
      } finally {
        setLoadingState(false);
      }
    },
    [provider]
  );

  const sendCallsFromSubAccount = useCallback(async () => {
    if (!subAccount) {
      setStatus("서브 어카운트를 사용할 수 없습니다");
      return;
    }

    const calls = [
      {
        to: "0x4bbfd120d9f352a0bed7a014bd67913a2007a878",
        data: "0x9846cd9e", // yoink
        value: "0x0",
      },
    ];

    await sendCalls(calls, subAccount.address, setLoadingSubAccount);
  }, [sendCalls, subAccount]);

  const sendCallsFromUniversal = useCallback(async () => {
    if (!universalAddress) {
      setStatus("범용 계정을 사용할 수 없습니다");
      return;
    }

    const calls = [
      {
        to: "0x4bbfd120d9f352a0bed7a014bd67913a2007a878",
        data: "0x9846cd9e", // yoink
        value: "0x0",
      },
    ];

    await sendCalls(calls, universalAddress, setLoadingUniversal);
  }, [sendCalls, universalAddress]);

  return (
    <div className="sub-account-demo">
      <h2>서브 어카운트 데모</h2>

      <div className="status">
        <p>
          <strong>상태:</strong> {status}
        </p>
        {universalAddress && (
          <p>
            <strong>범용 계정:</strong> {universalAddress}
          </p>
        )}
        {subAccount && (
          <p>
            <strong>서브 어카운트:</strong> {subAccount.address}
          </p>
        )}
      </div>

      <div className="actions">
        {!connected ? (
          <button
            onClick={connectWallet}
            disabled={loadingSubAccount || !provider}
            className="connect-btn"
          >
            {loadingSubAccount ? "연결 중..." : "지갑 연결"}
          </button>
        ) : !subAccount ? (
          <button
            onClick={createSubAccount}
            disabled={loadingSubAccount}
            className="create-btn"
          >
            {loadingSubAccount ? "생성 중..." : "서브 어카운트 추가"}
          </button>
        ) : (
          <div>
            <button
              onClick={sendCallsFromSubAccount}
              disabled={loadingSubAccount}
              className="sub-account-btn"
            >
              {loadingSubAccount ? "전송 중..." : "서브 어카운트에서 콜 전송"}
            </button>
            <button
              onClick={sendCallsFromUniversal}
              disabled={loadingUniversal}
              className="universal-btn"
            >
              {loadingUniversal
                ? "전송 중..."
                : "범용 계정에서 콜 전송"}
            </button>
          </div>
        )}
      </div>

      <style jsx>{`
        .sub-account-demo {
          max-width: 600px;
          margin: 0 auto;
          padding: 20px;
          font-family: Arial, sans-serif;
        }

        .status {
          border-radius: 8px;
          margin: 20px 0;
        }

        .status p {
          margin: 5px 0;
        }

        .actions {
          margin: 20px 0;
        }

        .connect-btn,
        .create-btn,
        .sub-account-btn,
        .universal-btn {
          background: #0052ff;
          color: white;
          border: none;
          padding: 12px 24px;
          border-radius: 8px;
          cursor: pointer;
          font-size: 16px;
          margin-right: 15px;
          margin-bottom: 10px;
        }

        .connect-btn:disabled,
        .create-btn:disabled,
        .sub-account-btn:disabled,
        .universal-btn:disabled {
          background: #ccc;
          cursor: not-allowed;
        }

        .connect-btn:hover:not(:disabled),
        .create-btn:hover:not(:disabled),
        .sub-account-btn:hover:not(:disabled),
        .universal-btn:hover:not(:disabled) {
          background: #0041cc;
        }
      `}</style>
    </div>
  );
}

동영상 가이드