nautilus_coinbase_intx/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use base64::prelude::*;
17use ring::hmac;
18use ustr::Ustr;
19
20/// Returns the environment variable for the given `key`.
21///
22/// # Errors
23///
24/// Returns an error if the environment variable is not set.
25pub fn get_env_var(key: &str) -> anyhow::Result<String> {
26    match std::env::var(key) {
27        Ok(var) => Ok(var),
28        Err(_) => anyhow::bail!("environment variable '{key}' must be set"),
29    }
30}
31
32/// Coinbase International API credentials for signing requests.
33///
34/// Uses HMAC SHA256 for request signing as per API specifications.
35#[derive(Debug, Clone)]
36pub struct Credential {
37    pub api_key: Ustr,
38    pub api_passphrase: Ustr,
39    hmac_key: hmac::Key,
40}
41
42impl Credential {
43    /// Creates a new [`Credential`] instance.
44    #[must_use]
45    pub fn new(api_key: String, api_secret: String, api_passphrase: String) -> Self {
46        let decoded_secret = BASE64_STANDARD
47            .decode(api_secret)
48            .expect("Invalid base64 secret key");
49
50        Self {
51            api_key: api_key.into(),
52            api_passphrase: api_passphrase.into(),
53            hmac_key: hmac::Key::new(hmac::HMAC_SHA256, &decoded_secret),
54        }
55    }
56
57    /// Signs a request message according to the Coinbase authentication scheme.
58    pub fn sign(&self, timestamp: &str, method: &str, endpoint: &str, body: &str) -> String {
59        // Extract the path without query parameters
60        let request_path = match endpoint.find('?') {
61            Some(index) => &endpoint[..index],
62            None => endpoint,
63        };
64
65        let message = format!("{timestamp}{method}{request_path}{body}");
66        tracing::trace!("Signing message: {message}");
67        let signature = hmac::sign(&self.hmac_key, message.as_bytes());
68        BASE64_STANDARD.encode(signature)
69    }
70
71    pub fn sign_ws(&self, timestamp: &str) -> String {
72        let message = format!("{timestamp}{}CBINTLMD{}", self.api_key, self.api_passphrase);
73        tracing::trace!("Signing message: {message}");
74        let signature = hmac::sign(&self.hmac_key, message.as_bytes());
75        BASE64_STANDARD.encode(signature)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use rstest::rstest;
82
83    use super::*;
84
85    const API_KEY: &str = "test_key_123";
86    const API_SECRET: &str = "dGVzdF9zZWNyZXRfYmFzZTY0"; // base64 encoded "test_secret_base64"
87    const API_PASSPHRASE: &str = "test_pass";
88
89    #[rstest]
90    fn test_simple_get() {
91        let credential = Credential::new(
92            API_KEY.to_string(),
93            API_SECRET.to_string(),
94            API_PASSPHRASE.to_string(),
95        );
96        let timestamp = "1641890400"; // 2022-01-11T00:00:00Z
97        let signature = credential.sign(timestamp, "GET", "/api/v1/fee-rate-tiers", "");
98
99        assert_eq!(signature, "h/9tnYzD/nsEbH1sV7dkB5uJ3Vygr4TjmOOxJNQB8ts=");
100    }
101}