Skip to main content

WebSocket-enabled Components with QueryAPI

In this article you'll learn how to create a NEAR component that gathers information from a QueryAPI indexer using WebSockets. In this example, the QueryAPI indexer monitors the widget activity on the blockchain, and the NEAR component gets that information using WebSockets.

info

QueryAPI is a fully managed solution to build indexer functions, extract on-chain data, store it in a database, and be able to query it using GraphQL endpoints.

QueryAPI indexer

The Widget Activity indexer keeps track of any widget activity on the social.near smart contract. Whenever a Widget transaction is found, the data is stored in a Postgres database.

DB schema

The schema for the indexer's database is pretty simple:

CREATE TABLE
"widget_activity" (
"id" SERIAL NOT NULL,
"account_id" VARCHAR NOT NULL,
"widget_name" VARCHAR NOT NULL,
"block_height" DECIMAL(58, 0) NOT NULL,
"receipt_id" VARCHAR NOT NULL,
"block_timestamp" DECIMAL(20, 0) NOT NULL,
CONSTRAINT "widgets_pkey" PRIMARY KEY ("id")
);

CREATE INDEX
idx_widget_activity_block_timestamp ON widget_activity (block_timestamp);

Indexer logic

In the following code snippet, you can find the simple indexer logic that filters widget transactions from the social.near smart contract, and if it finds widget development activity, then it adds a record to the widget_activity table defined previously.

tip

To learn more, check the complete source code of the Widget Activity indexer.

  // Add your code here
const SOCIAL_DB = "social.near";

const nearSocialWidgetTxs = block
.actions()
.filter((action) => action.receiverId === SOCIAL_DB)
.flatMap((action) =>
action.operations
.map((operation) => operation["FunctionCall"])
.filter((operation) => operation?.methodName === "set")
.map((functionCallOperation) => ({
...functionCallOperation,
args: base64decode(functionCallOperation.args),
receiptId: action.receiptId, // providing receiptId as we need it
}))
.filter((functionCall) => {
const accountId = Object.keys(functionCall.args.data)[0];
return Object.keys(functionCall.args.data[accountId]).includes(
"widget"
);
})
);

if (nearSocialWidgetTxs.length > 0) {
console.log("Found NEAR Widget Development Activity...");
const blockHeight = block.blockHeight;
const blockTimestamp = block.header().timestampNanosec;
console.log(nearSocialWidgetTxs);
await Promise.all(
nearSocialWidgetTxs.map(async (widgetEditTx) => {
const accountId = Object.keys(widgetEditTx.args.data)[0];
const widgetName = Object.keys(
widgetEditTx.args.data[accountId]["widget"]
)[0];

console.log(`ACCOUNT_ID: ${accountId}`);
console.log(widgetName);
await handleWidgetTx(
accountId,
widgetName,
blockHeight,
blockTimestamp,
widgetEditTx.receiptId
);
console.log(widgetEditTx);
})
);
}
}

This is the JS function that calls the GraphQL mutation InsertWidgetActivity and adds a record to the widget_activity table:

tip

Learn more about QueryAPI indexing functions and how to build your own indexers.

  async function handleWidgetTx(
accountId,
widgetName,
blockHeight,
blockTimestamp,
receiptId
) {
console.log(accountId, blockHeight, blockTimestamp, receiptId);
try {
const mutationData = {
activity: {
account_id: accountId,
widget_name: widgetName,
block_height: blockHeight,
block_timestamp: blockTimestamp,
receipt_id: receiptId,
},
};
await context.graphql(
`mutation InsertWidgetActivity($activity: roshaan_near_widget_activity_feed_widget_activity_insert_input = {}) {
insert_roshaan_near_widget_activity_feed_widget_activity_one(object: $activity) {
id
}
}`,
mutationData
);
} catch (e) {
console.log(`Could not add widget activity to DB, ${e}`);
}
}

Using WebSockets

Once you have a QueryAPI indexer running, you can use WebSockets to get the data in your NEAR Component. You only need to create a WebSocket object pointing to the QueryAPI's GraphQL endpoint.

Setup

Here's a code snippet from the NEAR component that subscribes and processes any activity from the Widget Activity indexer:

tip

The code below is only a snippet. If you want the full source code to play around with the component, you can fork the Widget 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 || "roshaan.near" || context.accountId;

State.init({
widgetActivities: [],
widgetActivityCount: 0,
startWebSocketWidgetActivity: null,
initialFetch: false,
soundEffect:
"https://bafybeic7uvzmhuwjficgctpleov5i43rteavwmktyyjrauwi346ntgja4a.ipfs.nftstorage.link/",
});

const widgetActivitySubscription = `
subscription IndexerQuery {
roshaan_near_widget_activity_feed_widget_activity(
order_by: {block_timestamp: desc}
limit: ${LIMIT}
) {
account_id
block_height
block_timestamp
id
receipt_id
widget_name
}
}
`;

const subscriptionWidgetActivity = {
type: "start",
id: "widgetActivity", // 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": "roshaan_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 === "widgetActivity") {
processWidgetActivities(data.payload.data);
}
};

ws.onerror = (err) => {
State.update({ ws_widgetActivity: null });
console.log("WebSocket error", err);
};

State.update({ ws_widgetActivity: ws });
}
info

Pay attention to 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:

tip

You can fork the Widget Activity Feed source code and build your own NEAR component.

function processWidgetActivities(incoming_data) {
let incoming_widgetActivities =
incoming_data.roshaan_near_widget_activity_feed_widget_activity.flatMap(
processWidgetActivity
);
const newActivities = [
...incoming_widgetActivities.filter((activity) => {
return (
state.widgetActivities.length == 0 ||
activity.block_timestamp > state.widgetActivities[0].block_timestamp
);
}),
];
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>
Widget 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>Widget Name: {activity.widget_name}</Text>
<Text bold>Account ID: {activity.account_id}</Text>
</div>
</CardBody>
<CardFooter>
<TextLink
href={`/#/near/widget/ComponentDetailsPage?src=${activity.account_id}/widget/${activity.widget_name}`}
>
View
</TextLink>
</CardFooter>
</Card>
))}
</RowContainer>
</div>
);

Was this page helpful?