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.

개요

Base Account는 내부적으로 스마트 지갑 컨트랙트를 사용합니다. 스마트 컨트랙트 지갑은 전통적인 외부 소유 어카운트(EOA)와 비교하여 메시지 서명 방식에 몇 가지 차이가 있습니다. 이 가이드에서는 Base Account를 사용하여 메시지 서명을 올바르게 구현하는 방법을 설명합니다. 표준 메시지와 타입드 데이터 서명, 그리고 일부 엣지 케이스를 다룹니다.

소개

Base Account를 사용하여 메시지를 서명하고 검증하는 방법을 자세히 살펴보기 전에, 지갑으로 메시지를 서명하는 사용 사례와 서명에 있어 EOA와 스마트 컨트랙트의 주요 차이점을 이해하는 것이 중요합니다.

지갑 서명 사용 사례

블록체인 기반 앱은 두 가지 주요 카테고리에서 지갑 서명을 사용합니다:
  1. 오프체인 검증을 위한 서명: 스푸핑을 방지하기 위해 온체인 앱에서 사용자를 인증하는 데 사용됩니다 (예: Ethereum으로 로그인). 서명은 온체인 액션에 사용되지 않습니다.
  2. 온체인 검증을 위한 서명: 온체인 퍼미션 서명 (예: Permit2) 또는 트랜잭션 배칭에 사용됩니다. 서명은 일반적으로 향후 트랜잭션을 위해 저장됩니다.

스마트 컨트랙트 지갑의 차이점

스마트 컨트랙트 지갑은 EOA와 다음과 같은 방식으로 서명을 다르게 처리합니다:
  • 컨트랙트 자체가 서명을 생성하지 않습니다 — 대신 소유자(예: 패스키)가 메시지에 서명합니다
  • 검증은 EIP-1271에 정의된 isValidSignature 함수를 통해 이루어집니다
  • 스마트 컨트랙트 지갑 주소는 종종 결정론적이어서 ERC-6492를 통해 배포 전에 서명 지원이 가능합니다

전체 흐름

이 가이드에서는 Base Account를 사용하여 메시지를 서명하고 검증하는 전체 흐름을 살펴봅니다.

구현

이 가이드에서는 사용자의 자금을 사용할 퍼미션이 포함된 간단한 타입드 데이터 페이로드를 예시로 사용합니다 (스펜드 퍼미션 참고).

코드 스니펫

import { createBaseAccountSDK } from "@base-org/account";

// Initialize the SDK
const provider = createBaseAccountSDK().getProvider();

// 1 — Prepare the typed data payload
const typedData = {
  domain: {
    name: 'Spend Permission Manager',
    version: '1',
    chainId: 8453, // or any other supported chain
    verifyingContract: SPEND_PERMISSION_MANAGER_ADDRESS,
  },
  types: {
    SpendPermission: [
      { name: 'account', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'token', type: 'address' },
      { name: 'allowance', type: 'uint160' },
      { name: 'period', type: 'uint48' },
      { name: 'start', type: 'uint48' },
      { name: 'end', type: 'uint48' },
      { name: 'salt', type: 'uint256' },
      { name: 'extraData', type: 'bytes' },
    ],
  },
  primaryType: 'SpendPermission',
  message: spendPermissionData,
};

// 2 — Request signature from user
try {
  const accounts = await provider.request({
    method: 'eth_requestAccounts'
  });
  
  const signature = await provider.request({
    method: 'eth_signTypedData_v4',
    params: [accounts[0], JSON.stringify(typedData)]
  });

  // 3 — Send to backend for verification
  const response = await fetch('/typed-data/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ 
      typedData, 
      signature, 
      address: accounts[0] 
    })
  });
  
  const result = await response.json();
  console.log('Verification result:', result);
} catch (err) {
  console.error('Signing failed:', err);
}

Express 서버 예시

server/typed-data.ts
import express from 'express';
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';

const app = express();
app.use(express.json());

const client = createPublicClient({ 
  chain: base, 
  transport: http() 
});

// Simple nonce store (use Redis/DB in production)
const usedNonces = new Set<string>();

app.get('/typed-data/prepare', (req, res) => {
  const { userAddress, action, resource } = req.query;
  
  const nonce = Math.floor(Math.random() * 1000000);
  const expiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour
  
  const typedData = {
    // YOUR TYPED DATA HERE
  }
  
  res.json(typedData);
});

app.post('/typed-data/verify', async (req, res) => {
  const { typedData, signature, address } = req.body;
  
  try {
    // 1. Check nonce hasn't been reused
    const nonceKey = `${address}-${typedData.message.nonce}`;
    if (usedNonces.has(nonceKey)) {
      return res.status(400).json({ error: 'Nonce already used' });
    }
    
    // 2. Check expiry
    const now = Math.floor(Date.now() / 1000);
    if (typedData.message.expiry < now) {
      return res.status(400).json({ error: 'Signature expired' });
    }
    
    // 3. Verify signature
    const valid = await client.verifyTypedData({
      address,
      domain: typedData.domain,
      types: typedData.types,
      primaryType: typedData.primaryType,
      message: typedData.message,
      signature
    });
    
    if (!valid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // 4. Mark nonce as used
    usedNonces.add(nonceKey);
    
    // 5. Process the verified action
    res.json({ 
      valid: true,
      message: 'Typed data verified successfully',
      action: typedData.message.action,
      resource: typedData.message.resource
    });
  } catch (error) {
    console.error('Verification error:', error);
    res.status(500).json({ error: 'Verification failed' });
  }
});

app.listen(3001, () => console.log('Typed data server listening on :3001'));

모범 사례

도메인 분리

서로 다른 애플리케이션 간의 서명 재생을 방지하기 위해 항상 고유한 도메인 파라미터를 사용하세요:
const domain = {
  name: 'Your App Name',           // Unique app identifier
  version: '1',                    // Version your types
  chainId: 8453,                   // Network-specific
  verifyingContract: contractAddr   // Contract that will verify
};

논스 관리

재생 공격을 방지하기 위해 논스를 포함하세요:
// Generate unique nonces
const nonce = crypto.randomBytes(16).toString('hex');

// Store and validate nonces server-side
const usedNonces = new Set(); // Use Redis/DB in production

만료 시간

시간 제한 서명을 위해 항상 만료 타임스탬프를 포함하세요:
const expiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour