GraphQL Queries
GraphQL provides a flexible query language for indexed blockchain data. This guide covers building a GraphQL API layer over your Solana indexer.
Schema Design
graphql
# schema.graphql
# Base types
scalar DateTime
scalar BigInt
scalar Bytes
# Account types
type Account {
pubkey: String!
programId: String!
lamports: BigInt!
data: Bytes
owner: String!
slot: BigInt!
updatedAt: DateTime!
}
# Transaction types
type Transaction {
signature: String!
slot: BigInt!
blockTime: DateTime
fee: BigInt!
status: TransactionStatus!
instructions: [Instruction!]!
events: [Event!]!
}
enum TransactionStatus {
SUCCESS
FAILED
}
type Instruction {
programId: String!
data: Bytes!
accounts: [String!]!
innerInstructions: [Instruction!]
}
# Event types
interface Event {
signature: String!
slot: BigInt!
blockTime: DateTime
programId: String!
}
type TransferEvent implements Event {
signature: String!
slot: BigInt!
blockTime: DateTime
programId: String!
from: String!
to: String!
amount: BigInt!
mint: String
}
type SwapEvent implements Event {
signature: String!
slot: BigInt!
blockTime: DateTime
programId: String!
tokenIn: String!
tokenOut: String!
amountIn: BigInt!
amountOut: BigInt!
pool: String!
}
type NFTEvent implements Event {
signature: String!
slot: BigInt!
blockTime: DateTime
programId: String!
type: NFTEventType!
mint: String!
buyer: String
seller: String
price: BigInt
}
enum NFTEventType {
MINT
SALE
LIST
DELIST
TRANSFER
}
# Token types
type Token {
mint: String!
name: String
symbol: String
decimals: Int!
supply: BigInt!
holders: Int!
}
type TokenAccount {
address: String!
mint: String!
owner: String!
balance: BigInt!
decimals: Int!
}
type TokenTransfer {
id: ID!
signature: String!
mint: String!
from: String!
to: String!
amount: BigInt!
blockTime: DateTime
}
# NFT types
type NFT {
mint: String!
name: String!
symbol: String!
uri: String!
sellerFeeBasisPoints: Int!
creators: [Creator!]
collection: Collection
attributes: [Attribute!]
owner: String
image: String
}
type Creator {
address: String!
share: Int!
verified: Boolean!
}
type Collection {
address: String!
name: String
verified: Boolean!
}
type Attribute {
traitType: String!
value: String!
}
# Pagination
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type TransactionConnection {
edges: [TransactionEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type TransactionEdge {
node: Transaction!
cursor: String!
}
# Queries
type Query {
# Accounts
account(pubkey: String!): Account
accounts(
programId: String
owner: String
first: Int
after: String
): [Account!]!
# Transactions
transaction(signature: String!): Transaction
transactions(
address: String
programId: String
first: Int
after: String
before: DateTime
after_time: DateTime
): TransactionConnection!
# Events
events(programId: String!, type: String, first: Int, after: String): [Event!]!
# Tokens
token(mint: String!): Token
tokenAccounts(owner: String!, mint: String): [TokenAccount!]!
tokenTransfers(
mint: String
address: String
first: Int
after: String
): [TokenTransfer!]!
# NFTs
nft(mint: String!): NFT
nftsByOwner(owner: String!): [NFT!]!
nftsByCollection(collection: String!, first: Int, after: String): [NFT!]!
nftSales(collection: String, first: Int, after: String): [NFTEvent!]!
}
# Subscriptions
type Subscription {
accountUpdated(pubkey: String!): Account!
newTransaction(programId: String!): Transaction!
newEvent(programId: String!, type: String): Event!
}
Resolver Implementation
TypeScript
import { Pool } from "pg";
import { PubSub } from "graphql-subscriptions";
import { GraphQLDateTime, GraphQLBigInt } from "graphql-scalars";
const pubsub = new PubSub();
interface Context {
db: Pool;
pubsub: PubSub;
}
export const resolvers = {
// Scalars
DateTime: GraphQLDateTime,
BigInt: GraphQLBigInt,
Bytes: {
serialize: (value: Buffer) => value.toString("base64"),
parseValue: (value: string) => Buffer.from(value, "base64"),
},
// Query resolvers
Query: {
account: async (
_: any,
{ pubkey }: { pubkey: string },
{ db }: Context,
) => {
const result = await db.query(
"SELECT * FROM accounts WHERE pubkey = $1",
[pubkey],
);
return result.rows[0] || null;
},
accounts: async (
_: any,
args: {
programId?: string;
owner?: string;
first?: number;
after?: string;
},
{ db }: Context,
) => {
let query = "SELECT * FROM accounts WHERE 1=1";
const params: any[] = [];
if (args.programId) {
params.push(args.programId);
query += ` AND program_id = $${params.length}`;
}
if (args.owner) {
params.push(args.owner);
query += ` AND owner = $${params.length}`;
}
if (args.after) {
params.push(args.after);
query += ` AND pubkey > $${params.length}`;
}
params.push(args.first || 100);
query += ` ORDER BY pubkey LIMIT $${params.length}`;
const result = await db.query(query, params);
return result.rows;
},
transaction: async (
_: any,
{ signature }: { signature: string },
{ db }: Context,
) => {
const txResult = await db.query(
"SELECT * FROM transactions WHERE signature = $1",
[signature],
);
if (txResult.rows.length === 0) return null;
const tx = txResult.rows[0];
// Fetch instructions
const ixResult = await db.query(
`SELECT * FROM instructions WHERE signature = $1 ORDER BY instruction_index`,
[signature],
);
// Fetch events
const eventResult = await db.query(
"SELECT * FROM events WHERE signature = $1",
[signature],
);
return {
...tx,
instructions: ixResult.rows,
events: eventResult.rows,
};
},
transactions: async (
_: any,
args: {
address?: string;
programId?: string;
first?: number;
after?: string;
before?: Date;
after_time?: Date;
},
{ db }: Context,
) => {
let query = `
SELECT t.* FROM transactions t
LEFT JOIN instructions i ON t.signature = i.signature
WHERE 1=1
`;
const params: any[] = [];
if (args.programId) {
params.push(args.programId);
query += ` AND i.program_id = $${params.length}`;
}
if (args.before) {
params.push(args.before);
query += ` AND t.block_time < $${params.length}`;
}
if (args.after_time) {
params.push(args.after_time);
query += ` AND t.block_time > $${params.length}`;
}
if (args.after) {
const cursor = decodeCursor(args.after);
params.push(cursor.blockTime, cursor.signature);
query += ` AND (t.block_time, t.signature) < ($${params.length - 1}, $${params.length})`;
}
query += ` GROUP BY t.signature ORDER BY t.block_time DESC, t.signature DESC`;
params.push((args.first || 20) + 1); // Fetch one extra to check hasNextPage
query += ` LIMIT $${params.length}`;
const result = await db.query(query, params);
const hasNextPage = result.rows.length > (args.first || 20);
const nodes = hasNextPage ? result.rows.slice(0, -1) : result.rows;
return {
edges: nodes.map((node: any) => ({
node,
cursor: encodeCursor({
blockTime: node.block_time,
signature: node.signature,
}),
})),
pageInfo: {
hasNextPage,
hasPreviousPage: !!args.after,
startCursor: nodes[0]
? encodeCursor({
blockTime: nodes[0].block_time,
signature: nodes[0].signature,
})
: null,
endCursor: nodes[nodes.length - 1]
? encodeCursor({
blockTime: nodes[nodes.length - 1].block_time,
signature: nodes[nodes.length - 1].signature,
})
: null,
},
totalCount: -1, // Expensive to compute, return -1 or implement separately
};
},
events: async (
_: any,
args: {
programId: string;
type?: string;
first?: number;
after?: string;
},
{ db }: Context,
) => {
let query = "SELECT * FROM events WHERE program_id = $1";
const params: any[] = [args.programId];
if (args.type) {
params.push(args.type);
query += ` AND event_type = $${params.length}`;
}
if (args.after) {
params.push(args.after);
query += ` AND id > $${params.length}`;
}
params.push(args.first || 100);
query += ` ORDER BY block_time DESC LIMIT $${params.length}`;
const result = await db.query(query, params);
return result.rows.map((row: any) => ({
...row,
__typename: getEventTypename(row.event_type),
}));
},
token: async (_: any, { mint }: { mint: string }, { db }: Context) => {
const result = await db.query("SELECT * FROM tokens WHERE mint = $1", [
mint,
]);
return result.rows[0] || null;
},
tokenAccounts: async (
_: any,
{ owner, mint }: { owner: string; mint?: string },
{ db }: Context,
) => {
let query = "SELECT * FROM token_accounts WHERE owner = $1";
const params: any[] = [owner];
if (mint) {
params.push(mint);
query += ` AND mint = $${params.length}`;
}
const result = await db.query(query, params);
return result.rows;
},
nft: async (_: any, { mint }: { mint: string }, { db }: Context) => {
const result = await db.query(
"SELECT * FROM nft_metadata WHERE mint = $1",
[mint],
);
return result.rows[0] || null;
},
nftsByOwner: async (
_: any,
{ owner }: { owner: string },
{ db }: Context,
) => {
const result = await db.query(
`SELECT n.* FROM nft_metadata n
JOIN token_accounts t ON n.mint = t.mint
WHERE t.owner = $1 AND t.balance > 0`,
[owner],
);
return result.rows;
},
nftsByCollection: async (
_: any,
args: { collection: string; first?: number; after?: string },
{ db }: Context,
) => {
let query = "SELECT * FROM nft_metadata WHERE collection = $1";
const params: any[] = [args.collection];
if (args.after) {
params.push(args.after);
query += ` AND mint > $${params.length}`;
}
params.push(args.first || 100);
query += ` ORDER BY mint LIMIT $${params.length}`;
const result = await db.query(query, params);
return result.rows;
},
},
// Type resolvers
Transaction: {
status: (tx: any) => (tx.err ? "FAILED" : "SUCCESS"),
blockTime: (tx: any) => tx.block_time,
},
Event: {
__resolveType: (event: any) => event.__typename,
},
NFT: {
creators: (nft: any) => nft.creators || [],
attributes: (nft: any) => nft.attributes || [],
sellerFeeBasisPoints: (nft: any) => nft.seller_fee_basis_points,
},
// Subscriptions
Subscription: {
accountUpdated: {
subscribe: (_: any, { pubkey }: { pubkey: string }) => {
return pubsub.asyncIterator([`ACCOUNT_UPDATED_${pubkey}`]);
},
},
newTransaction: {
subscribe: (_: any, { programId }: { programId: string }) => {
return pubsub.asyncIterator([`NEW_TX_${programId}`]);
},
},
newEvent: {
subscribe: (
_: any,
{ programId, type }: { programId: string; type?: string },
) => {
const channel = type
? `NEW_EVENT_${programId}_${type}`
: `NEW_EVENT_${programId}`;
return pubsub.asyncIterator([channel]);
},
},
},
};
// Helper functions
function encodeCursor(data: any): string {
return Buffer.from(JSON.stringify(data)).toString("base64");
}
function decodeCursor(cursor: string): any {
return JSON.parse(Buffer.from(cursor, "base64").toString());
}
function getEventTypename(type: string): string {
switch (type) {
case "transfer":
return "TransferEvent";
case "swap":
return "SwapEvent";
case "nft_sale":
case "nft_mint":
return "NFTEvent";
default:
return "Event";
}
}
// Publish events from indexer
export function publishAccountUpdate(pubkey: string, account: any) {
pubsub.publish(`ACCOUNT_UPDATED_${pubkey}`, { accountUpdated: account });
}
export function publishTransaction(programId: string, transaction: any) {
pubsub.publish(`NEW_TX_${programId}`, { newTransaction: transaction });
}
export function publishEvent(programId: string, type: string, event: any) {
pubsub.publish(`NEW_EVENT_${programId}`, { newEvent: event });
pubsub.publish(`NEW_EVENT_${programId}_${type}`, { newEvent: event });
}
Server Setup
TypeScript
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import express from "express";
import http from "http";
import cors from "cors";
import { Pool } from "pg";
import { PubSub } from "graphql-subscriptions";
import { readFileSync } from "fs";
async function startServer() {
// Database connection
const db = new Pool({
connectionString: process.env.DATABASE_URL,
});
const pubsub = new PubSub();
// Load schema
const typeDefs = readFileSync("./schema.graphql", "utf-8");
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
// Create HTTP server
const app = express();
const httpServer = http.createServer(app);
// Create WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: "/graphql",
});
const serverCleanup = useServer(
{
schema,
context: () => ({ db, pubsub }),
},
wsServer,
);
// Create Apollo Server
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
// Apply middleware
app.use(
"/graphql",
cors<cors.CorsRequest>(),
express.json(),
expressMiddleware(server, {
context: async () => ({ db, pubsub }),
}),
);
// Start server
const PORT = process.env.PORT || 4000;
httpServer.listen(PORT, () => {
console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`);
console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}/graphql`);
});
}
startServer();
Example Queries
graphql
# Get NFTs owned by wallet
query GetWalletNFTs($owner: String!) {
nftsByOwner(owner: $owner) {
mint
name
image
collection {
name
address
}
attributes {
traitType
value
}
}
}
# Get recent transactions for a program
query GetProgramTransactions($programId: String!, $first: Int) {
transactions(programId: $programId, first: $first) {
edges {
node {
signature
blockTime
fee
status
events {
... on SwapEvent {
tokenIn
tokenOut
amountIn
amountOut
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Subscribe to new events
subscription OnNewSwap($programId: String!) {
newEvent(programId: $programId, type: "swap") {
... on SwapEvent {
signature
tokenIn
tokenOut
amountIn
amountOut
pool
}
}
}
DataLoader for N+1 Prevention
TypeScript
import DataLoader from "dataloader";
function createLoaders(db: Pool) {
return {
accountLoader: new DataLoader(async (pubkeys: readonly string[]) => {
const result = await db.query(
"SELECT * FROM accounts WHERE pubkey = ANY($1)",
[pubkeys],
);
const accountMap = new Map(
result.rows.map((row: any) => [row.pubkey, row]),
);
return pubkeys.map((pubkey) => accountMap.get(pubkey) || null);
}),
nftLoader: new DataLoader(async (mints: readonly string[]) => {
const result = await db.query(
"SELECT * FROM nft_metadata WHERE mint = ANY($1)",
[mints],
);
const nftMap = new Map(result.rows.map((row: any) => [row.mint, row]));
return mints.map((mint) => nftMap.get(mint) || null);
}),
tokenAccountsLoader: new DataLoader(async (owners: readonly string[]) => {
const result = await db.query(
"SELECT * FROM token_accounts WHERE owner = ANY($1)",
[owners],
);
const grouped = new Map<string, any[]>();
for (const row of result.rows) {
const existing = grouped.get(row.owner) || [];
existing.push(row);
grouped.set(row.owner, existing);
}
return owners.map((owner) => grouped.get(owner) || []);
}),
};
}
Next: MPC Overview - Multi-party computation for enhanced security.