nautilus_coinbase_intx/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 aws_lc_rs::hmac;
17use base64::prelude::*;
18use ustr::Ustr;
19
20/// Coinbase International API credentials for signing requests.
21///
22/// Uses HMAC SHA256 for request signing as per API specifications.
23#[derive(Debug, Clone)]
24pub struct Credential {
25    pub api_key: Ustr,
26    pub api_passphrase: Ustr,
27    api_secret: Vec<u8>,
28}
29
30impl Credential {
31    /// Creates a new [`Credential`] instance.
32    ///
33    /// # Panics
34    ///
35    /// Panics if the provided `api_secret` is not valid base64.
36    #[must_use]
37    pub fn new(api_key: String, api_secret: String, api_passphrase: String) -> Self {
38        let decoded_secret = BASE64_STANDARD
39            .decode(api_secret)
40            .expect("Invalid base64 secret key");
41
42        Self {
43            api_key: api_key.into(),
44            api_passphrase: api_passphrase.into(),
45            api_secret: decoded_secret,
46        }
47    }
48
49    /// Signs a request message according to the Coinbase authentication scheme.
50    ///
51    /// # Panics
52    ///
53    /// Panics if signature generation fails due to key or cryptographic errors.
54    pub fn sign(&self, timestamp: &str, method: &str, endpoint: &str, body: &str) -> String {
55        // Extract the path without query parameters
56        let request_path = match endpoint.find('?') {
57            Some(index) => &endpoint[..index],
58            None => endpoint,
59        };
60
61        let message = format!("{timestamp}{method}{request_path}{body}");
62        tracing::trace!("Signing message: {message}");
63
64        let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
65        let tag = hmac::sign(&key, message.as_bytes());
66        BASE64_STANDARD.encode(tag.as_ref())
67    }
68
69    /// Signs a WebSocket authentication message.
70    ///
71    /// # Panics
72    ///
73    /// Panics if signature generation fails due to key or cryptographic errors.
74    pub fn sign_ws(&self, timestamp: &str) -> String {
75        let message = format!("{timestamp}{}CBINTLMD{}", self.api_key, self.api_passphrase);
76        tracing::trace!("Signing message: {message}");
77
78        let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
79        let tag = hmac::sign(&key, message.as_bytes());
80        BASE64_STANDARD.encode(tag.as_ref())
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use rstest::rstest;
87
88    use super::*;
89
90    const API_KEY: &str = "test_key_123";
91    const API_SECRET: &str = "dGVzdF9zZWNyZXRfYmFzZTY0"; // base64 encoded "test_secret_base64"
92    const API_PASSPHRASE: &str = "test_pass";
93
94    #[rstest]
95    fn test_simple_get() {
96        let credential = Credential::new(
97            API_KEY.to_string(),
98            API_SECRET.to_string(),
99            API_PASSPHRASE.to_string(),
100        );
101        let timestamp = "1641890400"; // 2022-01-11T00:00:00Z
102        let signature = credential.sign(timestamp, "GET", "/api/v1/fee-rate-tiers", "");
103
104        assert_eq!(signature, "h/9tnYzD/nsEbH1sV7dkB5uJ3Vygr4TjmOOxJNQB8ts=");
105    }
106}