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.