1use base64::{engine::general_purpose, Engine as _};
2use eyre::Result;
3use hmac::{Hmac, Mac};
4use reqwest::Client;
5use serde::Deserialize;
6use sha2::{Digest, Sha256, Sha512};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9type HmacSha512 = Hmac<Sha512>;
10
11pub struct Authenticator {
12 api_key: String,
13 api_secret: String,
14 client: Client,
15}
16
17#[derive(Deserialize)]
18struct TokenResponse {
19 error: Vec<String>,
20 result: Option<TokenResult>,
21}
22
23#[derive(Deserialize)]
24struct TokenResult {
25 token: String,
26}
27
28impl Authenticator {
29 pub fn new(api_key: String, api_secret: String) -> Self {
30 Self {
31 api_key,
32 api_secret,
33 client: Client::new(),
34 }
35 }
36
37 pub async fn get_ws_token(&self) -> Result<String> {
38 let nonce = SystemTime::now()
39 .duration_since(UNIX_EPOCH)?
40 .as_millis()
41 .to_string();
42
43 let path = "/0/private/GetWebSocketsToken";
44 let url = format!("https://api.kraken.com{}", path);
45 let post_data = format!("nonce={}", nonce);
46
47 let signature = sign_request(&self.api_secret, path, &nonce, &post_data)?;
48
49 let resp = self
51 .client
52 .post(&url)
53 .header("API-Key", &self.api_key)
54 .header("API-Sign", signature)
55 .body(post_data)
56 .send()
57 .await?
58 .json::<TokenResponse>()
59 .await?;
60
61 if !resp.error.is_empty() {
62 return Err(eyre::eyre!("Kraken API Error: {:?}", resp.error));
63 }
64
65 Ok(resp.result.unwrap().token)
66 }
67}
68
69pub fn sign_request(api_secret: &str, path: &str, nonce: &str, post_data: &str) -> Result<String> {
70 let mut sha256 = Sha256::new();
72 sha256.update(nonce.as_bytes());
73 sha256.update(post_data.as_bytes());
74 let sha256_digest = sha256.finalize();
75
76 let secret_bytes = general_purpose::STANDARD.decode(api_secret)?;
78 let mut mac = HmacSha512::new_from_slice(&secret_bytes)?;
79 mac.update(path.as_bytes());
80 mac.update(&sha256_digest);
81 let sig_bytes = mac.finalize().into_bytes();
82
83 Ok(general_purpose::STANDARD.encode(sig_bytes))
84}