# Verified Address

Verified Address는 특정 지갑 주소가 신뢰할 수 있는 발행자로부터 고객확인을 받았다는 사실을 나타내는 데이터에요. 이를 통해 웹3 금융 서비스를 더욱 더 안전하게 이용할 수 있습니다.

이 문서에서는 여러분이 온체인 앱을 만들 때 참고할 수 있도록 verified address를 조회하고 활용하는 방법을 소개합니다.

## Verified Address 조회하기

### 요구사항

아래 항목들이 설치되어 있어야해요.

* [node](https://nodejs.org/ko)
* [pnpm](https://pnpm.io/)

### 개발 환경 세팅

이 문서에서는 viem을 사용해요. [Viem](https://viem.sh/)은 Node.js 라이브러리이기 때문에 Node.js 프로젝트로 생성합니다.

{% stepper %}
{% step %}

#### 프로젝트 폴더 생성

```bash
mkdir giwa-verified-address-tutorial
cd giwa-verified-address-tutorial
```

{% endstep %}

{% step %}

#### 프로젝트 초기화

```bash
pnpm init
```

{% endstep %}

{% step %}

#### Dependencies 설치

```bash
pnpm add -D tsx @types/node
pnpm add viem@^2.38.0
```

{% endstep %}
{% endstepper %}

### Chain Client 설정

Verified Address 조회를 위해 chain client를 설정합니다.

{% code title="src/config.ts" lineNumbers="true" %}

```typescript
import {createPublicClient, defineChain, http} from "viem";
import {sepolia, giwaSepolia} from "viem/chains";

// GIWA 세폴리아 체인 데이터를 읽기 위한 client
export const publicClient = createPublicClient({
  chain: giwaSepolia,
  transport: http(),
});

```

{% endcode %}

### 컨트랙트 주소 및 ABI 설정

Verified Address는 [DojangScroll](https://docs.giwa.io/giwa-ecosystem/contracts#contracts) 컨트랙트와 [EAS](https://docs.giwa.io/giwa-ecosystem/contracts#contracts) 컨트랙트를 통해 조회해요. 이를 위해 사전에 컨트랙트 주소와 ABI를 정의합니다.

{% code title="src/contract.ts" lineNumbers="true" %}

```typescript
import {parseAbi} from "viem";

// Verified Addres 조회를 위해 필요한 컨트랙트 주소 정의
export const dojangScrollAddress = '0xd5077b67dcb56caC8b270C7788FC3E6ee03F17B9';
export const easAddress = '0x4200000000000000000000000000000000000021';

// Dojang 발행자 ID 정의
export const dojangAttesterIds = {
  // keccak256("dojang.dojangattesterids.upbitkorea")
  UPBIT_KOREA:
    '0xd99b42e778498aa3c9c1f6a012359130252780511687a35982e8e52735453034' as const
}

// DojangScroll ABI 정의
export const dojangScrollAbi = parseAbi([
  'function isVerified(address, bytes32) external view returns (bool)',
  'function getVerifiedAddressAttestationUid(address, bytes32) external view returns (bytes32)'
]);

// EAS ABI 정의
// 참고: https://github.com/ethereum-attestation-service/eas-contracts/blob/master/deployments/optimism-sepolia/EAS.json
export const easAbi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AccessDenied","type":"error"},{"inputs":[],"name":"AlreadyRevoked","type":"error"},{"inputs":[],"name":"AlreadyRevokedOffchain","type":"error"},{"inputs":[],"name":"AlreadyTimestamped","type":"error"},{"inputs":[],"name":"InsufficientValue","type":"error"},{"inputs":[],"name":"InvalidAttestation","type":"error"},{"inputs":[],"name":"InvalidAttestations","type":"error"},{"inputs":[],"name":"InvalidExpirationTime","type":"error"},{"inputs":[],"name":"InvalidLength","type":"error"},{"inputs":[],"name":"InvalidOffset","type":"error"},{"inputs":[],"name":"InvalidRegistry","type":"error"},{"inputs":[],"name":"InvalidRevocation","type":"error"},{"inputs":[],"name":"InvalidRevocations","type":"error"},{"inputs":[],"name":"InvalidSchema","type":"error"},{"inputs":[],"name":"InvalidSignature","type":"error"},{"inputs":[],"name":"InvalidVerifier","type":"error"},{"inputs":[],"name":"Irrevocable","type":"error"},{"inputs":[],"name":"NotFound","type":"error"},{"inputs":[],"name":"NotPayable","type":"error"},{"inputs":[],"name":"WrongSchema","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":true,"internalType":"address","name":"attester","type":"address"},{"indexed":false,"internalType":"bytes32","name":"uid","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"schema","type":"bytes32"}],"name":"Attested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":true,"internalType":"address","name":"attester","type":"address"},{"indexed":false,"internalType":"bytes32","name":"uid","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"schema","type":"bytes32"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"revoker","type":"address"},{"indexed":true,"internalType":"bytes32","name":"data","type":"bytes32"},{"indexed":true,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"RevokedOffchain","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"data","type":"bytes32"},{"indexed":true,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"Timestamped","type":"event"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint64","name":"expirationTime","type":"uint64"},{"internalType":"bool","name":"revocable","type":"bool"},{"internalType":"bytes32","name":"refUID","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structAttestationRequestData","name":"data","type":"tuple"}],"internalType":"structAttestationRequest","name":"request","type":"tuple"}],"name":"attest","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint64","name":"expirationTime","type":"uint64"},{"internalType":"bool","name":"revocable","type":"bool"},{"internalType":"bytes32","name":"refUID","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structAttestationRequestData","name":"data","type":"tuple"},{"components":[{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"structEIP712Signature","name":"signature","type":"tuple"},{"internalType":"address","name":"attester","type":"address"}],"internalType":"structDelegatedAttestationRequest","name":"delegatedRequest","type":"tuple"}],"name":"attestByDelegation","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"getAttestTypeHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"uid","type":"bytes32"}],"name":"getAttestation","outputs":[{"components":[{"internalType":"bytes32","name":"uid","type":"bytes32"},{"internalType":"bytes32","name":"schema","type":"bytes32"},{"internalType":"uint64","name":"time","type":"uint64"},{"internalType":"uint64","name":"expirationTime","type":"uint64"},{"internalType":"uint64","name":"revocationTime","type":"uint64"},{"internalType":"bytes32","name":"refUID","type":"bytes32"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"address","name":"attester","type":"address"},{"internalType":"bool","name":"revocable","type":"bool"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"structAttestation","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getDomainSeparator","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getName","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"revoker","type":"address"},{"internalType":"bytes32","name":"data","type":"bytes32"}],"name":"getRevokeOffchain","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getRevokeTypeHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getSchemaRegistry","outputs":[{"internalType":"contractISchemaRegistry","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"data","type":"bytes32"}],"name":"getTimestamp","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"uid","type":"bytes32"}],"name":"isAttestationValid","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint64","name":"expirationTime","type":"uint64"},{"internalType":"bool","name":"revocable","type":"bool"},{"internalType":"bytes32","name":"refUID","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structAttestationRequestData[]","name":"data","type":"tuple[]"}],"internalType":"structMultiAttestationRequest[]","name":"multiRequests","type":"tuple[]"}],"name":"multiAttest","outputs":[{"internalType":"bytes32[]","name":"","type":"bytes32[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint64","name":"expirationTime","type":"uint64"},{"internalType":"bool","name":"revocable","type":"bool"},{"internalType":"bytes32","name":"refUID","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structAttestationRequestData[]","name":"data","type":"tuple[]"},{"components":[{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"structEIP712Signature[]","name":"signatures","type":"tuple[]"},{"internalType":"address","name":"attester","type":"address"}],"internalType":"structMultiDelegatedAttestationRequest[]","name":"multiDelegatedRequests","type":"tuple[]"}],"name":"multiAttestByDelegation","outputs":[{"internalType":"bytes32[]","name":"","type":"bytes32[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"bytes32","name":"uid","type":"bytes32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structRevocationRequestData[]","name":"data","type":"tuple[]"}],"internalType":"structMultiRevocationRequest[]","name":"multiRequests","type":"tuple[]"}],"name":"multiRevoke","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"bytes32","name":"uid","type":"bytes32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structRevocationRequestData[]","name":"data","type":"tuple[]"},{"components":[{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"structEIP712Signature[]","name":"signatures","type":"tuple[]"},{"internalType":"address","name":"revoker","type":"address"}],"internalType":"structMultiDelegatedRevocationRequest[]","name":"multiDelegatedRequests","type":"tuple[]"}],"name":"multiRevokeByDelegation","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"data","type":"bytes32[]"}],"name":"multiRevokeOffchain","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"data","type":"bytes32[]"}],"name":"multiTimestamp","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"bytes32","name":"uid","type":"bytes32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structRevocationRequestData","name":"data","type":"tuple"}],"internalType":"structRevocationRequest","name":"request","type":"tuple"}],"name":"revoke","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32","name":"schema","type":"bytes32"},{"components":[{"internalType":"bytes32","name":"uid","type":"bytes32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"structRevocationRequestData","name":"data","type":"tuple"},{"components":[{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"structEIP712Signature","name":"signature","type":"tuple"},{"internalType":"address","name":"revoker","type":"address"}],"internalType":"structDelegatedRevocationRequest","name":"delegatedRequest","type":"tuple"}],"name":"revokeByDelegation","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"data","type":"bytes32"}],"name":"revokeOffchain","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"data","type":"bytes32"}],"name":"timestamp","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}];

```

{% endcode %}

### Verified Address 조회하기

이제 Verified Address를 조회해볼까요?

Verified Address 여부는 Dojang 서비스의 편의성 컨트랙트인 [DojangScroll](https://docs.giwa.io/giwa-ecosystem/contracts#contracts) 컨트랙트 함수를 통해 바로 조회할 수 있어요.

추가로 만료시점 등의 메타데이터가 필요한 경우, [EAS](https://docs.giwa.io/giwa-ecosystem/contracts#contracts) 컨트랙트를 통해 직접 **Attestation** 데이터를 조회해야해요.

{% stepper %}
{% step %}

#### 코드 작성하기

{% code title="src/index.ts" lineNumbers="true" %}

```typescript
import {Address} from "viem";
import {publicClient} from "./config";
import {dojangAttesterIds, dojangScrollAddress, easAddress, dojangScrollAbi, easAbi} from "./contract";

async function isVerified(address: Address) {
  // DojangScroll 컨트랙트로 verified 여부를 조회해요.
  return await publicClient.readContract({
    address: dojangScrollAddress,
    abi: dojangScrollAbi,
    functionName: 'isVerified',
    // 두번째 인자는 발행자 ID 에요.
    args: [address, dojangAttesterIds.UPBIT_KOREA],
  });
}

async function getVerifiedAddressAttestation(address: Address) {
  // DojangScroll 컨트랙트로 attestation uid를 조회해요.
  const attestationUid = await publicClient.readContract({
    address: dojangScrollAddress,
    abi: dojangScrollAbi,
    functionName: 'getVerifiedAddressAttestationUid',
    args: [address, dojangAttesterIds.UPBIT_KOREA],
  });

  // EAS 컨트랙트로 attestation 데이터를 조회해요.
  return await publicClient.readContract({
    address: easAddress,
    abi: easAbi,
    functionName: 'getAttestation',
    args: [attestationUid],
  })
}

async function main() {
  // ⚠️ 조회할 지갑 주소를 입력하세요.
  const addressToCheck = "0x...";

  const verified = await isVerified(addressToCheck);
  console.log(`${addressToCheck} is ${verified ? "Verified" : "Not verified"}\n`);

  if (verified) {
    const attestation = await getVerifiedAddressAttestation(addressToCheck);
    console.log(attestation);
  }
}

main().then();

```

{% endcode %}
{% endstep %}

{% step %}

#### 실행하기

```bash
node --import=tsx src/index.ts
```

{% endstep %}
{% endstepper %}

## Verified Address 활용하기

아래 예시처럼 특정 컨트랙트 함수를 verified address인 지갑들만 실행할 수 있게 제약할 수 있어요.

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IDojangScroll {
    function isVerified(address addr, bytes32 attesterId) external view returns (bool);
}

// 아래 abstract 컨트랙트에 포함된 modifier를 사용해 접근통제가 가능해요.
abstract contract VerifiedAddressAccessControl {
    address internal constant DOJANG_SCROLL = 0xd5077b67dcb56caC8b270C7788FC3E6ee03F17B9;

    // keccak256("dojang.dojangattesterids.upbitkorea")
    bytes32 internal constant ATTESTER_ID = 0xd99b42e778498aa3c9c1f6a012359130252780511687a35982e8e52735453034;

    error NotVerified();

    // 위 attester로부터 verified address를 발급받았는지 확인하는 modifier
    modifier onlyVerified() {
        if (!IDojangScroll(DOJANG_SCROLL).isVerified(msg.sender, ATTESTER_ID)) {
            revert NotVerified();
        }
        _;
    }
}

// 위에서 작성한 abstract 컨트랙트를 상속받아요.
contract VerifiedGiwa is VerifiedAddressAccessControl {
    // ℹ️ 아래 함수는 verified address인 지갑들만 실행할 수 있어요.
    function verifiedAddressFunction() public onlyVerified {
        // ⚠️ 함수를 구현하세요.
    }
}

```

{% endcode %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.giwa.io/giwa-ecosystem/dojang/verified-address.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
