1. API reference

GraphQL API

Every public item in carbon_core::graphql — the Juniper schema and Axum router helpers, and the GraphQL scalars for Solana types.

On this page

carbon_core::graphql serves indexed data over GraphQL with Juniper and Axum. It has two parts: server, which builds a schema and an Axum router with /graphql and /graphiql, and primitives, scalars that carry Solana pubkeys and wide integers without losing precision. For a walkthrough, see Serve data over GraphQL.

The module is behind the graphql feature of carbon-core:

Cargo.toml
[dependencies]
carbon-core = { version = "2.0.0", features = ["graphql"] }
juniper = "0.16.2"
axum = "0.8.7"
tokio = { version = "1.48.0", features = ["full"] }

Server#

carbon_core::graphql::server. Source: server.rs.

build_schema#

Wraps a query root in a Juniper schema with no mutations and no subscriptions.

Rust
pub fn build_schema<Q, C>(
    query_root: Q,
) -> Arc<RootNode<'static, Q, DefaultMutation<C>, DefaultSubscription<C>>>
where
    Q: juniper::GraphQLType<Context = C, TypeInfo = ()> + Send + Sync + 'static,
    C: juniper::Context + Send + Sync + 'static,

Details.

ParameterTypeDescription
query_rootQYour query type, usually a unit struct with a #[graphql_object(context = C)] impl. Generated decoders provide QueryRoot.

Returns the schema in an Arc, ready for graphql_router. The schema is read-only: you cannot add mutations or subscriptions through this helper. To have them, build a juniper::RootNode yourself and mount it with juniper_axum directly.

Example.

Rust
let schema = build_schema(Query);

See also. graphql_router, DefaultMutation.

graphql_router#

Builds an Axum Router that serves the schema.

Rust
pub fn graphql_router<Q, C>(
    schema: Arc<RootNode<'static, Q, DefaultMutation<C>, DefaultSubscription<C>>>,
    context: C,
) -> axum::Router
where
    Q: juniper::GraphQLTypeAsync<Context = C, TypeInfo = ()>
        + juniper::GraphQLValueAsync<Context = C, TypeInfo = ()>
        + Send + Sync + 'static,
    C: juniper::Context + Clone + Send + Sync + 'static,

Details.

ParameterTypeDescription
schemaArc<RootNode<…>>The schema from build_schema.
contextCShared context, cloned into each request. Typically holds an Arc<sqlx::PgPool>.

Routes:

PathMethodsServes
/graphqlGET, POSTExecutes a GraphQL request. GET reads ?query=…; POST reads a JSON body {"query": "…", "variables": {…}}.
/graphiqlGETThe GraphiQL playground, pointed at /graphql.

There is no subscriptions endpoint, no CORS layer and no authentication. Merge the router into your own app with Router::merge or Router::nest to add them. The router does not bind a port; pass it to axum::serve.

Example.

Rust
let app = graphql_router(schema, Context { slot: 380_000_000 });
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();

See also. Example: a minimal server.

DefaultMutation and DefaultSubscription#

Type aliases for the empty mutation and subscription roots build_schema uses.

Rust
pub type DefaultMutation<C> = juniper::EmptyMutation<C>;
pub type DefaultSubscription<C> = juniper::EmptySubscription<C>;

Details. Use them to name the schema type, for example in a function signature or a struct field.

Example.

Rust
use carbon_core::graphql::server::{DefaultMutation, DefaultSubscription};

type Schema = juniper::RootNode<'static, Query, DefaultMutation<Context>, DefaultSubscription<Context>>;

See also. build_schema.

Primitives#

carbon_core::graphql::primitives. Source: primitives.rs.

GraphQL's built-in Int is 32-bit and JavaScript numbers lose precision above 2⁵³, so Carbon's scalars send wide integers as strings. Each is a newtype with one public field, derives Clone, Debug, PartialEq, Eq, Serialize, Deserialize and juniper::GraphQLScalar, and appears in the schema under its Rust name.

ScalarWrapsOutputAccepted input
Pubkeysolana_pubkey::Pubkeybase58 stringbase58 string
I64i64decimal stringdecimal string
I128i128decimal stringdecimal string
U8u8decimal stringdecimal string
U32u32decimal stringdecimal string
U64u64decimal stringdecimal string
U128u128decimal stringdecimal string
Jsonserde_json::ValueGraphQL value (object, list, scalar)a string containing JSON

There is no U16 or I8/I16/I32 scalar; use GraphQL Int (i32) for those.

Pubkey#

A Solana public key as a base58 string.

Rust
#[derive(Clone, Debug, Deserialize, Eq, GraphQLScalar, PartialEq, Serialize)]
#[graphql(parse_token(String), description = "A Solana's public key")]
pub struct Pubkey(pub solana_pubkey::Pubkey);

impl Pubkey {
    pub fn new<S: Into<String>>(value: S) -> Result<Self, String>
}
impl Deref for Pubkey { type Target = solana_pubkey::Pubkey; }
impl Display for Pubkey

Details. new parses a base58 string and returns the parse error as a String. As an input value the scalar accepts only a base58 string; a number literal fails with There needs to be an Int type. It is a different type from carbon_core::postgres::primitives::Pubkey; convert with graphql::primitives::Pubkey(*pg_pubkey).

Example.

Rust
let program = Pubkey::new("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo").unwrap();
println!("{program}");

See also. Postgres Pubkey.

I64, I128, U8, U32, U64, U128#

Integers sent as decimal strings.

Rust
pub struct I64(pub i64);
pub struct I128(pub i128);
pub struct U8(pub u8);
pub struct U32(pub u32);
pub struct U64(pub u64);
pub struct U128(pub u128);

Details. Output is always a string, for example "slot": "380000000". Input must be a string literal or variable. For a field echo(v: U64!), echo(v: "18446744073709551615") works, echo(v: 5) fails with There needs to be an Int type, and a string that does not parse fails with Invalid value "\"abc\"" for type "U64". These types have no From or Deref implementations: construct them with U64(value) and read with .0.

Example.

Rust
#[derive(juniper::GraphQLObject)]
struct Status {
    slot: U64,
    program: Pubkey,
}

See also. Example: a minimal server.

Json#

An arbitrary JSON value.

Rust
#[derive(Clone, Debug, Serialize, Deserialize, Eq, GraphQLScalar, PartialEq)]
#[graphql(parse_token(String), description = "Arbitrary JSON value")]
pub struct Json(pub serde_json::Value);

Details. Output converts the JSON value to a GraphQL value of the same shape. Numbers that fit in i32 stay numbers; larger integers and every u64 above i64::MAX become strings, and floats stay floats. As input, the scalar accepts only a string that contains JSON (j(v: "{\"a\":1}")); a GraphQL object literal (j(v: {a: 1})) is a parse error. Generated decoders use it for the accounts field of instructions and for nested types.

Example.

Rust
let accounts = Json(serde_json::json!({ "lb_pair": "…" }));

See also. Generated decoder types.

Generated decoder types#

A decoder built with its graphql feature adds a schema that reads the decoder's typed Postgres tables.

ItemPathDescription
QueryRootgraphql (re-exported from graphql::query)Query root with two fields per account type and two per instruction type.
GraphQLContextgraphql::context#[derive(Clone)] pub struct GraphQLContext { pub pool: Arc<sqlx::PgPool> }.
<Account>GraphQLaccounts::graphqlGraphQL object for one account type, with an account_metadata: AccountMetadataGraphQL { pubkey, slot } field (accountMetadata in the schema).
<Instruction>GraphQLinstructions::graphqlGraphQL object for one instruction, with instruction_metadata: InstructionMetadataGraphQL { signature, instruction_index, stack_height, slot } and accounts: Json.

QueryRoot fields (Juniper exposes them in camelCase):

FieldArgumentsReturnsQuery
<account> (for example lbPair)pubkey: String!the account or nullLookup by pubkey
list<Account> (for example listLbPair)limit: Int!, offset: Int!listORDER BY __slot DESC
<instruction> (for example swap)signature: String!, instructionIndex: Int!list of every row for that top-level instructionORDER BY __stack_height ASC
list<Instruction> (for example listSwap)limit: Int!, offset: Int!listORDER BY __slot DESC, __signature DESC, __instruction_index ASC

Rows that fail to convert are skipped silently.

Example. Serve a generated decoder's schema over the tables written by PostgresInstructionProcessor:

src/main.rs
use {
    carbon_core::graphql::server::{build_schema, graphql_router},
    carbon_meteora_dlmm_decoder::graphql::{context::GraphQLContext, QueryRoot},
    std::sync::Arc,
};

#[tokio::main]
async fn main() {
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let pool = sqlx::PgPool::connect(&database_url).await.unwrap();

    let schema = build_schema(QueryRoot);
    let app = graphql_router(schema, GraphQLContext { pool: Arc::new(pool) });

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
GraphQL
{
  listSwap(limit: 5, offset: 0) {
    instructionMetadata { signature slot }
    amountIn
    minAmountOut
  }
}

Each generated QueryRoot is a distinct type, so one router serves one decoder. To serve several programs, wrap their query roots in a query type of your own that delegates to each.

Example: a minimal server#

A complete server with a custom query, no database required. Copy it to check your setup before wiring Postgres.

Cargo.toml
[dependencies]
carbon-core = { version = "2.0.0", features = ["graphql"] }
carbon-meteora-dlmm-decoder = "2.0.0"
juniper = "0.16.2"
axum = "0.8.7"
tokio = { version = "1.48.0", features = ["full"] }
src/main.rs
use {
    carbon_core::graphql::{
        primitives::{Pubkey, U64},
        server::{build_schema, graphql_router},
    },
    juniper::{graphql_object, GraphQLObject},
};

#[derive(Clone)]
struct Context {
    slot: u64,
}

impl juniper::Context for Context {}

#[derive(GraphQLObject)]
struct Status {
    slot: U64,
    program: Pubkey,
}

struct Query;

#[graphql_object(context = Context)]
impl Query {
    fn status(context: &Context) -> Status {
        Status {
            slot: U64(context.slot),
            program: Pubkey(carbon_meteora_dlmm_decoder::PROGRAM_ID),
        }
    }
}

#[tokio::main]
async fn main() {
    let schema = build_schema(Query);
    let app = graphql_router(schema, Context { slot: 380_000_000 });

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
    println!("GraphiQL: http://127.0.0.1:8080/graphiql");
    axum::serve(listener, app).await.unwrap();
}
Terminal
cargo run
Output
GraphiQL: http://127.0.0.1:8080/graphiql

In another terminal:

Terminal
curl -s -X POST http://127.0.0.1:8080/graphql \
  -H 'content-type: application/json' \
  -d '{"query":"{ status { slot program } }"}'
Output
{"data":{"status":{"slot":"380000000","program":"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo"}}}

The slot field is a string because U64 is a string scalar.

Your endpoints

Code samples use these instead of the placeholders, so you can copy and run them as they are. They’re stored only in this browser and never sent anywhere.