diff --git a/crates/core/component/dex/src/component/rpc.rs b/crates/core/component/dex/src/component/rpc.rs index 699cb097e5..6f218e0f6f 100644 --- a/crates/core/component/dex/src/component/rpc.rs +++ b/crates/core/component/dex/src/component/rpc.rs @@ -1,9 +1,11 @@ use std::{pin::Pin, sync::Arc}; -use crate::ExecutionCircuitBreaker; use async_stream::try_stream; -use cnidarium::{StateDelta, Storage}; use futures::{StreamExt, TryStreamExt}; +use tonic::Status; +use tracing::instrument; + +use cnidarium::{StateDelta, Storage}; use penumbra_asset::{asset, Value}; use penumbra_proto::{ core::component::dex::v1::{ @@ -20,18 +22,18 @@ use penumbra_proto::{ }, DomainType, StateReadProto, }; -use tonic::Status; -use tracing::instrument; -use super::{ - router::{RouteAndFill, RoutingParams}, - PositionRead, StateReadExt, -}; +use crate::ExecutionCircuitBreaker; use crate::{ lp::position::{self, Position}, state_key, DirectedTradingPair, SwapExecution, TradingPair, }; +use super::{ + router::{RouteAndFill, RoutingParams}, + PositionRead, StateReadExt, +}; + // TODO: Hide this and only expose a Router? pub struct Server { storage: Storage, @@ -541,7 +543,21 @@ impl SimulationService for Server { .await .map_err(|e| tonic::Status::internal(format!("error simulating trade: {:#}", e)))?; + let unfilled = Value { + amount: input + .amount + .checked_sub(&swap_execution.input.amount) + .ok_or_else(|| { + tonic::Status::failed_precondition( + "swap execution input amount is larger than request input amount" + .to_string(), + ) + })?, + asset_id: input.asset_id, + }; + Ok(tonic::Response::new(SimulateTradeResponse { + unfilled: Some(unfilled.into()), output: Some(swap_execution.into()), })) } diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs index 67a87a958b..9ef3b955a2 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs @@ -1254,6 +1254,9 @@ impl ::prost::Name for SimulateTradeRequest { pub struct SimulateTradeResponse { #[prost(message, optional, tag = "1")] pub output: ::core::option::Option, + /// Estimated input amount that will not be swapped due to liquidity + #[prost(message, optional, tag = "2")] + pub unfilled: ::core::option::Option, } impl ::prost::Name for SimulateTradeResponse { const NAME: &'static str = "SimulateTradeResponse"; diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs index 6fef673be7..ecd5756d47 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs @@ -4868,10 +4868,16 @@ impl serde::Serialize for SimulateTradeResponse { if self.output.is_some() { len += 1; } + if self.unfilled.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.SimulateTradeResponse", len)?; if let Some(v) = self.output.as_ref() { struct_ser.serialize_field("output", v)?; } + if let Some(v) = self.unfilled.as_ref() { + struct_ser.serialize_field("unfilled", v)?; + } struct_ser.end() } } @@ -4883,11 +4889,13 @@ impl<'de> serde::Deserialize<'de> for SimulateTradeResponse { { const FIELDS: &[&str] = &[ "output", + "unfilled", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Output, + Unfilled, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -4911,6 +4919,7 @@ impl<'de> serde::Deserialize<'de> for SimulateTradeResponse { { match value { "output" => Ok(GeneratedField::Output), + "unfilled" => Ok(GeneratedField::Unfilled), _ => Ok(GeneratedField::__SkipField__), } } @@ -4931,6 +4940,7 @@ impl<'de> serde::Deserialize<'de> for SimulateTradeResponse { V: serde::de::MapAccess<'de>, { let mut output__ = None; + let mut unfilled__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Output => { @@ -4939,6 +4949,12 @@ impl<'de> serde::Deserialize<'de> for SimulateTradeResponse { } output__ = map_.next_value()?; } + GeneratedField::Unfilled => { + if unfilled__.is_some() { + return Err(serde::de::Error::duplicate_field("unfilled")); + } + unfilled__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -4946,6 +4962,7 @@ impl<'de> serde::Deserialize<'de> for SimulateTradeResponse { } Ok(SimulateTradeResponse { output: output__, + unfilled: unfilled__, }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index b217512207..45487c1b7a 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/proto/penumbra/penumbra/core/component/dex/v1/dex.proto b/proto/penumbra/penumbra/core/component/dex/v1/dex.proto index f4f5e812cd..0eda7e2770 100644 --- a/proto/penumbra/penumbra/core/component/dex/v1/dex.proto +++ b/proto/penumbra/penumbra/core/component/dex/v1/dex.proto @@ -584,6 +584,8 @@ message SimulateTradeRequest { message SimulateTradeResponse { core.component.dex.v1.SwapExecution output = 1; + // Estimated input amount that will not be swapped due to liquidity + asset.v1.Value unfilled = 2; } message EventSwap {