Skip to content

Latest commit

 

History

History
178 lines (129 loc) · 7.17 KB

File metadata and controls

178 lines (129 loc) · 7.17 KB

Authentication

Every SDK request (except a handful of public endpoints) requires an OAuth2 bearer token:

Authorization: Bearer <40-char token>

Obtaining a token

  1. Log in to cryptohopper.com.
  2. Developer → Create App — gives you a client_id + client_secret.
  3. Complete the OAuth consent flow for your app, which returns a bearer token.

Options to automate step 3:

  • The official CLI: cryptohopper login opens the consent page, runs a loopback listener, and persists the token to ~/.cryptohopper/config.json. Read it from your Rust binary.
  • Your own code: call the server's /oauth2/authorize + /oauth2/token endpoints directly. The CLI's implementation is short (~300 lines of TypeScript) and a reasonable reference.

Client construction

use std::time::Duration;
use cryptohopper::Client;

let client = Client::builder()
    .api_key(std::env::var("CRYPTOHOPPER_TOKEN")?)
    .app_key(std::env::var("CRYPTOHOPPER_APP_KEY").unwrap_or_default())
    .base_url("https://api.cryptohopper.com/v1")
    .timeout(Duration::from_secs(30))
    .max_retries(3)
    .user_agent("my-app/1.0")
    .build()?;

Client::new(api_key) is a shortcut for the most common case (defaults for everything else). Use the builder when you need to set anything beyond the token.

.app_key(...)

Cryptohopper lets OAuth apps identify themselves on every request via the x-api-app-key header (value = your OAuth client_id). When set, the SDK adds the header automatically. Reasons to set it:

  • Shows up in Cryptohopper's server-side telemetry — you can attribute your own traffic.
  • Drives per-app rate limits — if two apps share a token, they get independent quotas.
  • Harmless to omit; the server accepts unattributed requests.

Empty strings are treated as "not set," so passing unwrap_or_default() from a missing env var is safe.

.base_url(...)

Override for staging or a local dev server. The default is https://api.cryptohopper.com/v1. The trailing /v1 is part of the base; resource paths are relative to it.

.http_client(reqwest::Client)

Bring your own reqwest::Client for proxies, custom CA bundles, connection-pool tuning, or tower-based middleware:

let custom = reqwest::Client::builder()
    .proxy(reqwest::Proxy::http("http://corporate-proxy.internal:3128")?)
    .danger_accept_invalid_certs(false)  // keep TLS verification ON; supply a custom root if needed
    .timeout(Duration::from_secs(30))
    .build()?;

let client = Client::builder()
    .api_key(token)
    .http_client(custom)
    .build()?;

When you supply your own reqwest::Client, the .timeout(...) builder option on the Cryptohopper client is overridden — your reqwest::Client controls the per-request connect/read/write timeout. The body-read timeout the SDK applies on top of resp.text() still uses the value passed via .timeout(...) on the builder; pair the two so the body-read timeout isn't tighter than your reqwest timeout.

For rustls vs OpenSSL: the SDK's default-built reqwest client uses rustls + webpki-roots (mozilla CA bundle). To use your OS cert store, supply your own client built with rustls-native-certs:

let custom = reqwest::Client::builder()
    // ... build with rustls-native-certs feature enabled ...
    .build()?;

.timeout(...) and .max_retries(...)

.timeout(Duration) — per-request total timeout. Applied to the connect + headers phase by reqwest, AND wrapped around the body read by the SDK (reqwest::Client::timeout only covers up to response headers; the body read needs a separate tokio::time::timeout to honour the same deadline). Defaults to 30 seconds.

.max_retries(u32) — automatic retries on HTTP 429. Default 3. Set to 0 to disable. See Rate Limits for details.

IP allowlisting

If your Cryptohopper app has IP allowlisting enabled, requests from unlisted IPs return 403 FORBIDDEN. The SDK surfaces this as cryptohopper::Error with code == ErrorCode::Forbidden and ip_address populated:

use cryptohopper::ErrorCode;

if let Err(err) = client.hoppers.list().await {
    if matches!(err.code, ErrorCode::Forbidden) {
        eprintln!("blocked: caller IP was {:?}", err.ip_address);
    }
}

For CI where the runner IP isn't stable, either disable IP allowlisting for that app or route outbound traffic through a stable IP (NAT gateway, VPN, dedicated proxy).

Rotating tokens

Cryptohopper bearer tokens are long-lived but can be revoked:

  • Manually from the dashboard.
  • When the user revokes consent.

The SDK surfaces revocation as Unauthorized on the next call. There is no automatic refresh-token handling in the SDK today — if your app uses refresh tokens, handle the Unauthorized branch by exchanging your refresh token for a new access token and constructing a fresh client. Use a tokio::sync::RwLock (or arc_swap::ArcSwap for higher concurrency) to swap the client atomically:

use std::sync::Arc;
use tokio::sync::RwLock;
use cryptohopper::{Client, ErrorCode};

#[derive(Clone)]
pub struct AutoRefresh {
    inner: Arc<RwLock<Client>>,
}

impl AutoRefresh {
    pub async fn call<T, F, Fut>(&self, f: F) -> Result<T, cryptohopper::Error>
    where
        F: Fn(Client) -> Fut,
        Fut: std::future::Future<Output = Result<T, cryptohopper::Error>>,
    {
        let snapshot = self.inner.read().await.clone();
        match f(snapshot).await {
            Ok(v) => Ok(v),
            Err(e) if matches!(e.code, ErrorCode::Unauthorized) => {
                let new_token = refresh_token().await?;
                let new_client = Client::new(new_token)?;
                {
                    let mut w = self.inner.write().await;
                    *w = new_client.clone();
                }
                f(new_client).await
            }
            Err(e) => Err(e),
        }
    }
}

Client is cheaply cloneable, so swapping is safe. In-flight requests on the old token complete with Unauthorized and trigger their own refresh on retry — there's no shared state to invalidate.

Concurrency

Client is Send + Sync + Clone. One client serving many tokio tasks is fine. The underlying reqwest::Client has its own connection pool which is also thread-safe.

use futures::stream::{FuturesUnordered, StreamExt};

let mut futures = FuturesUnordered::new();
for id in hopper_ids {
    let c = client.clone();
    futures.push(async move { c.hoppers.get(&id).await });
}
while let Some(res) = futures.next().await {
    handle(res);
}

See Rate Limits for guidance on capping concurrency.

Public-only access (no token)

A handful of endpoints accept anonymous calls:

  • /market/* — marketplace browse
  • /platform/* — i18n, country list, blog feed
  • /exchange/ticker, /exchange/candle, /exchange/orderbook, /exchange/markets, /exchange/exchanges, /exchange/forex-rates — public market data

The SDK still requires a non-empty api_key at construction; pass any placeholder if you only intend to hit public endpoints. The server ignores the bearer header on whitelisted routes.

let client = Client::new("anonymous")?;
let btc = client.exchange.ticker("binance", "BTC/USDT").await?;