use base64::Engine;
use chrono::Utc;
use openssl::{
hash::MessageDigest,
rsa::Padding,
sign::{RsaPssSaltlen, Signer},
};
use reqwest::header::{HeaderMap, HeaderValue};
use crate::kalshi_error::KalshiError;
use crate::Kalshi;
impl Kalshi {
pub async fn logout(&self) -> Result<(), KalshiError> {
Ok(())
}
pub(crate) async fn signed_get<T: serde::de::DeserializeOwned>(
&self,
path: &str,
) -> Result<T, KalshiError> {
self.signed_request::<(), T>("GET", path, None).await
}
pub(crate) async fn signed_post<B: serde::Serialize, T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> Result<T, KalshiError> {
self.signed_request("POST", path, Some(body)).await
}
pub(crate) async fn signed_delete<T: serde::de::DeserializeOwned>(
&self,
path: &str,
) -> Result<T, KalshiError> {
self.signed_request::<(), T>("DELETE", path, None).await
}
pub(crate) async fn signed_delete_with_body<
B: serde::Serialize,
T: serde::de::DeserializeOwned,
>(
&self,
path: &str,
body: &B,
) -> Result<T, KalshiError> {
self.signed_request("DELETE", path, Some(body)).await
}
pub(crate) async fn signed_put<B: serde::Serialize, T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: Option<&B>,
) -> Result<T, KalshiError> {
self.signed_request("PUT", path, body).await
}
async fn signed_request<B: serde::Serialize, T: serde::de::DeserializeOwned>(
&self,
method: &str,
path: &str,
body: Option<&B>,
) -> Result<T, KalshiError> {
let key_id = &self.key_id;
let pkey = &self.private_key;
let ts_ms = Utc::now().timestamp_millis();
let path_without_query = path.split('?').next().unwrap_or(path);
let message = format!("{ts_ms}{method}/trade-api/v2{path_without_query}");
let mut signer = Signer::new(MessageDigest::sha256(), pkey)?;
signer.set_rsa_padding(Padding::PKCS1_PSS)?;
signer.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH)?;
signer.update(message.as_bytes())?;
let sig_raw = signer.sign_to_vec()?;
let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig_raw);
let url = format!("{}{}", self.base_url, path);
let mut headers = HeaderMap::with_capacity(3);
headers.insert("KALSHI-ACCESS-KEY", HeaderValue::from_str(key_id)?);
headers.insert("KALSHI-ACCESS-TIMESTAMP", HeaderValue::from(ts_ms));
headers.insert("KALSHI-ACCESS-SIGNATURE", HeaderValue::from_str(&sig_b64)?);
let builder = match method {
"GET" => self.client.get(&url),
"POST" => self.client.post(&url),
"PUT" => self.client.put(&url),
"DELETE" => self.client.delete(&url),
other => self.client.request(other.parse()?, &url),
}
.headers(headers);
let resp = if let Some(b) = body {
builder.json(b).send().await?
} else {
builder.send().await?
};
let status = resp.status();
if !status.is_success() {
let body_text = resp.text().await.unwrap_or_default();
if status.as_u16() == 401 {
return Err(KalshiError::Auth(format!(
"Authentication failed (401): {}. Check your API key and ensure the private key matches.",
body_text
)));
} else if status.is_client_error() {
return Err(KalshiError::UserInputError(format!(
"Request failed with status {}: {}",
status, body_text
)));
} else {
return Err(KalshiError::InternalError(format!(
"Server error {}: {}",
status, body_text
)));
}
}
Ok(resp.json::<T>().await?)
}
}