└── README.md /README.md: -------------------------------------------------------------------------------- 1 | ## Building an NFT API on NEAR with The Graph 2 | 3 | In this workshopp we'll build a subgraph for querying NTF data from the [Misfits](https://explorer.near.org/accounts/misfits.tenk.near) smart contract, implementing queries for fetching NFTs as well as their owners, and building relationships between them. 4 | 5 | You can view the example codebase we will be building [here](https://github.com/dabit3/near-nft-subgraph). 6 | 7 | ### Prerequisites 8 | 9 | To be successful in this tutorial, you should have [Node.js](https://nodejs.org/en/) installed on your machine. These days, I recommend using either [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm/blob/master/docs/commands.md) to manage Node.js versions. 10 | 11 | ### Creating the Graph project in The Graph dashboard 12 | 13 | To get started, open The Graph [dashboard](https://thegraph.com/hosted-service/dashboard) and either sign in or create a new account. 14 | 15 | Next, click on __Add Subgraph__ to create a new subgraph. 16 | 17 | Configure your subgraph with the following properties: 18 | 19 | - Subgraph Name - __Nearmisfits__ 20 | - Subtitle - __A subgraph for querying NFTs__ 21 | - Optional - Fill the description and GITHUB URL properties 22 | 23 | Once the subgraph is created, we will initialize the subgraph locally using the Graph CLI. 24 | 25 | ### Initializing a new subgraph using the Graph CLI 26 | 27 | Next, install the Graph CLI: 28 | 29 | ```sh 30 | $ npm install -g @graphprotocol/graph-cli 31 | 32 | # or 33 | 34 | $ yarn global add @graphprotocol/graph-cli 35 | ``` 36 | 37 | Once the Graph CLI has been installed you can initialize a new subgraph with the Graph CLI `init` command. 38 | 39 | There are two ways to initialize a new subgraph: 40 | 41 | 1 - From an example subgraph 42 | 43 | ```sh 44 | $ graph init --from-example / [] 45 | ``` 46 | 47 | 2 - From an existing smart contract 48 | 49 | If you already have a smart contract deployed to Ethereum mainnet or one of the testnets, initializing a new subgraph from this contract is an easy way to get up and running. 50 | 51 | ```sh 52 | $ graph init --from-contract \ 53 | [--network ] \ 54 | [--abi ] \ 55 | / [] 56 | ``` 57 | 58 | In our case we'll be starting with the [Misfits NFT contract](https://explorer.near.org/accounts/misfits.tenk.near) so we can initialize from that contract address by passing in the contract address using the `--from-contract` flag: 59 | 60 | ```sh 61 | $ graph init --from-contract misfits.tenk.near --contract-name Token 62 | 63 | ? Protocol › near 64 | ? Subgraph name › your-username/nearmisfits 65 | ? Directory to create the subgraph in › nearmisfits 66 | ? NEAR network › near-mainnet 67 | ? Contract account › misfits.tenk.near 68 | ? Contract Name · Token 69 | ``` 70 | 71 | This command will generate a basic subgraph based off of the contract address passed in as the argument to `--from-contract`. By using this contract address, the CLI will initialize a few things in your project to get you started. 72 | 73 | The main configuration and definition for the subgraph lives in the __subgraph.yaml__ file. The subgraph codebase consists of a few files: 74 | 75 | - __subgraph.yaml__: a YAML file containing the subgraph manifest 76 | - __schema.graphql__: a GraphQL schema that defines what data is stored for your subgraph, and how to query it via GraphQL 77 | - __AssemblyScript Mappings__: AssemblyScript code that translates from the event data in Ethereum to the entities defined in your schema (e.g. mapping.ts in this tutorial) 78 | 79 | The entries in __subgraph.yaml__ that we will be working with are: 80 | 81 | - `description` (optional): a human-readable description of what the subgraph is. This description is displayed by the Graph Explorer when the subgraph is deployed to the Hosted Service. 82 | - `dataSources.source`: the address of the account that the subgraph sources. 83 | - `dataSources.source.startBlock` (optional): the number of the block that the data source starts indexing from. In most cases we suggest using the block in which the contract was created. 84 | - `dataSources.mapping.entities` : the entities that the data source writes to the store. The schema for each entity is defined in the the schema.graphql file. 85 | - `dataSources.mapping.receiptHandlers`: lists the receipts this subgraph reacts to and the handlers in the mapping — __./src/mapping.ts__ in the example — that transform these events into entities in the store. 86 | 87 | ### Defining the entities 88 | 89 | With The Graph, you define entity types in __schema.graphql__, and Graph Node will generate top level fields for querying single instances and collections of that entity type. Each type that should be an entity is required to be annotated with an `@entity` directive. 90 | 91 | The entities / data we will be indexing are the `Token` and `User`. This way we can index the Tokens created by the users as well as the users themselves. We'll also enable full text search for searching by account name as well as the `kind` of Misfit NFT. 92 | 93 | To do this, update __schema.graphql__ with the following code: 94 | 95 | ```graphql 96 | type _Schema_ 97 | @fulltext( 98 | name: "tokenSearch" 99 | language: en 100 | algorithm: rank 101 | include: [{ entity: "Token", fields: [{ name: "ownerId" }, { name: "kind" }] }] 102 | ) 103 | 104 | type Token @entity { 105 | id: ID! 106 | owner: User! 107 | ownerId: String! 108 | tokenId: String! 109 | image: String! 110 | metadata: String! 111 | kind: String! 112 | seed: Int! 113 | } 114 | 115 | type User @entity { 116 | id: ID! 117 | tokens: [Token!]! @derivedFrom(field: "owner") 118 | } 119 | ``` 120 | 121 | ### On Relationships via `@derivedFrom` (from the docs): 122 | 123 | Reverse lookups can be defined on an entity through the `@derivedFrom` field. This creates a virtual field on the entity that may be queried but cannot be set manually through the mappings API. Rather, it is derived from the relationship defined on the other entity. For such relationships, it rarely makes sense to store both sides of the relationship, and both indexing and query performance will be better when only one side is stored and the other is derived. 124 | 125 | For one-to-many relationships, the relationship should always be stored on the 'one' side, and the 'many' side should always be derived. Storing the relationship this way, rather than storing an array of entities on the 'many' side, will result in dramatically better performance for both indexing and querying the subgraph. In general, storing arrays of entities should be avoided as much as is practical. 126 | 127 | Now that we have created the GraphQL schema for our app, we can generate the entities locally to start using in the `mappings` created by the CLI: 128 | 129 | ```sh 130 | graph codegen 131 | ``` 132 | 133 | In order to make working smart contracts, events and entities easy and type-safe, the Graph CLI generates AssemblyScript types from a combination of the subgraph's GraphQL schema and the contract ABIs included in the data sources. 134 | 135 | ## Updating the subgraph with the entities and mappings 136 | 137 | Now we can configure the __subgraph.yaml__ to use the entities that we have just created and configure their mappings. 138 | 139 | To do so, first update the `dataSources.mapping.entities` field with the `User` and `Token` entities: 140 | 141 | ```yaml 142 | entities: 143 | - Token 144 | - User 145 | ``` 146 | 147 | Next, update the configuration to add the startBlock and change the contract `address` to the [main proxy contract](https://etherscan.io/address/0x3B3ee1931Dc30C1957379FAc9aba94D1C48a5405) address: 148 | 149 | ```yaml 150 | source: 151 | account: "misfits.tenk.near" 152 | startBlock: 53472065 153 | ``` 154 | 155 | Next, we'll want to enable a couple of features like __Full Text Search__ as well as the __IPFS API__. 156 | 157 | To do this, add the following feature flags as top level configurations (after the `schema` declaration): 158 | 159 | ```yaml 160 | features: 161 | - ipfsOnEthereumContracts 162 | - fullTextSearch 163 | ``` 164 | 165 | Finally, update the `specVersion` to be 0.0.4: 166 | 167 | ```yaml 168 | specVersion: 0.0.4 169 | ``` 170 | 171 | The final __subgraph.yaml__ should look like this: 172 | 173 | ```yaml 174 | specVersion: 0.0.4 175 | schema: 176 | file: ./schema.graphql 177 | features: 178 | - ipfsOnEthereumContracts 179 | - fullTextSearch 180 | dataSources: 181 | - kind: near 182 | name: Token 183 | network: near-mainnet 184 | source: 185 | account: "misfits.tenk.near" 186 | startBlock: 53472065 187 | mapping: 188 | apiVersion: 0.0.5 189 | language: wasm/assemblyscript 190 | entities: 191 | - Token 192 | - User 193 | receiptHandlers: 194 | - handler: handleReceipt 195 | file: ./src/mapping.ts 196 | 197 | ``` 198 | 199 | ## Assemblyscript mappings 200 | 201 | Next, open __src/mappings.ts__ to write the mappings that we defined in our subgraph subgraph `eventHandlers`. 202 | 203 | Update the file with the following code: 204 | 205 | ```typescript 206 | import { near, JSONValue, json, ipfs, log } from "@graphprotocol/graph-ts" 207 | import { Token, User } from "../generated/schema" 208 | 209 | export function handleReceipt( 210 | receipt: near.ReceiptWithOutcome 211 | ): void { 212 | const actions = receipt.receipt.actions; 213 | for (let i = 0; i < actions.length; i++) { 214 | handleAction(actions[i], receipt) 215 | } 216 | } 217 | 218 | function handleAction( 219 | action: near.ActionValue, 220 | receiptWithOutcome: near.ReceiptWithOutcome 221 | ): void { 222 | if (action.kind != near.ActionKind.FUNCTION_CALL) { 223 | return; 224 | } 225 | const outcome = receiptWithOutcome.outcome; 226 | const functionCall = action.toFunctionCall(); 227 | const ipfsHash = 'bafybeiew2l6admor2lx6vnfdaevuuenzgeyrpfle56yrgse4u6nnkwrfeu' 228 | const methodName = functionCall.methodName 229 | 230 | if (methodName == 'buy' || methodName == 'nft_mint_one') { 231 | for (let logIndex = 0; logIndex < outcome.logs.length; logIndex++) { 232 | const outcomeLog = outcome.logs[logIndex].toString(); 233 | 234 | log.info('outcomeLog {}', [outcomeLog]) 235 | 236 | const parsed = outcomeLog.replace('EVENT_JSON:', '') 237 | 238 | const jsonData = json.try_fromString(parsed) 239 | const jsonObject = jsonData.value.toObject() 240 | 241 | const eventData = jsonObject.get('data') 242 | if (eventData) { 243 | const eventArray:JSONValue[] = eventData.toArray() 244 | 245 | const data = eventArray[0].toObject() 246 | const tokenIds = data.get('token_ids') 247 | const owner_id = data.get('owner_id') 248 | if (!tokenIds || !owner_id) return 249 | 250 | const ids:JSONValue[] = tokenIds.toArray() 251 | const tokenId = ids[0].toString() 252 | 253 | let token = Token.load(tokenId) 254 | 255 | if (!token) { 256 | token = new Token(tokenId) 257 | token.tokenId = tokenId 258 | 259 | token.image = ipfsHash + '/' + tokenId + '.png' 260 | const metadata = ipfsHash + '/' + tokenId + '.json' 261 | token.metadata = metadata 262 | 263 | const metadataResult = ipfs.cat(metadata) 264 | if (metadataResult) { 265 | const value = json.fromBytes(metadataResult).toObject() 266 | if (value) { 267 | const kind = value.get('kind') 268 | if (kind) { 269 | token.kind = kind.toString() 270 | } 271 | const seed = value.get('seed') 272 | if (seed) { 273 | token.seed = seed.toI64() as i32 274 | } 275 | } 276 | } 277 | } 278 | 279 | token.ownerId = owner_id.toString() 280 | token.owner = owner_id.toString() 281 | 282 | let user = User.load(owner_id.toString()) 283 | if (!user) { 284 | user = new User(owner_id.toString()) 285 | } 286 | 287 | token.save() 288 | user.save() 289 | } 290 | } 291 | } 292 | } 293 | 294 | ``` 295 | 296 | These mappings will handle events for when a new token is minted or bought. When these actions happen, the mappings will save the data into the subgraph. 297 | 298 | ### Running a build 299 | 300 | Next, let's run a build to make sure that everything is configured properly. To do so, run the `build` command: 301 | 302 | ```sh 303 | $ graph build 304 | ``` 305 | 306 | If the build is successful, you should see a new __build__ folder generated in your root directory. 307 | 308 | ## Deploying the subgraph 309 | 310 | To deploy, we can run the `deploy` command using the Graph CLI. To deploy, you will first need to copy the __Access token__ for your account, available in [The Graph dashboard](https://thegraph.com/hosted-service/dashboard): 311 | 312 | ![Graph Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/820lwqh8yo3iyu7fsbhj.jpg) 313 | 314 | Next, run the following command: 315 | 316 | ```sh 317 | $ graph auth https://api.thegraph.com/deploy/ 318 | 319 | $ yarn deploy 320 | ``` 321 | 322 | Once the subgraph is deployed, you should see it show up in your dashboard: 323 | 324 | ![Graph Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/elmlu7jf6wdrz07nhu3x.png) 325 | 326 | When you click on the subgraph, it should open the Graph explorer: 327 | 328 | ![The Near Subgraph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5jr3dk9yc0b3nvs2w93.png) 329 | 330 | ## Querying for data 331 | 332 | Now that we are in the dashboard, we should be able to start querying for data. Run the following query to get a list of tokens and their metadata: 333 | 334 | ```graphql 335 | { 336 | tokens { 337 | id 338 | ownerId 339 | tokenId 340 | image 341 | metadata 342 | image 343 | kind 344 | seed 345 | } 346 | } 347 | ``` 348 | 349 | We can also configure the order direction: 350 | 351 | ```graphql 352 | { 353 | tokens( 354 | orderBy:id, 355 | orderDirection: desc 356 | ) { 357 | id 358 | ownerId 359 | tokenId 360 | image 361 | metadata 362 | image 363 | kind 364 | seed 365 | } 366 | } 367 | ``` 368 | 369 | Or choose to skip forward a certain number of results to implement some basic pagination: 370 | 371 | ```graphql 372 | { 373 | tokens( 374 | skip: 100, 375 | orderBy:id, 376 | orderDirection: desc 377 | ) { 378 | id 379 | owner 380 | tokenId 381 | image 382 | metadata 383 | image 384 | kind 385 | seed 386 | } 387 | } 388 | ``` 389 | 390 | Or query for users and their associated content: 391 | 392 | ```graphql 393 | { 394 | users { 395 | id 396 | tokens { 397 | id 398 | ownerId 399 | tokenId 400 | image 401 | metadata 402 | image 403 | kind 404 | seed 405 | } 406 | } 407 | } 408 | ``` 409 | 410 | We can also with full text search: 411 | 412 | ```graphql 413 | { 414 | tokenSearch(text: "Normies") { 415 | id 416 | ownerId 417 | tokenId 418 | image 419 | metadata 420 | image 421 | kind 422 | seed 423 | } 424 | } 425 | ``` 426 | --------------------------------------------------------------------------------