Skip to content
This repository was archived by the owner on Feb 16, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 233 additions & 75 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ rust-version = "1.85.0"
publish = false

[dependencies]
async-trait = "0.1"
base64 = "0.22"
chrono = { version = "0.4", features = ["serde"] }
oauth2 = { git = "https://github.com/ramosbugs/oauth2-rs", tag = "5.0.0" }
p256 = { version = "0.13", features = ["pem", "pkcs8"] }
reqwest = { version = "0.12", default-features = false, features = ["http2", "json", "rustls-tls"] }
ring = "0.17"
Expand Down
83 changes: 74 additions & 9 deletions src/app/agent.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
use std::sync::Arc;
use std::time::Duration;

use oauth2::basic::{
BasicClient, BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse,
BasicTokenResponse,
};
use oauth2::{AuthUrl, EndpointNotSet, EndpointSet, RedirectUrl, StandardRevocableToken, TokenUrl};
use reqwest::header::{CONTENT_TYPE, HeaderValue, USER_AGENT};
use reqwest::{Client, Method, Response};
use url::Url;

use super::auth::CoinbaseAuth;
use super::auth::jwt::Jwt;
use super::constant::{API_ROOT_URL, API_SANDBOX_URL, CB_VERSION, USER_AGENT_NAME};
use super::constant::{
API_ROOT_URL, API_SANDBOX_URL, CB_VERSION, OAUTH_ACCESS_TOKEN_URL, OAUTH_AUTHORIZE_URL,
USER_AGENT_NAME,
};
use super::error::Error;
use super::oauth::{CoinbaseAppOAuth2Callback, CoinbaseOAuth2Token};

#[derive(Debug, Clone)]
enum Authenticator {
None,
Jwt(Jwt),
OAuth2 {
client: oauth2::Client<
BasicErrorResponse,
BasicTokenResponse,
BasicTokenIntrospectionResponse,
StandardRevocableToken,
BasicRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointSet,
>,
token: Arc<Mutex<Option<CoinbaseOAuth2Token>>>
},
}

#[derive(Debug, Clone)]
struct HttpClientAgent {
Expand Down Expand Up @@ -87,31 +118,65 @@ impl HttpClientAgent {

#[derive(Debug, Clone)]
pub struct SecureHttpClientAgent {
/// JWT generator, disabled in sandbox mode.
jwt: Option<Jwt>,
/// Authenticator
authenticator: Authenticator,
/// OAuth2 callback
oauth2_callback: Option<Arc<dyn CoinbaseAppOAuth2Callback>>,
/// Base client that is responsible for making the requests.
base: HttpClientAgent,
}

impl SecureHttpClientAgent {
pub(super) fn new(auth: CoinbaseAuth, sandbox: bool, timeout: Duration) -> Result<Self, Error> {
let jwt: Option<Jwt> = match auth {
CoinbaseAuth::None => None,
pub(super) fn new(
auth: CoinbaseAuth,
sandbox: bool,
timeout: Duration,
oauth2_callback: Option<Arc<dyn CoinbaseAppOAuth2Callback>>,
) -> Result<Self, Error> {
let authenticator: Authenticator = match auth {
CoinbaseAuth::None => Authenticator::None,
CoinbaseAuth::ApiKeys {
api_key,
secret_key,
} => {
// Do not generate JWT in sandbox mode.
if sandbox {
None
Authenticator::None
} else {
Some(Jwt::new(api_key, secret_key)?)
Authenticator::Jwt(Jwt::new(api_key, secret_key)?)
}
}
CoinbaseAuth::OAuth {
client_id,
client_secret,
redirect_url,
token,
} => {
let auth_url: Url = Url::parse(OAUTH_AUTHORIZE_URL)?;
let auth_url: AuthUrl = AuthUrl::from_url(auth_url);

let token_url: Url = Url::parse(OAUTH_ACCESS_TOKEN_URL)?;
let token_url: TokenUrl = TokenUrl::from_url(token_url);

let redirect_url: RedirectUrl = RedirectUrl::from_url(redirect_url);

let client = BasicClient::new(client_id)
.set_client_secret(client_secret)
.set_auth_uri(auth_url)
.set_token_uri(token_url)
.set_redirect_uri(redirect_url);

//client.exchange_refresh_token()

//client.exchange_code().request_async()

Authenticator::OAuth2 { client, token: Arc::new(Mutex::new(token)) }
}
};

Ok(Self {
jwt,
authenticator,
oauth2_callback,
base: HttpClientAgent::new(sandbox, timeout)?,
})
}
Expand Down
16 changes: 16 additions & 0 deletions src/app/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

use std::fmt;

use oauth2::{ClientId, ClientSecret};
use url::Url;

pub(super) mod jwt;

use crate::app::oauth::CoinbaseOAuth2Token;

/// Coinbase authentication
#[derive(Clone, Default)]
pub enum CoinbaseAuth {
Expand All @@ -19,6 +24,17 @@ pub enum CoinbaseAuth {
/// Secret Key
secret_key: String,
},
/// OAuth2
OAuth {
/// Client ID
client_id: ClientId,
/// Client secret
client_secret: ClientSecret,
/// Redirect URL
redirect_url: Url,
/// Token
token: Option<CoinbaseOAuth2Token>,
},
}

impl fmt::Debug for CoinbaseAuth {
Expand Down
17 changes: 17 additions & 0 deletions src/app/builder.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
//! Coinbase App client builder

use std::sync::Arc;
use std::time::Duration;

use super::auth::CoinbaseAuth;
use super::client::CoinbaseAppClient;
use super::error::Error;
use super::oauth::CoinbaseAppOAuth2Callback;

/// Coinbase App client builder
#[derive(Debug, Clone)]
pub struct CoinbaseAppClientBuilder {
/// Authentication
pub auth: CoinbaseAuth,
/// OAuth2 callback
///
/// This is needed for receiving the refreshed token
pub oauth_callback: Option<Arc<dyn CoinbaseAppOAuth2Callback>>,
/// Use sandbox APIs
pub sandbox: bool,
/// Requests timeout
Expand All @@ -21,6 +27,7 @@ impl Default for CoinbaseAppClientBuilder {
fn default() -> Self {
Self {
auth: CoinbaseAuth::default(),
oauth_callback: None,
sandbox: false,
timeout: Duration::from_secs(20),
}
Expand All @@ -35,6 +42,16 @@ impl CoinbaseAppClientBuilder {
self
}

/// Set OAuth2 callback
#[inline]
pub fn oauth2_callback<T>(mut self, callback: T) -> Self
where
T: CoinbaseAppOAuth2Callback + 'static,
{
self.oauth_callback = Some(Arc::new(callback));
self
}

/// Set sandbox APIs
#[inline]
pub fn sandbox(mut self, sandbox: bool) -> Self {
Expand Down
7 changes: 6 additions & 1 deletion src/app/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ impl CoinbaseAppClient {
#[inline]
pub(super) fn from_builder(builder: CoinbaseAppClientBuilder) -> Result<Self, Error> {
Ok(Self {
client: SecureHttpClientAgent::new(builder.auth, builder.sandbox, builder.timeout)?,
client: SecureHttpClientAgent::new(
builder.auth,
builder.sandbox,
builder.timeout,
builder.oauth_callback,
)?,
})
}

Expand Down
3 changes: 3 additions & 0 deletions src/app/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
pub(super) const API_ROOT_URL: &str = "https://api.coinbase.com";
pub(super) const API_SANDBOX_URL: &str = "https://api-sandbox.coinbase.com";

pub(super) const OAUTH_AUTHORIZE_URL: &str = "https://login.coinbase.com/oauth2/auth";
pub(super) const OAUTH_ACCESS_TOKEN_URL: &str = "https://login.coinbase.com/oauth2/token";

/// User Agent for the client
pub(super) const USER_AGENT_NAME: &str = concat!("coinbase-api/", env!("CARGO_PKG_VERSION"));

Expand Down
1 change: 1 addition & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub mod builder;
pub mod client;
mod constant;
pub mod error;
pub mod oauth;
pub mod response;
28 changes: 28 additions & 0 deletions src/app/oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Coinbase App OAuth2 client

use std::fmt::Debug;

use oauth2::{AccessToken, RefreshToken};

// /// Coinbase App OAuth2 refreshed token
// #[derive(Debug, Clone)]
// pub struct CoinbaseOAuth2Token {
// /// The new access token
// pub access_token: AccessToken,
// /// The refresh token
// pub refresh_token: RefreshToken,
// /// The expiration time
// pub expires_at: Option<u64>,
// }

/// Coinbase App OAuth2 callback
#[async_trait::async_trait]
pub trait CoinbaseAppOAuth2Callback: Debug + Send + Sync {
// /// The token has been refreshed
// async fn token_refreshed(
// &self,
// token: CoinbaseOAuth2Token,
// ) -> Result<(), Box<dyn std::error::Error>>;

async fn get_access_token(&self) -> Result<AccessToken, Box<dyn std::error::Error>>;
}
Loading