use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use clap::Parser;
use time::OffsetDateTime;
use questrade_client::{CachedToken, OnTokenRefresh, QuestradeClient, TokenManager};
fn token_dir() -> PathBuf {
let dir = std::env::temp_dir().join("questrade-example");
std::fs::create_dir_all(&dir).expect("create token dir");
dir
}
fn save_refresh_token(token: &str) {
let path = token_dir().join("refresh_token");
std::fs::write(&path, token).expect("write refresh token");
eprintln!("Saved refresh token to {}", path.display());
}
fn load_refresh_token() -> Option<String> {
let path = token_dir().join("refresh_token");
std::fs::read_to_string(path).ok().filter(|s| !s.is_empty())
}
fn save_access_token(access_token: &str, api_server: &str, expires_at: OffsetDateTime) {
let json = serde_json::json!({
"access_token": access_token,
"api_server": api_server,
"expires_at": expires_at.format(&time::format_description::well_known::Rfc3339).unwrap(),
});
let path = token_dir().join("access_token.json");
std::fs::write(&path, json.to_string()).expect("write access token cache");
eprintln!("Cached access token to {}", path.display());
}
fn load_cached_token() -> Option<CachedToken> {
let path = token_dir().join("access_token.json");
let text = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&text).ok()?;
let expires_at = time::OffsetDateTime::parse(
v["expires_at"].as_str()?,
&time::format_description::well_known::Rfc3339,
)
.ok()?;
Some(CachedToken {
access_token: v["access_token"].as_str()?.to_string(),
api_server: v["api_server"].as_str()?.to_string(),
expires_at,
})
}
#[derive(Parser)]
struct Args {
#[arg(long, env = "QUESTRADE_REFRESH_TOKEN")]
refresh_token: Option<String>,
#[arg(long, default_value_t = false)]
practice: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter("questrade_client=debug")
.with_writer(std::io::stderr)
.init();
let args = Args::parse();
let refresh_token = args
.refresh_token
.or_else(load_refresh_token)
.context("No refresh token. Pass --refresh-token or run once to persist one.")?;
let on_refresh: OnTokenRefresh = Arc::new(|token| {
save_refresh_token(&token.refresh_token);
let expires_at =
OffsetDateTime::now_utc() + time::Duration::seconds(token.expires_in as i64 - 30);
save_access_token(&token.access_token, &token.api_server, expires_at);
});
let cached = load_cached_token();
if cached.is_some() {
eprintln!("Found cached access token — will skip OAuth refresh if still valid.");
}
let tm = TokenManager::new(refresh_token, args.practice, Some(on_refresh), cached).await?;
let client = QuestradeClient::new(tm)?;
let time = client.get_server_time().await?;
println!("Server time: {time}");
let accounts = client.get_accounts().await?;
for acct in &accounts {
println!(
"Account: {} (type={}, status={})",
acct.number, acct.account_type, acct.status
);
}
Ok(())
}