@@ -80,9 +80,15 @@ impl<E: ExchangePort + 'static, S: Store + 'static> FundingService<E, S> {
8080 if balance. asset == "USDT" || balance. free <= Decimal :: ZERO {
8181 continue ;
8282 }
83- let symbol = format ! ( "{}USDT" , balance. asset) ;
84- let price = self . exchange . get_spot_price ( & symbol) . await ?;
85- let gross = balance. free * price. as_decimal ( ) ;
83+ let Some ( route) = self . resolve_usdt_route ( & balance. asset ) . await ? else {
84+ tracing:: debug!( asset = %balance. asset, "Skipping spot asset without USDT route" ) ;
85+ continue ;
86+ } ;
87+ let price = self . exchange . get_spot_price ( & route. symbol ) . await ?;
88+ let gross = match route. side {
89+ SpotOrderSide :: Sell => balance. free * price. as_decimal ( ) ,
90+ SpotOrderSide :: Buy => balance. free / price. as_decimal ( ) ,
91+ } ;
8692 if gross < self . config . dust_usdt {
8793 continue ;
8894 }
@@ -97,7 +103,7 @@ impl<E: ExchangePort + 'static, S: Store + 'static> FundingService<E, S> {
97103 asset : balance. asset ,
98104 qty : balance. free ,
99105 est_usdt,
100- symbol,
106+ symbol : route . symbol ,
101107 } ) ;
102108 }
103109
@@ -152,15 +158,16 @@ impl<E: ExchangePort + 'static, S: Store + 'static> FundingService<E, S> {
152158 if view. state == FundingState :: Converting . as_str ( ) {
153159 for item in & quote. items {
154160 let client_order_id = spot_client_order_id ( quote_id, & item. asset ) ;
161+ let route = route_from_quote_item ( & item. asset , & item. symbol ) ?;
155162 let order =
156163 match self . exchange . get_spot_order ( & item. symbol , & client_order_id) . await ? {
157164 Some ( order) if order. status == "FILLED" => order,
158165 _ => {
159166 self . exchange
160167 . place_spot_market_order ( SpotOrderRequest {
161168 symbol : item. symbol . clone ( ) ,
162- side : SpotOrderSide :: Sell ,
163- quantity_kind : SpotOrderQuantity :: Base ,
169+ side : route . side ,
170+ quantity_kind : route . quantity_kind ,
164171 quantity : item. qty ,
165172 client_order_id : client_order_id. clone ( ) ,
166173 } )
@@ -174,14 +181,21 @@ impl<E: ExchangePort + 'static, S: Store + 'static> FundingService<E, S> {
174181 item. asset
175182 ) ) ) ;
176183 }
184+ let usdt_out = match route. side {
185+ SpotOrderSide :: Sell => order. cummulative_quote_qty ,
186+ SpotOrderSide :: Buy if order. fee_asset == "USDT" => {
187+ ( order. executed_qty - order. fee ) . max ( Decimal :: ZERO )
188+ } ,
189+ SpotOrderSide :: Buy => order. executed_qty ,
190+ } ;
177191 self . append_and_project (
178192 quote_id,
179193 FundingState :: Converting ,
180194 "ConversionExecuted" ,
181195 json ! ( {
182196 "asset" : item. asset,
183197 "qty" : order. executed_qty,
184- "usdt_out" : order . cummulative_quote_qty ,
198+ "usdt_out" : usdt_out ,
185199 "client_order_id" : client_order_id,
186200 } ) ,
187201 )
@@ -338,6 +352,28 @@ impl<E: ExchangePort + 'static, S: Store + 'static> FundingService<E, S> {
338352 . unwrap_or ( Decimal :: ZERO ) )
339353 }
340354
355+ async fn resolve_usdt_route ( & self , asset : & str ) -> DaemonResult < Option < SpotConversionRoute > > {
356+ let direct = format ! ( "{asset}USDT" ) ;
357+ if self . exchange . spot_symbol_is_trading ( & direct) . await ? {
358+ return Ok ( Some ( SpotConversionRoute {
359+ symbol : direct,
360+ side : SpotOrderSide :: Sell ,
361+ quantity_kind : SpotOrderQuantity :: Base ,
362+ } ) ) ;
363+ }
364+
365+ let inverse = format ! ( "USDT{asset}" ) ;
366+ if self . exchange . spot_symbol_is_trading ( & inverse) . await ? {
367+ return Ok ( Some ( SpotConversionRoute {
368+ symbol : inverse,
369+ side : SpotOrderSide :: Buy ,
370+ quantity_kind : SpotOrderQuantity :: Quote ,
371+ } ) ) ;
372+ }
373+
374+ Ok ( None )
375+ }
376+
341377 async fn fail ( & self , saga_id : Uuid , reason : & str ) -> DaemonResult < ( ) > {
342378 self . append_and_project (
343379 saga_id,
@@ -480,3 +516,31 @@ fn spot_client_order_id(saga_id: Uuid, asset: &str) -> String {
480516fn transfer_client_key ( saga_id : Uuid ) -> String {
481517 format ! ( "rbx-fund-transfer-{}" , saga_id. simple( ) )
482518}
519+
520+ #[ cfg( feature = "postgres" ) ]
521+ struct SpotConversionRoute {
522+ symbol : String ,
523+ side : SpotOrderSide ,
524+ quantity_kind : SpotOrderQuantity ,
525+ }
526+
527+ #[ cfg( feature = "postgres" ) ]
528+ fn route_from_quote_item ( asset : & str , symbol : & str ) -> DaemonResult < SpotConversionRoute > {
529+ if symbol == format ! ( "{asset}USDT" ) {
530+ return Ok ( SpotConversionRoute {
531+ symbol : symbol. to_string ( ) ,
532+ side : SpotOrderSide :: Sell ,
533+ quantity_kind : SpotOrderQuantity :: Base ,
534+ } ) ;
535+ }
536+
537+ if symbol == format ! ( "USDT{asset}" ) {
538+ return Ok ( SpotConversionRoute {
539+ symbol : symbol. to_string ( ) ,
540+ side : SpotOrderSide :: Buy ,
541+ quantity_kind : SpotOrderQuantity :: Quote ,
542+ } ) ;
543+ }
544+
545+ Err ( DaemonError :: Config ( format ! ( "invalid_funding_route:{asset}:{symbol}" ) ) )
546+ }
0 commit comments