> ## 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.

# 타입드 데이터 서명 및 검증

> Base Account를 위한 EIP-712 구조화 데이터 서명 및 검증 방법을 알아보세요.

## 개요

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

## 소개

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

### 지갑 서명 사용 사례

블록체인 기반 앱은 두 가지 주요 카테고리에서 지갑 서명을 사용합니다:

1. **오프체인 검증을 위한 서명**: 스푸핑을 방지하기 위해 온체인 앱에서 사용자를 인증하는 데 사용됩니다 (예: Ethereum으로 로그인). 서명은 온체인 액션에 사용되지 않습니다.

2. **온체인 검증을 위한 서명**: 온체인 퍼미션 서명 (예: [Permit2](https://github.com/Uniswap/permit2)) 또는 트랜잭션 배칭에 사용됩니다. 서명은 일반적으로 향후 트랜잭션을 위해 저장됩니다.

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

스마트 컨트랙트 지갑은 EOA와 다음과 같은 방식으로 서명을 다르게 처리합니다:

* 컨트랙트 자체가 서명을 생성하지 않습니다 — 대신 소유자(예: 패스키)가 메시지에 서명합니다
* 검증은 [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271)에 정의된 `isValidSignature` 함수를 통해 이루어집니다
* 스마트 컨트랙트 지갑 주소는 종종 결정론적이어서 [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492)를 통해 배포 전에 서명 지원이 가능합니다

## 전체 흐름

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

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Browser
    participant AppServer as "App Server"
    participant SDK
    participant Account

    User->>Browser: Trigger signing action
    Browser->>AppServer: GET /typed-data/prepare
    AppServer-->>Browser: EIP-712 payload

    Browser->>SDK: eth_signTypedData_v4
    SDK->>Account: eth_signTypedData_v4(payload)
    User->>Account: Review and approve signature
    Account-->>SDK: signature
    SDK-->>Browser: signature

    Browser-->>AppServer: POST /typed-data/verify {payload, signature}
    AppServer-->>Browser: verification result
```

## 구현

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

### 코드 스니펫

<CodeGroup>
  ```ts Browser (SDK) theme={null}
  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);
  }
  ```

  ```ts Backend (Viem) theme={null}
  import { createPublicClient, http } from 'viem';
  import { base } from 'viem/chains';

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

  export async function verifyTypedData(req, res) {
    const { typedData, signature, address } = req.body;
    
    try {
      // Verify the typed data 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' });
      }

      // Additional validation logic here
      // e.g., check expiry, nonce, permissions, etc.
      const now = Math.floor(Date.now() / 1000);
      if (typedData.message.expiry < now) {
        return res.status(401).json({ error: 'Signature expired' });
      }

      // Process the verified typed data
      res.json({ 
        valid: true, 
        message: 'Signature verified successfully',
        data: typedData.message 
      });
    } catch (error) {
      console.error('Verification error:', error);
      res.status(500).json({ error: 'Verification failed' });
    }
  }
  ```
</CodeGroup>

## Express 서버 예시

```ts title="server/typed-data.ts" theme={null}
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'));
```

## 모범 사례

### 도메인 분리

서로 다른 애플리케이션 간의 서명 재생을 방지하기 위해 항상 고유한 도메인 파라미터를 사용하세요:

```tsx theme={null}
const domain = {
  name: 'Your App Name',           // Unique app identifier
  version: '1',                    // Version your types
  chainId: 8453,                   // Network-specific
  verifyingContract: contractAddr   // Contract that will verify
};
```

### 논스 관리

재생 공격을 방지하기 위해 논스를 포함하세요:

```tsx theme={null}
// 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
```

### 만료 시간

시간 제한 서명을 위해 항상 만료 타임스탬프를 포함하세요:

```tsx theme={null}
const expiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour
```
