Minted NFTs Indexer
NEAR QueryAPI is currently under development. Users who want to test-drive this solution need to be added to the allowlist before creating or forking QueryAPI indexers.
You can request access through this link.
Overview
This tutorial creates a working NFT indexer using NEAR QueryAPI, and builds a NEAR component that presents the data. The indexer is watching for nft_mint
Events and captures some relevant data:
receiptId
of the Receipt where the mint has happenedreceiverId
- Marketplace
- Links to the transaction on NEAR Explorer
In this tutorial you'll learn how you can listen to Events generated by smart contracts and how you can index them.
The indexer's source code can be found by following this link.
NFT Events
NEAR Protocol supports Events. These 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. Events have a field standard
which aligns with NEPs. In this tutorial we'll be talking about NEP-171 Non-Fungible Token standard.
The indexer watches all the NFTs minted following the NEP-171 Events standard. It should detect every single NFT minted, and store a basic set of data like: in what Receipt it was minted, and which marketplace created it (for example, Paras, ShardDog, and Mintbase).
Defining the Database Schema
The first step to creating an indexer is to define the database schema. This is done by editing the schema.sql
file in the code editor. The schema for this indexer looks like this:
CREATE TABLE
"nfts" (
"id" SERIAL NOT NULL,
"marketplace" TEXT,
"block_height" BIGINT,
"block_timestamp" BIGINT,
"receipt_id" TEXT,
"receiver_id" TEXT,
"nft_data" TEXT,
PRIMARY KEY ("id", "block_height", "block_timestamp")
);
This schema defines one table: nfts
. The table has these columns:
id
: a unique identifier for each row in the tablemarketplace
: the marketplace where the NFT was createdblock_height
: the height of the block in which the NFT was createdblock_timestamp
: the timestamp of the block in which the NFT was createdreceipt_id
: the receipt ID of the transaction that created the NFTreceiver_id
: the receiver ID of the transaction that created the NFTnft_data
: the content of the minted NFT
Defining the indexing logic
The next step is to define the indexing logic. This is done by editing the indexingLogic.js
file in the code editor. The logic for this indexer can be divided into two parts:
- Filtering blockchain transactions for a specific type of transaction
- Saving the data from the filtered transactions to the database
Filtering Blockchain transactions
The first part of the logic is to filter blockchain transactions for a specific type of transaction, where the Event
is a NEP-171 nft_mint
. This is done by using the getBlock
function. This function takes in a block and a context and returns a promise. The block
is a Near Protocol block, and the context
is a set of helper methods to retrieve and commit state. The getBlock
function is called for every block on the blockchain.
The getBlock
function for this NFT indexer looks like this:
async function getBlock(block: Block) {
for (let ev of block.events()) {
const r = block.actionByReceiptId(ev.relatedReceiptId);
const createdOn = block.streamerMessage.block.header.timestamp;
try {
let event = ev.rawEvent;
if (event.standard === "nep171" && event.event === "nft_mint") {
console.log(event);
let marketplace = "unknown";
if (r.receiverId.endsWith(".paras.near")) marketplace = "Paras";
else if (r.receiverId.endsWith(".sharddog.near"))
marketplace = "ShardDog";
else if (r.receiverId.match(/\.mintbase\d+\.near$/))
marketplace = "Mintbase";
const nftMintData = {
marketplace: marketplace,
block_height: block.header().height,
block_timestamp: createdOn,
receipt_id: r.receiptId,
receiver_id: r.receiverId,
nft_data: JSON.stringify(event.data),
};
await context.db.Nfts.insert(nftMintData);
console.log(`NFT by ${r.receiptId} has been added to the database`);
}
} catch (e) {
console.log(e);
}
}
}
This indexer filters Blocks that have Events of type nft_mint
and standard nep171
. In addition, it stores the JSON event data and identifies the NFT marketplace.
Saving the data to the Database
The second part of the logic is to save the data from the filtered transactions to the database.
This is solved easily by using the context.db.Nfts.insert
helper method:
The logic for this looks like:
const nftMintData = {
marketplace: marketplace,
block_height: h,
block_timestamp: createdOn,
receipt_id: r.receiptId,
receiver_id: r.receiverId,
nft_data: JSON.stringify(event.data),
};
// store result to the database
await context.db.Nfts.insert(nftMintData);
NEAR Component
The final step is querying the indexer using GraphQL from a NEAR component with WebSockets.
Here's a simple GraphQL query that gets the last ${LIMIT}
minted NFTs:
IndexerQuery {
bucanero_near_nft_v4_nfts(order_by: {block_timestamp: desc}, limit: ${LIMIT}) {
block_height
block_timestamp
id
marketplace
nft_data
receipt_id
receiver_id
}
Setup
Here's a code snippet that subscribes and processes the most recent activity (last 10 NFTs) from the NFT indexer:
The code below is only a snippet. If you want the full source code to play around with the component, you can fork the NFT Activity Feed source code and build your own NEAR component.
const GRAPHQL_ENDPOINT = "near-queryapi.api.pagoda.co";
const LIMIT = 10;
const accountId = props.accountId || "bucanero.near" || context.accountId;
State.init({
widgetActivities: [],
widgetActivityCount: 0,
startWebSocketWidgetActivity: null,
initialFetch: false,
});
const widgetActivitySubscription = `
subscription IndexerQuery {
bucanero_near_nft_v4_nfts(order_by: {block_timestamp: desc}, limit: ${LIMIT}) {
block_height
block_timestamp
id
marketplace
nft_data
receipt_id
receiver_id
}
}
`;
const subscriptionWidgetActivity = {
type: "start",
id: "widgetNftActivity", // You can use any unique identifier
payload: {
operationName: "IndexerQuery",
query: widgetActivitySubscription,
variables: {},
},
};
function processWidgetActivity(activity) {
return { ...activity };
}
function startWebSocketWidgetActivity(processWidgetActivities) {
let ws = State.get().ws_widgetActivity;
if (ws) {
ws.close();
return;
}
ws = new WebSocket(`wss://${GRAPHQL_ENDPOINT}/v1/graphql`, "graphql-ws");
ws.onopen = () => {
console.log(`Connection to WS has been established`);
ws.send(
JSON.stringify({
type: "connection_init",
payload: {
headers: {
"Content-Type": "application/json",
"Hasura-Client-Name": "hasura-console",
"x-hasura-role": "bucanero_near",
},
lazy: true,
},
})
);
setTimeout(() => ws.send(JSON.stringify(subscriptionWidgetActivity)), 50);
};
ws.onclose = () => {
State.update({ ws_widgetActivity: null });
console.log(`WS Connection has been closed`);
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log("received data", data);
if (data.type === "data" && data.id === "widgetNftActivity") {
processWidgetActivities(data.payload.data);
}
};
ws.onerror = (err) => {
State.update({ ws_widgetActivity: null });
console.log("WebSocket error", err);
};
State.update({ ws_widgetActivity: ws });
}
Pay attention to the widgetActivitySubscription
GraphQL query and the subscriptionWidgetActivity
JSON payload.
Processing
This is the JS function that process the incoming widget activities generated by the QueryAPI indexer, allowing the NEAR component to create a feed based on the blockchain's widget activity:
You can fork the NFT Activity Feed source code and build your own NEAR component.
function processWidgetActivities(incoming_data) {
let incoming_widgetActivities =
incoming_data.bucanero_near_nft_v4_nfts.flatMap(processWidgetActivity);
const newActivities = [
...incoming_widgetActivities.filter((activity) => {
return (
state.widgetActivities.length == 0 ||
activity.block_timestamp > state.widgetActivities[0].block_timestamp
);
}),
];
if (newActivities.length > 0 && state.widgetActivities.length > 0) {
}
const prevActivities = state.prevActivities || [];
State.update({ widgetActivities: [...newActivities, ...prevActivities] });
}
if (state.ws_widgetActivity === undefined) {
State.update({
startWebSocketWidgetActivity: startWebSocketWidgetActivity,
});
state.startWebSocketWidgetActivity(processWidgetActivities);
}
Rendering
Finally, rendering the activity feed on the NEAR component is straight-forward, by iterating through the state.widgetActivities
map:
return (
<div>
<Title>
NFT Minting Activity Feed{" "}
<TextLink href="https://near.org/dataplatform.near/widget/QueryApi.App">
{" "}
Powered By QueryAPI{" "}
</TextLink>
</Title>
<RowContainer>
{state.widgetActivities.map((activity, i) => (
<Card>
<div>
<Widget
src="mob.near/widget/TimeAgo"
props={{ blockHeight: activity.block_height }}
/>{" "}
ago
</div>
<CardBody>
<div key={i}>
<Text bold>NFT Marketplace: {activity.marketplace}</Text>
<TextLink
href={`https://nearblocks.io/address/${activity.receiver_id}`}
>
{activity.receiver_id}
</TextLink>
<Text bold>Receipt ID: {activity.receipt_id}</Text>
</div>
</CardBody>
<CardFooter>
<TextLink
href={`https://legacy.nearblocks.io/?query=${activity.receipt_id}`}
>
View details on NEAR Explorer
</TextLink>
</CardFooter>
</Card>
))}
</RowContainer>
</div>
);