A Rust client for the Dune Analytics API. Execute queries, wait for completion, and deserialize results into your own types.
cargo add dunersYou’ll need the tokio runtime (e.g. tokio with rt-multi-thread and macros).
-
Get an API key from Dune → Settings → API.
-
Set it (or put it in a
.envfile asDUNE_API_KEY=...):export DUNE_API_KEY="your-api-key"
-
Run a saved query using
run_query(execute → wait until done → return all results):
use duners::{DuneClient, DuneRequestError};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Row {
symbol: String,
max_price: f64,
}
#[tokio::main]
async fn main() -> Result<(), DuneRequestError> {
let client = DuneClient::from_env();
let result = client.run_query::<Row>(971694, None, None).await?;
println!("{:?}", result.get_rows());
Ok(())
}The query ID (e.g. 971694) is the number at the end of a Dune query URL: https://dune.com/queries/971694.
To execute repository-owned SQL without creating a saved query, use run_sql:
let result = client
.run_sql::<MyRow>("SELECT 1 AS value", None, None)
.await?;For large result sets, stream_query and stream_sql yield each result page as it is fetched instead of buffering everything in memory:
use futures_util::StreamExt;
let pages = client
.stream_sql::<MyRow>("SELECT 1 AS value", None, None)
.await?;
let mut pages = std::pin::pin!(pages);
while let Some(page) = pages.next().await {
println!("{:?}", page?.get_rows());
}DuneClient::new(api_key)— pass the API key directly.DuneClient::from_env()— readsDUNE_API_KEYfrom the environment. If a.envfile exists in the current directory, it is loaded first.
For saved queries that take parameters, pass a list of Parameter as the second argument to run_query (or execute_query):
use duners::{DuneClient, Parameter};
let params = vec![
Parameter::text("WalletAddress", "0x1234..."),
Parameter::number("MinAmount", "100"),
Parameter::list("Token", "ETH"),
];
let result = client.run_query::<MyRow>(QUERY_ID, Some(params), None).await?;Parameter names must match the names defined in the query on Dune.
Define a struct whose fields match the query’s columns and derive Deserialize. You can use your own types; depending on the column type, the API returns numbers and dates either as JSON numbers or as strings, so use the helpers in parse_utils to accept both:
use chrono::{DateTime, Utc};
use duners::parse_utils::{datetime_from_str, f64_from_str, optional_datetime_from_str, u64_from_str};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct ResultStruct {
text_field: String,
#[serde(deserialize_with = "f64_from_str")]
volume_usd: f64,
#[serde(deserialize_with = "u64_from_str")]
trade_count: u64,
#[serde(deserialize_with = "datetime_from_str")]
block_time: DateTime<Utc>,
#[serde(default, deserialize_with = "optional_datetime_from_str")]
first_trade_at: Option<DateTime<Utc>>,
}f64_from_str/u64_from_str— for numeric columns, whether they arrive as JSON numbers or strings (Dune encodes e.g. decimals and bigints as strings).datetime_from_str/optional_datetime_from_str— for date/timestamp columns; accepts RFC 3339 as well as Dune result formats like2022-01-01 01:02:03[.000][ UTC].
For more control (e.g. custom polling or cancellation):
execute_query(query_id, params)— start execution; returns anexecution_id.get_status(execution_id)— check status (Complete,Executing,Pending,Cancelled,Failed).get_results(execution_id)— fetch result rows (only valid when status isComplete).cancel_execution(execution_id)— cancel a running execution.
See the API docs for details and types.
Submit contracts for decoding in batches and track their status. Submissions are attributed to the user who created the API key; see the docs for plan requirements.
use duners::{ContractSubmissionInput, DuneClient, ListContractSubmissionsRequest, SubmitContractsRequest};
# async fn run() -> Result<(), duners::DuneRequestError> {
let client = DuneClient::from_env();
let resp = client.submit_contracts(SubmitContractsRequest {
submissions: vec![ContractSubmissionInput {
blockchain_name: "ethereum".into(),
address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984".into(),
project_name: "uniswap".into(),
contract_name: "UniswapToken".into(),
abi: serde_json::json!([{"type": "event", "name": "Transfer", "inputs": []}]),
idempotency_key: Some("uniswap-token/ethereum/1".into()), // optional, makes retries safe
..Default::default()
}],
}).await?;
// One result per submission, matched by index: submission_id + status "pending", or error.
println!("{:?}", resp.results);
let page = client.list_contract_submissions(ListContractSubmissionsRequest {
limit: Some(20),
..Default::default()
}).await?;
// Pass page.next_cursor back as `cursor` to fetch the next page.
println!("{} of {}", page.submissions.len(), page.total);
# Ok(()) }All fallible methods return Result<_, DuneRequestError>. Use ? to propagate. DuneRequestError implements std::error::Error and Display; variants are:
DuneRequestError::Dune(msg)— API returned an error (e.g. invalid API key, query not found).DuneRequestError::Request(msg)— network/HTTP error (e.g. connection failed, timeout).
Full API reference: docs.rs/duners
MIT OR Apache-2.0