use std::sync::{Arc, RwLock};
use thiserror::Error;
use tokio::sync::oneshot;
use tonic::{
metadata::errors::InvalidMetadataValue,
service::{Interceptor, interceptor::InterceptedService},
transport::{Channel, ClientTlsConfig},
};
use crate::proto::grpc::tradeapi::v1::{
accounts::accounts_service_client::AccountsServiceClient,
assets::assets_service_client::AssetsServiceClient,
auth::{AuthRequest, auth_service_client::AuthServiceClient},
marketdata::market_data_service_client::MarketDataServiceClient,
orders::orders_service_client::OrdersServiceClient,
};
pub mod proto;
const SOURCE_APP_ID: &str = "https://github.com/artemevsevev/finam";
pub type FinamAccountsServiceClient =
AccountsServiceClient<InterceptedService<Channel, FinamSdkInterceptor>>;
pub type FinamAssetsServiceClient =
AssetsServiceClient<InterceptedService<Channel, FinamSdkInterceptor>>;
pub type FinamAuthServiceClient =
AuthServiceClient<InterceptedService<Channel, FinamSdkInterceptor>>;
pub type FinamMarketDataServiceClient =
MarketDataServiceClient<InterceptedService<Channel, FinamSdkInterceptor>>;
pub type FinamOrdersServiceClient =
OrdersServiceClient<InterceptedService<Channel, FinamSdkInterceptor>>;
#[derive(Clone, Debug)]
pub struct FinamSdk {
accounts: FinamAccountsServiceClient,
assets: FinamAssetsServiceClient,
auth: FinamAuthServiceClient,
market_data: FinamMarketDataServiceClient,
orders: FinamOrdersServiceClient,
}
impl FinamSdk {
pub async fn new(secret: &str) -> Result<Self, FinamSdkError> {
let tls = ClientTlsConfig::new().with_native_roots();
let channel = Channel::from_static("https://api.finam.ru")
.tls_config(tls)?
.connect()
.await?;
let interceptor = FinamSdkInterceptor::new(secret, channel.clone()).await?;
Ok(Self {
accounts: AccountsServiceClient::with_interceptor(channel.clone(), interceptor.clone()),
assets: AssetsServiceClient::with_interceptor(channel.clone(), interceptor.clone()),
auth: AuthServiceClient::with_interceptor(channel.clone(), interceptor.clone()),
market_data: MarketDataServiceClient::with_interceptor(
channel.clone(),
interceptor.clone(),
),
orders: OrdersServiceClient::with_interceptor(channel.clone(), interceptor.clone()),
})
}
pub fn accounts(&self) -> FinamAccountsServiceClient {
self.accounts.clone()
}
pub fn assets(&self) -> FinamAssetsServiceClient {
self.assets.clone()
}
pub fn auth(&self) -> FinamAuthServiceClient {
self.auth.clone()
}
pub fn market_data(&self) -> FinamMarketDataServiceClient {
self.market_data.clone()
}
pub fn orders(&self) -> FinamOrdersServiceClient {
self.orders.clone()
}
}
struct ShutdownGuard {
sender: Option<oneshot::Sender<()>>,
}
impl std::fmt::Debug for ShutdownGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShutdownGuard").finish_non_exhaustive()
}
}
impl Drop for ShutdownGuard {
fn drop(&mut self) {
if let Some(sender) = self.sender.take() {
let _ = sender.send(()); }
}
}
#[derive(Clone, Debug)]
pub struct FinamSdkInterceptor {
jwt_token: Arc<RwLock<String>>,
#[allow(dead_code)]
shutdown_guard: Arc<ShutdownGuard>,
}
impl FinamSdkInterceptor {
pub async fn new(secret: &str, channel: Channel) -> Result<Self, FinamSdkError> {
let token = Arc::new(RwLock::new(
generate_jwt_token(channel.clone(), secret.to_string()).await?,
));
let secret = secret.to_string();
let updating_token = token.clone();
let (shutdown_sender, mut shutdown_receiver) = oneshot::channel();
tokio::spawn(async move {
loop {
tokio::select! {
_ = &mut shutdown_receiver => {
log::info!("Token refresh task shutting down");
break;
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(60 * 10)) => {
loop {
match generate_jwt_token(channel.clone(), secret.clone()).await {
Ok(value) => match updating_token.write() {
Ok(mut token_guard) => {
*token_guard = value;
break;
}
Err(error) => {
log::error!(
"Failed to write JWT token. Waiting for 5 seconds... {:?}",
error
);
}
},
Err(error) => {
log::error!(
"Failed to generate JWT token. Waiting for 5 seconds... {:?}",
error
);
}
};
tokio::select! {
_ = &mut shutdown_receiver => {
log::info!("Token refresh task shutting down during retry");
return;
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {}
}
}
}
}
}
});
Ok(Self {
jwt_token: token,
shutdown_guard: Arc::new(ShutdownGuard {
sender: Some(shutdown_sender),
}),
})
}
fn get_jwt_token(&self) -> Result<String, tonic::Status> {
Ok(self
.jwt_token
.read()
.map_err(|_| tonic::Status::internal("Can't read JWT token"))?
.clone())
}
}
impl Interceptor for FinamSdkInterceptor {
fn call(
&mut self,
mut request: tonic::Request<()>,
) -> Result<tonic::Request<()>, tonic::Status> {
let jwt_token = self
.get_jwt_token()?
.parse()
.map_err(|_| tonic::Status::internal("Invalid JWT token"))?;
request.metadata_mut().append("authorization", jwt_token);
Ok(request)
}
}
async fn generate_jwt_token(channel: Channel, secret: String) -> Result<String, FinamSdkError> {
let mut auth_service_client = AuthServiceClient::new(channel);
let response = auth_service_client
.auth(AuthRequest {
secret,
source_app_id: SOURCE_APP_ID.to_string(),
})
.await?
.into_inner();
Ok(response.token)
}
#[derive(Error, Debug)]
pub enum FinamSdkError {
#[error(transparent)]
Transport(#[from] tonic::transport::Error),
#[error(transparent)]
Status(#[from] tonic::Status),
#[error(transparent)]
InvalidMetadataValue(#[from] InvalidMetadataValue),
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::time::{Duration, sleep};
#[tokio::test]
async fn test_token_refresh_shutdown() {
let _ = env_logger::try_init();
let task_running = Arc::new(AtomicBool::new(true));
let task_running_clone = task_running.clone();
let _token = Arc::new(RwLock::new("initial_token".to_string()));
let (shutdown_sender, mut shutdown_receiver) = oneshot::channel();
let background_task = tokio::spawn(async move {
loop {
tokio::select! {
_ = &mut shutdown_receiver => {
log::info!("Test token refresh task shutting down");
task_running_clone.store(false, Ordering::SeqCst);
break;
}
_ = sleep(Duration::from_millis(100)) => {
log::debug!("Test token refresh tick");
}
}
}
});
assert!(task_running.load(Ordering::SeqCst));
sleep(Duration::from_millis(50)).await;
assert!(task_running.load(Ordering::SeqCst));
let _ = shutdown_sender.send(());
let _ = background_task.await;
assert!(!task_running.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_interceptor_drop_triggers_shutdown() {
let _ = env_logger::try_init();
let task_completed = Arc::new(AtomicBool::new(false));
let task_completed_clone = task_completed.clone();
{
let token = Arc::new(RwLock::new("test_token".to_string()));
let (shutdown_sender, mut shutdown_receiver) = oneshot::channel();
tokio::spawn(async move {
tokio::select! {
_ = &mut shutdown_receiver => {
log::info!("Shutdown signal received in test");
task_completed_clone.store(true, Ordering::SeqCst);
}
_ = sleep(Duration::from_secs(10)) => {
log::error!("Test task timed out waiting for shutdown signal");
}
}
});
let interceptor = FinamSdkInterceptor {
jwt_token: token,
shutdown_guard: Arc::new(ShutdownGuard {
sender: Some(shutdown_sender),
}),
};
sleep(Duration::from_millis(50)).await;
drop(interceptor);
}
sleep(Duration::from_millis(100)).await;
assert!(task_completed.load(Ordering::SeqCst));
}
}