use std::sync::Arc;
use log::{debug, error, warn};
use time::macros::format_description;
use time::OffsetDateTime;
use time_tz::Tz;
use super::ConnectionMetadata;
use crate::accounts::AccountUpdate;
use crate::common::timezone::{find_timezone, resolve_local};
use crate::errors::Error;
use crate::messages::{
encode_length, encode_protobuf_message, split_message_id, unknown_message_type_notice, IncomingMessages, Notice, OutgoingMessages,
ResponseMessage, CLIENT_ID_IN_USE_CODE, HANDSHAKE_DECODE_FAILURE_CODE, HANDSHAKE_UNKNOWN_FRAME_CODE, PROTOBUF_MSG_ID,
};
use crate::orders::{CommissionReport, ExecutionData, OrderData, OrderStatus};
use crate::server_versions;
use crate::transport::common::NoticeSink;
#[derive(Debug)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub enum StartupMessage {
OpenOrder(OrderData),
OrderStatus(OrderStatus),
OpenOrderEnd,
AccountUpdate(AccountUpdate),
Execution(ExecutionData),
CommissionReport(CommissionReport),
CompletedOrder(OrderData),
ExecutionDataEnd,
CompletedOrdersEnd,
}
impl StartupMessage {
pub fn message_type(&self) -> IncomingMessages {
match self {
StartupMessage::OpenOrder(_) => IncomingMessages::OpenOrder,
StartupMessage::OrderStatus(_) => IncomingMessages::OrderStatus,
StartupMessage::OpenOrderEnd => IncomingMessages::OpenOrderEnd,
StartupMessage::AccountUpdate(au) => match au {
AccountUpdate::AccountValue(_) => IncomingMessages::AccountValue,
AccountUpdate::PortfolioValue(_) => IncomingMessages::PortfolioValue,
AccountUpdate::UpdateTime(_) => IncomingMessages::AccountUpdateTime,
AccountUpdate::End => IncomingMessages::AccountDownloadEnd,
},
StartupMessage::Execution(_) => IncomingMessages::ExecutionData,
StartupMessage::CommissionReport(_) => IncomingMessages::CommissionsReport,
StartupMessage::CompletedOrder(_) => IncomingMessages::CompletedOrder,
StartupMessage::ExecutionDataEnd => IncomingMessages::ExecutionDataEnd,
StartupMessage::CompletedOrdersEnd => IncomingMessages::CompletedOrdersEnd,
}
}
}
pub(crate) struct StartupHandshakeContext<'a> {
pub client_id: i32,
pub startup: Option<&'a (dyn Fn(StartupMessage) + Send + Sync)>,
pub notice_sink: &'a (dyn NoticeSink + Sync),
}
pub(crate) type StartupCallback = Arc<dyn Fn(StartupMessage) + Send + Sync>;
pub(crate) struct ConnectionConfig {
pub(crate) address: String,
pub(crate) client_id: i32,
pub(crate) tcp_no_delay: bool,
pub(crate) startup_callback: Option<StartupCallback>,
pub(crate) max_reconnect_attempts: Option<u32>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct HandshakeData {
pub min_version: i32,
pub max_version: i32,
pub server_version: i32,
pub server_time: String,
}
pub trait ConnectionProtocol {
type Error;
fn format_handshake(&self) -> Vec<u8>;
fn parse_handshake_response(&self, response: &mut ResponseMessage) -> Result<HandshakeData, Self::Error>;
fn format_start_api(&self, client_id: i32) -> Vec<u8>;
fn parse_account_info(&self, message: &ResponseMessage, ctx: &StartupHandshakeContext<'_>) -> Result<AccountInfo, Self::Error>;
}
#[derive(Debug, Clone, Default)]
pub struct AccountInfo {
pub next_order_id: Option<i32>,
pub managed_accounts: Option<String>,
}
pub(crate) const MAX_ACCOUNT_INFO_ATTEMPTS: i32 = 100;
impl AccountInfo {
pub(crate) fn merge(&mut self, other: AccountInfo) -> bool {
if other.next_order_id.is_some() {
self.next_order_id = other.next_order_id;
}
if other.managed_accounts.is_some() {
self.managed_accounts = other.managed_accounts;
}
self.next_order_id.is_some() && self.managed_accounts.is_some()
}
}
impl ConnectionMetadata {
pub(crate) fn apply_account_info(&mut self, info: AccountInfo) {
if let Some(next_order_id) = info.next_order_id {
self.next_order_id = next_order_id;
}
if let Some(managed_accounts) = info.managed_accounts {
self.managed_accounts = managed_accounts;
}
}
}
#[derive(Debug)]
pub struct ConnectionHandler {
pub min_version: i32,
pub max_version: i32,
}
impl Default for ConnectionHandler {
fn default() -> Self {
Self {
min_version: server_versions::PROTOBUF_REST_MESSAGES_3,
max_version: server_versions::CONDITIONS_INCLUDE_OVERNIGHT,
}
}
}
impl ConnectionProtocol for ConnectionHandler {
type Error = Error;
fn format_handshake(&self) -> Vec<u8> {
let version_string = format!("v{}..{}", self.min_version, self.max_version);
debug!("Handshake version: {version_string}");
let mut handshake = Vec::from(b"API\0");
handshake.extend_from_slice(&encode_length(&version_string));
handshake
}
fn parse_handshake_response(&self, response: &mut ResponseMessage) -> Result<HandshakeData, Self::Error> {
let server_version = response.next_int()?;
let server_time = response.next_string()?;
Ok(HandshakeData {
min_version: self.min_version,
max_version: self.max_version,
server_version,
server_time,
})
}
fn format_start_api(&self, client_id: i32) -> Vec<u8> {
use prost::Message;
let request = crate::proto::StartApiRequest {
client_id: Some(client_id),
optional_capabilities: None,
};
encode_protobuf_message(OutgoingMessages::StartApi as i32, &request.encode_to_vec())
}
fn parse_account_info(&self, message: &ResponseMessage, ctx: &StartupHandshakeContext<'_>) -> Result<AccountInfo, Self::Error> {
let mut info = AccountInfo::default();
match message.message_type() {
IncomingMessages::NextValidId => {
let proto: crate::proto::NextValidId = message.proto()?;
info.next_order_id = proto.order_id;
}
IncomingMessages::ManagedAccounts => {
let proto: crate::proto::ManagedAccounts = message.proto()?;
info.managed_accounts = proto.accounts_list;
}
IncomingMessages::Error if Notice::from(message).code == CLIENT_ID_IN_USE_CODE => {
dispatch_unsolicited_message(message, ctx);
return Err(Error::ClientIdInUse { client_id: ctx.client_id });
}
_ => dispatch_unsolicited_message(message, ctx),
}
Ok(info)
}
}
pub(crate) fn dispatch_unsolicited_message(message: &ResponseMessage, ctx: &StartupHandshakeContext<'_>) {
use crate::accounts::common::decode_account_update_message;
use crate::orders::common::{decode_commission_report, decode_completed_order, decode_execution_data, decode_open_order, decode_order_status};
fn dispatch_typed<T>(
ctx: &StartupHandshakeContext<'_>,
kind: IncomingMessages,
decode: impl FnOnce() -> Result<T, Error>,
wrap: impl FnOnce(T) -> StartupMessage,
) {
let Some(cb) = ctx.startup else { return };
match decode() {
Ok(t) => cb(wrap(t)),
Err(e) => ctx.notice_sink.deliver(Notice::synthesized(
HANDSHAKE_DECODE_FAILURE_CODE,
format!("handshake decoder failed for {kind:?}: {e}"),
)),
}
}
fn dispatch_unit(ctx: &StartupHandshakeContext<'_>, msg: StartupMessage) {
if let Some(cb) = ctx.startup {
cb(msg);
}
}
let kind = message.message_type();
match kind {
IncomingMessages::Error => {
let notice = Notice::from(message);
notice.log();
ctx.notice_sink.deliver(notice);
}
IncomingMessages::OpenOrder => dispatch_typed(ctx, kind, || decode_open_order(message), StartupMessage::OpenOrder),
IncomingMessages::OrderStatus => dispatch_typed(ctx, kind, || decode_order_status(message), StartupMessage::OrderStatus),
IncomingMessages::OpenOrderEnd => dispatch_unit(ctx, StartupMessage::OpenOrderEnd),
IncomingMessages::AccountValue
| IncomingMessages::PortfolioValue
| IncomingMessages::AccountUpdateTime
| IncomingMessages::AccountDownloadEnd => dispatch_typed(ctx, kind, || decode_account_update_message(message), StartupMessage::AccountUpdate),
IncomingMessages::ExecutionData => dispatch_typed(ctx, kind, || decode_execution_data(message), StartupMessage::Execution),
IncomingMessages::CommissionsReport => dispatch_typed(ctx, kind, || decode_commission_report(message), StartupMessage::CommissionReport),
IncomingMessages::CompletedOrder => dispatch_typed(ctx, kind, || decode_completed_order(message), StartupMessage::CompletedOrder),
IncomingMessages::ExecutionDataEnd => dispatch_unit(ctx, StartupMessage::ExecutionDataEnd),
IncomingMessages::CompletedOrdersEnd => dispatch_unit(ctx, StartupMessage::CompletedOrdersEnd),
IncomingMessages::NotValid => ctx.notice_sink.deliver(unknown_message_type_notice(message)),
_ => {
warn!("unrouted handshake frame: {kind:?}");
ctx.notice_sink.deliver(Notice::synthesized(
HANDSHAKE_UNKNOWN_FRAME_CODE,
format!("unsolicited handshake frame with no typed variant: {kind:?}"),
));
}
}
}
pub(crate) fn require_protobuf_support(server_version: i32) -> Result<(), Error> {
if server_version < server_versions::PROTOBUF_REST_MESSAGES_3 {
return Err(Error::ServerVersion(
server_versions::PROTOBUF_REST_MESSAGES_3,
server_version,
format!(
"protobuf transport — rust-ibapi 3.x requires TWS or IB Gateway with server version {} or later; please upgrade",
server_versions::PROTOBUF_REST_MESSAGES_3
),
));
}
Ok(())
}
#[derive(Debug)]
pub(crate) struct HandshakeAck {
pub server_version: i32,
pub connection_time: Option<OffsetDateTime>,
pub time_zone: Option<&'static Tz>,
}
pub(crate) fn parse_handshake_ack(handler: &ConnectionHandler, ack: Result<Vec<u8>, Error>) -> Result<HandshakeAck, Error> {
match ack {
Ok(data) => {
let raw_string = String::from_utf8_lossy(&data).into_owned();
let mut response = ResponseMessage::from(&raw_string);
let handshake_data = handler.parse_handshake_response(&mut response)?;
let (connection_time, time_zone) = parse_connection_time(&handshake_data.server_time);
Ok(HandshakeAck {
server_version: handshake_data.server_version,
connection_time,
time_zone,
})
}
Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => Err(Error::ConnectionRejected(format!(
"server may be rejecting connections from this host: {err}"
))),
Err(err) => Err(err),
}
}
pub fn parse_connection_time(connection_time: &str) -> (Option<OffsetDateTime>, Option<&'static Tz>) {
let mut parts = connection_time.splitn(3, ' ');
let (Some(date), Some(time), Some(tz_name)) = (parts.next(), parts.next(), parts.next()) else {
error!("Invalid connection time format: {connection_time}");
return (None, None);
};
let Some(timezone) = find_timezone(tz_name) else {
warn!("{}", Error::UnsupportedTimeZone(tz_name.to_string()));
return (None, None);
};
let format = format_description!("[year][month][day] [hour]:[minute]:[second]");
let date_str = format!("{date} {time}");
let date = time::PrimitiveDateTime::parse(date_str.as_str(), format);
match date {
Ok(connected_at) => (Some(resolve_local(connected_at, timezone)), Some(timezone)),
Err(err) => {
warn!("Could not parse connection time from {date_str}: {err}");
(None, Some(timezone))
}
}
}
pub fn parse_raw_message(data: &[u8]) -> Result<ResponseMessage, Error> {
let Some((msg_id, payload)) = split_message_id(data) else {
return Err(Error::InvalidFrame(format!(
"frame body of {} bytes cannot hold a message id",
data.len()
)));
};
if msg_id > PROTOBUF_MSG_ID {
let real_type = msg_id - PROTOBUF_MSG_ID;
debug!("<- protobuf msg_id={real_type}");
Ok(ResponseMessage::from_protobuf(real_type, payload.to_vec()))
} else {
let raw = String::from_utf8_lossy(payload);
debug!("<- {raw:?}");
let mut fields = vec![msg_id.to_string()];
fields.extend(raw.split_terminator('\0').map(|s| s.to_string()));
Ok(ResponseMessage::from_text_fields(fields))
}
}
#[cfg(test)]
#[path = "common_tests.rs"]
mod tests;