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_ 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:
[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_. Source: server.rs.
build_ schema#
Wraps a query root in a Juniper schema with no mutations and no subscriptions.
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.
| Parameter | Type | Description |
|---|---|---|
query_root | Q | Your query type, usually a unit struct with a #[graphql_ impl. Generated decoders provide QueryRoot. |
Returns the schema in an Arc, ready for graphql_. The schema is read-only: you cannot add mutations or subscriptions through this helper. To have them, build a juniper:: yourself and mount it with juniper_ directly.
Example.
let schema = build_schema(Query);See also. graphql_, DefaultMutation.
graphql_ router#
Builds an Axum Router that serves the schema.
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.
| Parameter | Type | Description |
|---|---|---|
schema | Arc< | The schema from build_. |
context | C | Shared context, cloned into each request. Typically holds an Arc<. |
Routes:
| Path | Methods | Serves |
|---|---|---|
/graphql | GET, POST | Executes a GraphQL request. GET reads ?query=…; POST reads a JSON body {"query": "…", "variables": {…}}. |
/graphiql | GET | The 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:: or Router:: to add them. The router does not bind a port; pass it to axum::serve.
Example.
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_ uses.
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.
use carbon_core::graphql::server::{DefaultMutation, DefaultSubscription};
type Schema = juniper::RootNode<'static, Query, DefaultMutation<Context>, DefaultSubscription<Context>>;See also. build_.
Primitives#
carbon_. Source: primitives..
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::, and appears in the schema under its Rust name.
| Scalar | Wraps | Output | Accepted input |
|---|---|---|---|
Pubkey | solana_ | base58 string | base58 string |
I64 | i64 | decimal string | decimal string |
I128 | i128 | decimal string | decimal string |
U8 | u8 | decimal string | decimal string |
U32 | u32 | decimal string | decimal string |
U64 | u64 | decimal string | decimal string |
U128 | u128 | decimal string | decimal string |
Json | serde_ | GraphQL 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.
#[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 PubkeyDetails. 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_; convert with graphql::.
Example.
let program = Pubkey::new("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo").unwrap();
println!("{program}");See also. Postgres Pubkey.
I64, I128, U8, U32, U64, U128#
Integers sent as decimal strings.
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.
#[derive(juniper::GraphQLObject)]
struct Status {
slot: U64,
program: Pubkey,
}See also. Example: a minimal server.
Json#
An arbitrary JSON value.
#[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.
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.
| Item | Path | Description |
|---|---|---|
QueryRoot | graphql (re-exported from graphql::) | Query root with two fields per account type and two per instruction type. |
GraphQLContext | graphql:: | #[derive(Clone)] pub struct GraphQLContext { pub pool: Arc<. |
< | accounts:: | GraphQL object for one account type, with an account_ field (accountMetadata in the schema). |
< | instructions:: | GraphQL object for one instruction, with instruction_ and accounts: Json. |
QueryRoot fields (Juniper exposes them in camelCase):
| Field | Arguments | Returns | Query |
|---|---|---|---|
<account> (for example lbPair) | pubkey: String! | the account or null | Lookup by pubkey |
list< (for example listLbPair) | limit: Int!, offset: Int! | list | ORDER BY _ |
< (for example swap) | signature: String!, instructionIndex: Int! | list of every row for that top-level instruction | ORDER BY _ |
list< (for example listSwap) | limit: Int!, offset: Int! | list | ORDER BY _ |
Rows that fail to convert are skipped silently.
Example. Serve a generated decoder's schema over the tables written by PostgresInstructionProcessor:
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();
}{
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.
[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"] }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();
}cargo runGraphiQL: http://127.0.0.1:8080/graphiqlIn another terminal:
curl -s -X POST http://127.0.0.1:8080/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ status { slot program } }"}'{"data":{"status":{"slot":"380000000","program":"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo"}}}The slot field is a string because U64 is a string scalar.