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}