NFT 인덱서 구축
near-examples/near-lake-nft-indexer
: source code for this tutorial
끝
This tutorial ends with a working NFT indexer built on top NEAR Lake Framework JS. The indexer is watching for nft_mint
Events and prints some relevant data:
receiptId
of the Receipt where the mint has happened- 마켓플레이스
- NFT 소유자 계정 이름
- 마켓플레이스 내 NFT로의 링크
The final source code is available on the GitHub near-examples/near-lake-nft-indexer
목표
NEAR Protocol had introduced a nice feature Events. The Events allow a contract developer to add standardized logs to the ExecutionOutcomes
thus allowing themselves or other developers to read those logs in more convenient manner via API or indexers.
The Events have a field standard
which aligns with NEPs. In this tutorial we'll be talking about NEP-171 Non-Fungible Token standard.
In this tutorial our goal is to show you how you can "listen" to the Events contracts emit and how you can benefit from them.
As the example we will be building an indexer that watches all the NFTs minted following the NEP-171 Events standard, assuming we're collectors who don't want to miss a thing. Our indexer should notice every single NFT minted and give us a basic set of data like: in what Receipt it was minted, and show us the link to a marketplace (we'll cover Paras and Mintbase in our example).
We will use JS version of NEAR Lake Framework in this tutorial. Though the concept is the same for Rust, but we want to show more people that it's not that complex to build your own indexer.
준비
Please, ensure you've the credentials set up as described on the Credentials page. Otherwise you won't be able to get the code working.
You will need:
node
가 설치 및 구성됨
Let's create our project folder
mkdir lake-nft-indexer && cd lake-nft-indexer
Let's add the package.json
{
"name": "lake-nft-indexer",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "tsc && node index.js"
},
"dependencies": {
"near-lake-framework": "^1.0.2"
},
"devDependencies": {
"typescript": "^4.6.4"
}
}
You may have noticed we've added typescript
as a dev dependency. Let's configure the TypeScript. We'll need to create tsconfig.json
file for that
{
"compilerOptions": {
"lib": [
"ES2019",
"dom"
]
}
}
Please, note the ES2019
edition used. We require it because we are going to use .flatMap()
and .flat()
in our code. These methods were introduces in ES2019
. Though you can use even more recent
Let's create empty index.ts
in the project root and thus finish the preparations.
npm install
Now we can start a real work.
NEAR Lake 프레임워크 설정
In the index.ts
let's import startStream
function and types
from near-lake-framework
:
import { startStream, types } from 'near-lake-framework';
Add the instantiation of LakeConfig
below:
const lakeConfig: types.LakeConfig = {
s3BucketName: "near-lake-data-mainnet",
s3RegionName: "eu-central-1",
startBlockHeight: 66264389,
};
Just a few words on the config, we have set s3BucketName
for mainnet, default s3RegionName
and a fresh-ish block height for startBlockHeight
. You can go to NEAR Explorer and get the freshest block height for your setup. Though you can use the same as we do.
Now we need to create a callback function that we'll be called to handle StreamerMessage
our indexer receives.
async function handleStreamerMessage(
streamerMessage: types.StreamerMessage
): Promise<void> {
}
In near-lake-framework
JS library the handler have to be presented as a callback function. This function have to:
- 동기
- accept an argument of type
StreamerMessage
- 아무것도 반환하지 않아야 함(
void
)
And an actual start of our indexer in the end of the index.ts
(async () => {
await startStream(lakeConfig, handleStreamerMessage);
})();
The final index.ts
at this moment should look like the following:
import { startStream, types } from 'near-lake-framework';
const lakeConfig: types.LakeConfig = {
s3BucketName: "near-lake-data-mainnet",
s3RegionName: "eu-central-1",
startBlockHeight: 66264389,
};
async function handleStreamerMessage(
streamerMessage: types.StreamerMessage
): Promise<void> {
}
(async () => {
await startStream(lakeConfig, handleStreamerMessage);
})();
이벤트와 이를 감지할 수 있는 곳
First of all let's find out where we can catch the Events. We hope you are familiar with how the Data Flow in NEAR Blockchain, but let's revise our knowledge:
- Mint an NFT is an action in an NFT contract (doesn't matter which one)
- Actions are located in a Receipt
- A result of the Receipt execution is ExecutionOutcome
ExecutionOutcome
는 따라서, 차례로 컨트랙트가 "출력"하는 로그를 잡습니다.- Events는 로그 내에 존재합니다.
This leads us to the realization that we can watch only for ExecutionOutcomes and ignore everything else StreamerMessage
brings us.
Also, we need to define an interface to catch the Events. Let's copy the interface definition from the Events Nomicon page and paste it before the handleStreamerMessage
function.
interface EventLogData {
standard: string,
version: string,
event: string,
data?: unknown,
};
필요한 데이터만 포착
Inside the callback function handleStreamerMessage
we've prepared in the Preparation section let's start filtering the data we need:
async function handleStreamerMessage(
streamerMessage: types.StreamerMessage
): Promise<void> {
const relevantOutcomes = streamerMessage
.shards
.flatMap(shard => shard.receiptExecutionOutcomes)
}
We have iterated through all the Shards and collected the lists of all ExecutionOutcomes into a single list (in our case we don't care on which Shard did the mint happen)
Now we want to deal only with those ExecutionOutcomes that contain logs of Events format. Such logs start with EVENT_JSON:
according to the Events docs.
Also, we don't require all the data from ExecutionOutcome, let's handle it:
async function handleStreamerMessage(
streamerMessage: types.StreamerMessage
): Promise<void> {
const relevantOutcomes = streamerMessage
.shards
.flatMap(shard => shard.receiptExecutionOutcomes)
.map(outcome => ({
receipt: {
id: outcome.receipt.receiptId,
receiverId: outcome.receipt.receiverId,
},
events: outcome.executionOutcome.outcome.logs.map(
(log: string): EventLogData => {
const [_, probablyEvent] = log.match(/^EVENT_JSON:(.*)$/) ?? []
try {
return JSON.parse(probablyEvent)
} catch (e) {
return
}
}
)
.filter(event => event !== undefined)
}))
}
Let us explain what we are doing here:
- ExecutionOutcomes를 진행 중입니다.
receipt
(ID 및 수신자)와 이벤트를 포함하는events
가 담긴 객체 목록을 구성하고 있습니다.- 이벤트를 수집하기 위해, 정규 표현을 사용하여 이벤트를 구문 분석하려는 ExecutionOutcome의 로그를 반복하고 있습니다.
EventLogData
의 구문 분석에 실패하면undefined
를 반환합니다 - 마지막으로
events
목록이 수집되면,undefined
를 반환하는 것들을 필터링합니다.
Fine, so now we have only a list of our objects that contain some Receipt data and the list of successfully parsed EventLogData
.
The goal for our indexer is to return the useful data about a minted NFT that follows NEP-171 standard. We need to drop irrelevant standard Events:
.filter(relevantOutcome =>
relevantOutcome.events.some(
event => event.standard === "nep171" && event.event === "nft_mint"
)
)
거의 완료
So far we have collected everything we need corresponding to our requirements.
We can print everything in the end of the handleStreamerMessage
:
relevantOutcomes.length && console.dir(relevantOutcomes, { depth: 10 })
The final look of the handleStreamerMessage
function:
async function handleStreamerMessage(
streamerMessage: types.StreamerMessage
): Promise<void> {
const relevantOutcomes = streamerMessage
.shards
.flatMap(shard => shard.receiptExecutionOutcomes)
.map(outcome => ({
receipt: {
id: outcome.receipt.receiptId,
receiverId: outcome.receipt.receiverId,
},
events: outcome.executionOutcome.outcome.logs.map(
(log: string): EventLogData => {
const [_, probablyEvent] = log.match(/^EVENT_JSON:(.*)$/) ?? []
try {
return JSON.parse(probablyEvent)
} catch (e) {
return
}
}
)
.filter(event => event !== undefined)
}))
.filter(relevantOutcome =>
relevantOutcome.events.some(
event => event.standard === "nep171" && event.event === "nft_mint"
)
)
relevantOutcomes.length && console.dir(relevantOutcomes, { depth: 10 })
}
And if we run our indexer we will be catching nft_mint
event and print the data in the terminal.
npm run start
Having troubles running the indexer? Please, check you haven't skipped the Credentials part :)
Not so fast! Remember we were talking about having the links to the marketplaces to see the minted tokens? We're gonna extend our data with links whenever possible. At least we're gonna show you how to deal with the NFTs minted on Paras and Mintbase.