@ -1,116 +1,204 @@
use crate ::asb ::Rate ;
use crate ::asb ::Rate ;
use anyhow ::Result ;
use anyhow ::{ anyhow , Context , Result } ;
use bitcoin ::util ::amount ::ParseAmountError ;
use futures ::{ SinkExt , StreamExt , TryStreamExt } ;
use futures ::{ SinkExt , StreamExt } ;
use reqwest ::Url ;
use serde ::{ Deserialize , Serialize } ;
use serde ::{ Deserialize , Serialize } ;
use serde_json ::Value ;
use std ::convert ::{ Infallible , TryFrom } ;
use std ::convert ::TryFrom ;
use std ::sync ::Arc ;
use std ::time ::Duration ;
use tokio ::sync ::watch ;
use tokio ::sync ::watch ;
/// Connect to Kraken websocket API for a constant stream of rate updates.
///
/// If the connection fails, it will automatically be re-established.
pub fn connect ( ) -> Result < RateUpdateStream > {
let ( rate_update , rate_update_receiver ) = watch ::channel ( Err ( Error ::NotYetAvailable ) ) ;
let rate_update = Arc ::new ( rate_update ) ;
tokio ::spawn ( async move {
let result = backoff ::future ::retry_notify ::< Infallible , _ , _ , _ , _ , _ > (
backoff ::ExponentialBackoff ::default ( ) ,
| | {
let rate_update = rate_update . clone ( ) ;
async move {
let mut stream = connection ::new ( ) . await ? ;
while let Some ( update ) = stream . try_next ( ) . await . map_err ( to_backoff ) ? {
let send_result = rate_update . send ( Ok ( update ) ) ;
if send_result . is_err ( ) {
return Err ( backoff ::Error ::Permanent ( anyhow ! (
"receiver disconnected"
) ) ) ;
}
}
Err ( backoff ::Error ::Transient ( anyhow ! ( "stream ended" ) ) )
}
} ,
| error , next : Duration | {
tracing ::info ! ( % error , "Kraken websocket connection failed, retrying in {}ms" , next . as_millis ( ) ) ;
}
)
. await ;
match result {
Err ( e ) = > {
tracing ::warn ! ( "Rate updates incurred an unrecoverable error: {:#}" , e ) ;
// in case the retries fail permanently, let the subscribers know
rate_update . send ( Err ( Error ::PermanentFailure ) )
}
Ok ( never ) = > match never { } ,
}
} ) ;
Ok ( RateUpdateStream {
inner : rate_update_receiver ,
} )
}
#[ derive(Clone, Debug) ]
pub struct RateUpdateStream {
inner : watch ::Receiver < RateUpdate > ,
}
impl RateUpdateStream {
pub async fn wait_for_update ( & mut self ) -> Result < RateUpdate > {
self . inner . changed ( ) . await ? ;
Ok ( self . inner . borrow ( ) . clone ( ) )
}
pub fn latest_update ( & mut self ) -> RateUpdate {
self . inner . borrow ( ) . clone ( )
}
}
#[ derive(Clone, Debug, thiserror::Error) ]
pub enum Error {
#[ error( " Rate is not yet available " ) ]
NotYetAvailable ,
#[ error( " Permanently failed to retrieve rate from Kraken " ) ]
PermanentFailure ,
}
type RateUpdate = Result < Rate , Error > ;
/// Maps a [`connection::Error`] to a backoff error, effectively defining our
/// retry strategy.
fn to_backoff ( e : connection ::Error ) -> backoff ::Error < anyhow ::Error > {
use backoff ::Error ::* ;
match e {
// Connection closures and websocket errors will be retried
connection ::Error ::ConnectionClosed = > Transient ( anyhow ::Error ::from ( e ) ) ,
connection ::Error ::WebSocket ( _ ) = > Transient ( anyhow ::Error ::from ( e ) ) ,
// Failures while parsing a message are permanent because they most likely present a
// programmer error
connection ::Error ::Parse ( _ ) = > Permanent ( anyhow ::Error ::from ( e ) ) ,
}
}
/// Kraken websocket connection module.
///
/// Responsible for establishing a connection to the Kraken websocket API and
/// transforming the received websocket frames into a stream of rate updates.
/// The connection may fail in which case it is simply terminated and the stream
/// ends.
mod connection {
use super ::* ;
use crate ::kraken ::wire ;
use futures ::stream ::BoxStream ;
use tokio_tungstenite ::tungstenite ;
use tokio_tungstenite ::tungstenite ;
use tracing ::{ error , trace } ;
pub async fn connect ( ) -> Result < RateUpdateStream > {
pub async fn new ( ) -> Result < BoxStream < ' static , Result < Rate , Error > > > {
let ( rate_update , rate_update_receiver ) = watch ::channel ( Err ( Error ::NotYetRetrieved ) ) ;
let ( mut rate_stream , _ ) = tokio_tungstenite ::connect_async ( "wss://ws.kraken.com" )
. await
. context ( "Failed to connect to Kraken websocket API" ) ? ;
let ( rate_stream , _response ) =
rate_stream
tokio_tungstenite ::connect_async ( Url ::parse ( KRAKEN_WS_URL ) . expect ( "valid url" ) ) . await ? ;
. send ( SUBSCRIBE_XMR_BTC_TICKER_PAYLOAD . into ( ) )
. await ? ;
let ( mut rate_stream_sink , mut rate_stream ) = rate_stream . split ( ) ;
let stream = rate_stream . err_into ( ) . try_filter_map ( parse_message ) . boxed ( ) ;
tokio ::spawn ( async move {
Ok ( stream )
while let Some ( msg ) = rate_stream . next ( ) . await {
}
/// Parse a websocket message into a [`Rate`].
///
/// Messages which are not actually ticker updates are ignored and result in
/// `None` being returned. In the context of a [`TryStream`], these will
/// simply be filtered out.
async fn parse_message ( msg : tungstenite ::Message ) -> Result < Option < Rate > , Error > {
let msg = match msg {
let msg = match msg {
Ok ( tungstenite ::Message ::Text ( msg ) ) = > msg ,
tungstenite ::Message ::Text ( msg ) = > msg ,
Ok ( tungstenite ::Message ::Close ( close_frame ) ) = > {
tungstenite ::Message ::Close ( close_frame ) = > {
if let Some ( tungstenite ::protocol ::CloseFrame { code , reason } ) = close_frame {
if let Some ( tungstenite ::protocol ::CloseFrame { code , reason } ) = close_frame {
error ! (
tracing ::debug ! (
"Kraken rate stream was closed with code {} and reason: {}" ,
"Kraken rate stream was closed with code {} and reason: {}" ,
code , reason
code ,
reason
) ;
) ;
} else {
} else {
error ! ( "Kraken rate stream was closed without code and reason" ) ;
tracing ::debug ! ( "Kraken rate stream was closed without code and reason" ) ;
}
}
let _ = rate_update . send ( Err ( Error ::ConnectionClosed ) ) ;
continue ;
return Err ( Error ::ConnectionClosed ) ;
}
}
Ok ( msg ) = > {
msg = > {
trace ! (
tracing ::trace ! (
"Kraken rate stream returned non text message that will be ignored: {}" ,
"Kraken rate stream returned non text message that will be ignored: {}" ,
msg
msg
) ;
) ;
continue ;
}
return Ok ( None ) ;
Err ( e ) = > {
error ! ( % e , "Error when reading from Kraken rate stream" ) ;
let _ = rate_update . send ( Err ( e . into ( ) ) ) ;
continue ;
}
}
} ;
} ;
let update = match serde_json ::from_str ::< Event > ( & msg ) {
let update = match serde_json ::from_str ::< wire ::Event > ( & msg ) {
Ok ( Event ::SystemStatus ) = > {
Ok ( wire ::Event ::SystemStatus ) = > {
tracing ::debug ! ( "Connected to Kraken websocket API" ) ;
tracing ::debug ! ( "Connected to Kraken websocket API" ) ;
continue ;
return Ok ( None ) ;
}
}
Ok ( Event ::SubscriptionStatus ) = > {
Ok ( wire ::Event ::SubscriptionStatus ) = > {
tracing ::debug ! ( "Subscribed to updates for ticker" ) ;
tracing ::debug ! ( "Subscribed to updates for ticker" ) ;
continue ;
return Ok ( None ) ;
}
}
Ok ( Event ::Heartbeat ) = > {
Ok ( wire ::Event ::Heartbeat ) = > {
tracing ::trace ! ( "Received heartbeat message" ) ;
tracing ::trace ! ( "Received heartbeat message" ) ;
continue ;
return Ok ( None ) ;
}
}
// if the message is not an event, it is a ticker update or an unknown event
// if the message is not an event, it is a ticker update or an unknown event
Err ( _ ) = > match serde_json ::from_str ::< TickerUpdate > ( & msg ) {
Err ( _ ) = > match serde_json ::from_str ::< wire ::TickerUpdate > ( & msg ) {
Ok ( ticker ) = > ticker ,
Ok ( ticker ) = > ticker ,
Err ( e ) = > {
Err ( e ) = > {
tracing ::warn ! ( % e , "Failed to deserialize message '{}' as ticker update" , msg ) ;
tracing ::warn ! ( % e , "Failed to deserialize message '{}' as ticker update" , msg ) ;
let _ = rate_update . send ( Err ( Error ::UnknownMessage { msg } ) ) ;
continue ;
return Ok ( None ) ;
}
}
} ,
} ,
} ;
} ;
let rate = match Rate ::try_from ( update ) {
let update = Rate ::try_from ( update ) ? ;
Ok ( rate ) = > rate ,
Err ( e ) = > {
let _ = rate_update . send ( Err ( e ) ) ;
continue ;
}
} ;
let _ = rate_update . send ( Ok ( rate ) ) ;
Ok ( Some ( update ) )
}
}
} ) ;
rate_stream_sink
#[ derive(Debug, thiserror::Error) ]
. send ( SUBSCRIBE_XMR_BTC_TICKER_PAYLOAD . into ( ) )
pub enum Error {
. await ? ;
#[ error( " The Kraken server closed the websocket connection " ) ]
ConnectionClosed ,
Ok ( RateUpdateStream {
#[ error( " Failed to read message from websocket stream " ) ]
inner : rate_update_receiver ,
WebSocket ( #[ from ] tungstenite ::Error ) ,
} )
#[ error( " Failed to parse rate from websocket message " ) ]
}
Parse ( #[ from ] wire ::Error ) ,
#[ derive(Clone, Debug) ]
pub struct RateUpdateStream {
inner : watch ::Receiver < Result < Rate , Error > > ,
}
impl RateUpdateStream {
pub async fn wait_for_update ( & mut self ) -> Result < Result < Rate , Error > > {
self . inner . changed ( ) . await ? ;
Ok ( self . inner . borrow ( ) . clone ( ) )
}
pub fn latest_update ( & mut self ) -> Result < Rate , Error > {
self . inner . borrow ( ) . clone ( )
}
}
}
const KRAKEN_WS_URL : & str = "wss://ws.kraken.com" ;
const SUBSCRIBE_XMR_BTC_TICKER_PAYLOAD : & str = r #"
const SUBSCRIBE_XMR_BTC_TICKER_PAYLOAD : & str = r #"
{ "event" : "subscribe" ,
{ "event" : "subscribe" ,
"pair" : [ "XMR/XBT" ] ,
"pair" : [ "XMR/XBT" ] ,
@ -118,36 +206,19 @@ const SUBSCRIBE_XMR_BTC_TICKER_PAYLOAD: &str = r#"
"name" : "ticker"
"name" : "ticker"
}
}
} " #;
} " #;
#[ derive(Clone, Debug, thiserror::Error) ]
pub enum Error {
#[ error( " Rate has not yet been retrieved from Kraken websocket API " ) ]
NotYetRetrieved ,
#[ error( " The Kraken server closed the websocket connection " ) ]
ConnectionClosed ,
#[ error( " Websocket: {0} " ) ]
WebSocket ( String ) ,
#[ error( " Received unknown message from Kraken: {msg} " ) ]
UnknownMessage { msg : String } ,
#[ error( " Data field is missing " ) ]
DataFieldMissing ,
#[ error( " Ask Rate Element is of unexpected type " ) ]
UnexpectedAskRateElementType ,
#[ error( " Ask Rate Element is missing " ) ]
MissingAskRateElementType ,
#[ error( " Bitcoin amount parse error: " ) ]
BitcoinParseAmount ( #[ from ] ParseAmountError ) ,
}
}
impl From < tungstenite ::Error > for Error {
/// Kraken websocket API wire module.
fn from ( err : tungstenite ::Error ) -> Self {
///
Error ::WebSocket ( format! ( "{:#}" , err ) )
/// Responsible for parsing websocket text messages to events and rate updates.
}
mod wire {
}
use super ::* ;
use bitcoin ::util ::amount ::ParseAmountError ;
use serde_json ::Value ;
#[ derive(Debug, Serialize, Deserialize, PartialEq) ]
#[ derive(Debug, Serialize, Deserialize, PartialEq) ]
#[ serde(tag = " event " ) ]
#[ serde(tag = " event " ) ]
enum Event {
pub enum Event {
#[ serde(rename = " systemStatus " ) ]
#[ serde(rename = " systemStatus " ) ]
SystemStatus ,
SystemStatus ,
#[ serde(rename = " heartbeat " ) ]
#[ serde(rename = " heartbeat " ) ]
@ -156,19 +227,31 @@ enum Event {
SubscriptionStatus ,
SubscriptionStatus ,
}
}
#[ derive(Clone, Debug, thiserror::Error) ]
pub enum Error {
#[ error( " Data field is missing " ) ]
DataFieldMissing ,
#[ error( " Ask Rate Element is of unexpected type " ) ]
UnexpectedAskRateElementType ,
#[ error( " Ask Rate Element is missing " ) ]
MissingAskRateElementType ,
#[ error( " Failed to parse Bitcoin amount " ) ]
BitcoinParseAmount ( #[ from ] ParseAmountError ) ,
}
#[ derive(Debug, Serialize, Deserialize) ]
#[ derive(Debug, Serialize, Deserialize) ]
#[ serde(transparent) ]
#[ serde(transparent) ]
struct TickerUpdate ( Vec < TickerField > ) ;
pub struct TickerUpdate ( Vec < TickerField > ) ;
#[ derive(Debug, Serialize, Deserialize) ]
#[ derive(Debug, Serialize, Deserialize) ]
#[ serde(untagged) ]
#[ serde(untagged) ]
enum TickerField {
pub enum TickerField {
Data ( TickerData ) ,
Data ( TickerData ) ,
Metadata ( Value ) ,
Metadata ( Value ) ,
}
}
#[ derive(Debug, Serialize, Deserialize) ]
#[ derive(Debug, Serialize, Deserialize) ]
struct TickerData {
pub struct TickerData {
#[ serde(rename = " a " ) ]
#[ serde(rename = " a " ) ]
ask : Vec < RateElement > ,
ask : Vec < RateElement > ,
#[ serde(rename = " b " ) ]
#[ serde(rename = " b " ) ]
@ -177,7 +260,7 @@ struct TickerData {
#[ derive(Debug, Serialize, Deserialize) ]
#[ derive(Debug, Serialize, Deserialize) ]
#[ serde(untagged) ]
#[ serde(untagged) ]
enum RateElement {
pub enum RateElement {
Text ( String ) ,
Text ( String ) ,
Number ( u64 ) ,
Number ( u64 ) ,
}
}
@ -235,3 +318,4 @@ mod tests {
let _ = serde_json ::from_str ::< TickerUpdate > ( message ) . unwrap ( ) ;
let _ = serde_json ::from_str ::< TickerUpdate > ( message ) . unwrap ( ) ;
}
}
}
}
}