GraphQL

GraphQL은 간략하게 설명하면, REST fetch와 다르게 API에 필요한 데이터만을 질의해 가져올 수 있는 쿼리 언어 & 런타임이다.

The Graph는 GraphQL로 온체인 데이터를 질의할 수 있게 해주는 탈중앙 프로토콜이다.

이 글은 전반적으로 https://thegraph.com/docs/en/developing/creating-a-subgraph 를 요약한 것이다. 원문과 충돌하는 부분이 있을 경우 원문이 우선한다.

Subgraph

https://thegraph.com/docs/en/developing/creating-a-subgraph

블록체인에서 데이터를 뽑아 인덱싱하기 위해서는 subgraph를 정의해야 한다. 다루려면 Graph CLI를 설치할 필요가 있다.

yarn global add @graphprotocol/graph-cli

Subgraph는 세 가지 파일들로 구성된다.

  • subgraph.yaml: 매니페스트
  • schema.graphql: GraphQL 스키마. 어떤 데이터가 저장되고 어떻게 쿼리될지 정의한다.
  • AssemblyScript Mappings: AssemblyScript(제한된 Typescript) 코드. 이벤트 데이터를 엔티티로 변환하는 법을 정의한다.

DataSource

=contract. Subgraph가 이벤트/call/block에 대한 데이터를 받아오게 될 대상이다. 예를 들어, Uniswap V2 Pool 컨트랙트 각각이 dataSource가 되어 swap 등의 이벤트를 받아오게 된다.

Manifest

Subgraph의 속성들을 서술하는 파일이다. https://thegraph.com/docs/en/developing/creating-a-subgraph

ABI

Data source로 지정된 컨트랙트의 ABI는 Graph CLI가 Etherscan을 조회해 자동으로 다운받는 것이 기본값이며 --abi <FILE>로 지정할 수도 있다.

GraphQL IDL Specs

https://thegraph.com/docs/en/querying/graphql-api 참조

https://graphql.org/learn/schema 도 또한 참고할 만 하다.

Entities

schema.graphql에서 정의한다. @entity directive를 붙여서 정의하면 되며, 가변성이 필요한 게 아니라면 @entity(immutable: true)로 불변 엔티티로서 정의하는 것을 추천한다. 불변 엔티티가 더 저장하고 쿼리하기 빠르기 때문이다.

이벤트나 함수 호출을 엔티티로 1:1 대응하는 것보다는 공통된 속성을 모아서 엔티티로 정의하는 것이 권장된다.

각 엔티티는 id 필드를 항상 가져야 하며 Bytes! 혹은 String! (human-readable text가 아닌 한 Bytes! 권장)으로 정의해야 한다(!은 required field를 의미). id 필드에는 다음과 같은 값을 설정해볼 수 있다. (참고: String임)

  • event.params.id.toHex()
  • event.transaction.from.toHex()
  • event.transaction.hash.toHex() + "-" + event.logIndex.toString()

엔티티의 스칼라 타입으로는 Bytes, String, Boolean, Int (참고: 32비트), BigInt, BigDecimal 을 지원한다.

Relationships

한 엔티티의 필드에서 다른 엔티티를 참조함으로서 관계를 표현할 수 있다. 참조는 기본적으로 단방향이나 서로가 참조함으로서 양방향 관계도 표현 가능하다.

One-to-One

단순히 서로의 필드를 참조하기만 하면 된다.

type Transaction @entity(immutable: true) {
  id: Bytes!
  transactionReceipt: TransactionReceipt
}

type TransactionReceipt @entity(immutable: true) {
  id: Bytes!
  transaction: Transaction
}

One-to-Many

단순히 array를 통해 one-to-many를 표현하는 것은 성능 패널티가 있다. ‘Many’ 쪽에서 @derivedFrom directive을 사용해서 표현하면 성능이 훨씬 나아진다.

type Token @entity(immutable: true) {
  id: Bytes!
  tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")
}

type TokenBalance @entity {
  id: Bytes!
  amount: Int!
  token: Token!
}

Many-to-Many

두 가지 방법이 있다. 첫 번째 방법은 array를 통해 한쪽 방향 관계를 표현하고 반대방향은 @derivedFrom 을 이용하는 것이다.

type Organization @entity {
  id: Bytes!
  name: String!
  members: [User!]!
}

type User @entity {
  id: Bytes!
  name: String!
  organizations: [Organization!]! @derivedFrom(field: "members")
}

성능적으로 더 나은 방법은 인접 리스트와 같이 연결 자체를 엔티티로서 표현하는 것이다. 이로서 양쪽 모두 @derivedFrom 을 사용할 수 있게 된다.

type Organization @entity {
  id: Bytes!
  name: String!
  members: [UserOrganization!]! @derivedFrom(field: "organization")
}

type User @entity {
  id: Bytes!
  name: String!
  organizations: [UserOrganization!] @derivedFrom(field: "user")
}

type UserOrganization @entity {
  id: Bytes! # Set to `user.id.concat(organization.id)`
  user: User!
  organization: Organization!
}

대신에 쿼리문이 인접 리스트 기반으로 이루어지기 때문에 한 단계 더 들어가게 된다는 점을 인지해야 한다.

query usersWithOrganizations {
  users {
    organizations {
      # this is a UserOrganization entity
      organization {
        name
      }
    }
  }
}

Comments

GraphQL 스펙에 따라, 문자열을 필드 위에 두어서 주석을 표현할 수 있다.

type MyFirstEntity @entity {
  "unique identifier and primary key of the entity"
  id: Bytes!
  address: Bytes!
}

Data Source Templates

Uniswap pool과 같이, 이더리움에서는 registry/factory contract가 있어 이들이 임의 개수의 새로운 contract를 ‘찍어내는’ 패턴이 흔한 편이다. 임의 개수에서 알 수 있듯이 이러한 경우에는 어떤 컨트랙트를 추적할 지 미리 파악이 불가능하다는 문제가 있다. 이런 경우에 data source template를 사용하면 된다.

Main contract에 대해 (일반적인) data source를 정의하고, 거기서 동적으로 생성되는 컨트랙트들에 대해 data source template를 정의하는 방식이며, 자세한 코드는 https://thegraph.com/docs/en/developing/creating-a-subgraph 를 참고하면 좋다.

Call Handlers

가스비를 아끼기 위해 이벤트 로그를 노출하지 않는 컨트랙트의 경우, 해당 컨트랙트의 함수 호출을 추적하여 이벤트처럼 취급할 수 있다. 문제는 Parity tracing API에 의존하기 때문에 일부 체인에서는 동작하지 않는다는 것이다. 자세한 내용은 https://thegraph.com/docs/en/developing/creating-a-subgraph/#call-handlers 를 참고하면 좋다.

Block Handlers

매 블럭 생성마다 컨트랙트의 변경사항을 추적할 수 있는 핸들러이며, 필터를 걸어서 특정 조건에 따라서만 실행할 수도 있다. (현재 유일하게 존재하는 call 필터는 해당 컨트랙트에 대한 호출이 블럭 내에 있는 경우에만으로 한정 가능하나, 이 역시 Parity tracing API에 의존하는 문제가 있음)

자세한 내용은 https://thegraph.com/docs/en/developing/creating-a-subgraph/#block-handlers 을 참고하면 좋다.

Tx Receipts in Event Handlers

매니페스트에서 이벤트 핸들러 속성 receipt: true 를 줌으로서 Event.receipt 필드에 대한 접근이 활성화된다. 이 경우 해당 트랜잭션의 receipt에 접근 가능하다. (가스비 등?)

Grafting

모든 data source가 제네시스(혹은 startBlock)부터 인덱싱하면 리소스 낭비가 심하기 때문에, 기존 data source에서 데이터를 재활용할 수 있는데 이를 grafting이라 한다. 여러 제약사항이 있기 때문에 개발 용도로나 긴급상황일 때만 하는 것을 추천한다.