Skip to main content

crypdefi_bot_sdk/
http.rs

1use crate::error::BotSdkError;
2use secp256k1::ecdsa::{self, SerializedSignature};
3use serde::{Deserialize, Serialize};
4
5pub const DEFAULT_URL: &str = "https://api.release.crypdefi.eu";
6fn set_url(base_url: &str, endpoint: &str) -> String {
7    let joinded_url: String = format!("{base_url}{endpoint}");
8
9    joinded_url
10}
11
12#[derive(Serialize)]
13pub enum AuthMethod {
14    #[serde(rename(serialize = "cra"))]
15    Cra,
16}
17#[derive(Serialize)]
18pub struct LoginRequest {
19    pub user_id: String,
20    pub auth_method: String,
21}
22
23#[derive(Deserialize, Serialize, Debug)]
24pub struct LoginResponse {
25    pub challenge: String,
26    pub nonce: String,
27    pub timestamp: i64,
28    pub deadline: usize,
29}
30
31/// makes login call to the crypdefi servers
32pub async fn login(
33    client: &reqwest::Client,
34    req: LoginRequest,
35    base_url: &str,
36) -> Result<LoginResponse, BotSdkError> {
37    let url = set_url(base_url, "/auth/login");
38    let response = client.post(url).json(&req).send().await?;
39
40    if response.status().is_success() {
41        let res = response.json::<LoginResponse>().await?;
42        return Ok(res);
43    }
44
45    let status = response.status();
46    let res = response.text().await?;
47
48    Err(BotSdkError::Custom(format!(
49        "{}.\n Status: {} \n Body: {}",
50        "Could not make login request", status, res
51    )))
52}
53
54pub async fn warmup_connection(client: &reqwest::Client, url: &str) -> Result<(), BotSdkError> {
55    let url = set_url(url, "/health");
56    let response = client.get(url).send().await?;
57
58    if response.status().is_success() {
59        return Ok(());
60    }
61
62    let status = response.status();
63    let res: String = response.text().await?;
64
65    Err(BotSdkError::Custom(format!(
66        "{}.\n Status: {} \n Body: {}",
67        "Could not warmup connection", status, res
68    )))
69}
70
71#[derive(Serialize)]
72pub enum HashAlgo {
73    #[serde(rename(serialize = "sha256"))]
74    Sha256,
75}
76
77#[derive(Serialize)]
78pub struct CraRequest {
79    pub user_id: String,
80    pub challenge: String,
81    pub response: String,
82    pub hash_algorithm: HashAlgo,
83}
84
85#[derive(Deserialize, Debug)]
86pub struct CraResponse {
87    pub token: String,
88    pub refresh_token: String,
89    pub expires_at: u64,
90    pub seconds: u64,
91}
92
93/// makes the cra login auth call to servers
94pub async fn cra_login(
95    client: &reqwest::Client,
96    req: CraRequest,
97    base_url: &str,
98) -> Result<CraResponse, BotSdkError> {
99    let url = set_url(base_url, "/auth/cra");
100    let response = client.post(url).json(&req).send().await?;
101
102    if response.status().is_success() {
103        let res = response.json::<CraResponse>().await?;
104        return Ok(res);
105    }
106
107    let status = response.status();
108    let res = response.text().await?;
109
110    Err(BotSdkError::Custom(format!(
111        "{}.\n Status: {} \n Body: {}",
112        "Could not make CRA login", status, res
113    )))
114}
115
116#[derive(Deserialize, Debug, Clone, Serialize)]
117pub enum WalletState {
118    #[serde(rename = "active")]
119    Active,
120    #[serde(rename = "inactive")]
121    Inactive,
122}
123
124#[allow(dead_code)]
125#[derive(Deserialize, Clone, Debug)]
126pub struct ChainSimple {
127    /** The ID of the chain */
128    pub chain_id: String,
129    /** The common name of the chain */
130    pub name: String,
131}
132
133#[allow(dead_code)]
134#[derive(Deserialize, Clone, Debug)]
135pub struct Wallet {
136    /** The id of the wallet */
137    pub wallet_id: String,
138    /** The friendly name of the wallet */
139    pub name: String,
140    /** The date when this wallet was created */
141    pub created_at: String,
142    /** The status of the wallet */
143    pub state: WalletState,
144
145    pub address: String,
146    pub chain: ChainSimple,
147}
148
149/// makes call to get wallets from backend for user.
150pub async fn get_wallets(
151    client: &reqwest::Client,
152    access_token: &str,
153    base_url: &str,
154) -> Result<Vec<Wallet>, BotSdkError> {
155    let url = set_url(base_url, "/wallets?accessOnly=true");
156
157    let response = client.get(url).bearer_auth(access_token).send().await?;
158
159    if response.status().is_success() {
160        let res = response.json::<Vec<Wallet>>().await?;
161        return Ok(res);
162    }
163
164    let status = response.status();
165    let res = response.text().await?;
166
167    Err(BotSdkError::Custom(format!(
168        "{}.\n Status: {} \n Body: {}",
169        "Could not get wallets", status, res
170    )))
171}
172
173#[derive(Serialize, Deserialize, Debug)]
174pub struct SignRequest {
175    pub kind: SignatureRequestKind,
176    pub data: String,
177    // /** The hashed bytes that were or should be signed */
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub raw_bytes: Option<String>,
180}
181
182#[allow(dead_code)]
183#[derive(Deserialize, Clone, Debug)]
184pub struct Signature {
185    pub r: String,
186    pub s: String,
187    pub recovery_id: Option<u64>,
188}
189
190impl Signature {
191    pub fn to_der(&self) -> Result<SerializedSignature, BotSdkError> {
192        let mut data = [0u8; 64];
193        const_hex::decode_to_slice(&self.r, &mut data[..32])?;
194        const_hex::decode_to_slice(&self.s, &mut data[32..])?;
195
196        let sig = ecdsa::Signature::from_compact(&data)?;
197        Ok(sig.serialize_der())
198    }
199}
200
201#[derive(Deserialize, Debug, Clone, Serialize)]
202pub enum KeyAlgorithm {
203    #[serde(rename = "ECDSA_SECP256k1")]
204    EcdsaSecp256k1,
205    #[serde(rename = "EDDSA_ED25519")]
206    EddsaEd25519,
207}
208
209#[allow(dead_code)]
210#[derive(Deserialize, Clone, Debug)]
211pub struct PublicKey {
212    pub algorithm: KeyAlgorithm,
213    pub public_key: String,
214}
215
216#[allow(dead_code)]
217#[derive(Deserialize, Debug)]
218pub struct SigResponse {
219    pub payload: SignRequest,
220    pub key: PublicKey,
221    pub signature: Signature,
222}
223
224#[derive(Deserialize, Debug, Clone, Serialize)]
225#[serde(rename_all = "snake_case")]
226pub enum SignatureRequestKind {
227    Raw,
228    Transaction,
229    Message,
230    RdxAuthenticationRequest,
231    EvmEip712,
232    CantonTopology,
233}
234
235/// Request signature from keyvault passing hex transactions
236pub async fn sign(
237    client: &reqwest::Client,
238    access_token: &str,
239    wallet_id: &str,
240    tx_type: SignatureRequestKind,
241    hex: String,
242    base_url: &str,
243) -> Result<SigResponse, BotSdkError> {
244    let url = set_url(base_url, &format!("/wallets/{wallet_id}/sign"));
245
246    let req = SignRequest {
247        kind: tx_type,
248        data: hex,
249        raw_bytes: None,
250    };
251
252    let response = client
253        .post(url)
254        .json(&req)
255        .bearer_auth(access_token)
256        .send()
257        .await?;
258
259    if response.status().is_success() {
260        let res = response.json::<SigResponse>().await?;
261        return Ok(res);
262    }
263
264    let status = response.status();
265    let res = response.text().await?;
266
267    Err(BotSdkError::Custom(format!(
268        "{}.\n Status: {} \n Body: {}",
269        "Could not get signature", status, res
270    )))
271}
272
273/// Request signature from keyvault passing hex transactions
274pub async fn logout(
275    client: &reqwest::Client,
276    access_token: &str,
277    base_url: &str,
278) -> Result<(), BotSdkError> {
279    let url = set_url(base_url, "/auth/logout");
280
281    let response = client.post(url).bearer_auth(access_token).send().await?;
282
283    if response.status().is_success() {
284        return Ok(());
285    }
286    let status = response.status();
287    let res = response.text().await?;
288
289    Err(BotSdkError::Custom(format!(
290        "{}.\n Status: {} \n Body: {}",
291        "Could not logout", status, res
292    )))
293}
294
295#[derive(Serialize, Debug)]
296struct RefreshRequest {
297    refresh_token: String,
298}
299
300/// Make call to try to refresh the access token
301pub async fn refresh_auth(
302    client: &reqwest::Client,
303    refresh_token: &str,
304    access_token: &str,
305    base_url: &str,
306) -> Result<CraResponse, BotSdkError> {
307    let url = set_url(base_url, "/auth/refresh");
308
309    let req_body = RefreshRequest {
310        refresh_token: refresh_token.to_owned(),
311    };
312
313    let response = client
314        .post(url)
315        .json(&req_body)
316        .bearer_auth(access_token)
317        .send()
318        .await?;
319    if response.status().is_success() {
320        let res = response.json::<CraResponse>().await?;
321        return Ok(res);
322    }
323
324    let status = response.status();
325    let res = response.text().await?;
326
327    Err(BotSdkError::Custom(format!(
328        "{}.\n Status: {} \n Body: {}",
329        "Could not refresh auth", status, res
330    )))
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_signature_to_der() {
339        let signature = Signature {
340            r: "bc87e27ae505b41bab7f228a60205f61756b3f5ba67ad33fd35664731720a0c9".to_string(),
341            s: "4a28dc564d87e36871d032a208ccef1229a8c95875928f867d77daa21991ca2e".to_string(),
342            recovery_id: Some(0),
343        };
344
345        let der_result = signature.to_der().unwrap();
346        let der_hex = const_hex::encode(&der_result);
347
348        let expected_der_hex = "3045022100bc87e27ae505b41bab7f228a60205f61756b3f5ba67ad33fd35664731720a0c902204a28dc564d87e36871d032a208ccef1229a8c95875928f867d77daa21991ca2e";
349
350        assert_eq!(der_hex, expected_der_hex);
351    }
352    #[test]
353    fn test_signature_to_der_2() {
354        let signature = Signature {
355            r: "d12c949a67ebdaf38350fda2b6d4e0e2bb8a8c8160cfbc6487a3719e2187dd02".to_string(),
356            s: "18f9b3eb182ef670832698f57dc663ddf0501e202aa90865d13abe12e2cd7559".to_string(),
357            recovery_id: Some(1),
358        };
359
360        let der_result = signature.to_der().unwrap();
361        let der_hex = const_hex::encode(&der_result);
362
363        let expected_der_hex = "3045022100d12c949a67ebdaf38350fda2b6d4e0e2bb8a8c8160cfbc6487a3719e2187dd02022018f9b3eb182ef670832698f57dc663ddf0501e202aa90865d13abe12e2cd7559";
364
365        assert_eq!(der_hex, expected_der_hex);
366    }
367}