pcli/command/tx/replicate/
xyk.rs1use std::io::Write;
2use std::path::PathBuf;
3
4use anyhow::{anyhow, Context};
5use dialoguer::Confirm;
6use rand_core::OsRng;
7
8use penumbra_sdk_asset::Value;
9use penumbra_sdk_dex::{lp::position::Position, DirectedUnitPair};
10use penumbra_sdk_keys::keys::AddressIndex;
11use penumbra_sdk_num::{fixpoint::U128x128, Amount};
12use penumbra_sdk_proto::view::v1::GasPricesRequest;
13use penumbra_sdk_view::{Planner, ViewClient};
14
15use crate::dex_utils;
16use crate::dex_utils::replicate::debug;
17use crate::{warning, App};
18
19#[derive(Debug, Clone, clap::Args)]
20pub struct ConstantProduct {
21 pub pair: DirectedUnitPair,
22 pub input: Value,
23
24 #[clap(short, long)]
25 pub current_price: Option<f64>,
26
27 #[clap(short, long, default_value_t = 0u32)]
28 pub fee_bps: u32,
29 #[clap(short, long)]
31 pub yes: bool,
32
33 #[clap(short, long, hide(true))]
34 pub debug_file: Option<PathBuf>,
35 #[clap(long, default_value = "0", hide(true))]
36 pub source: u32,
37}
38
39impl ConstantProduct {
40 pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> {
41 self.validate()?;
42 let pair = self.pair.clone();
43 let current_price =
44 super::process_price_or_fetch_spread(app, self.current_price, self.pair.clone())
45 .await?;
46
47 let positions = dex_utils::replicate::xyk::replicate(
48 &pair,
49 &self.input,
50 current_price.try_into()?,
51 self.fee_bps,
52 )?;
53
54 let (amount_start, amount_end) =
55 positions
56 .iter()
57 .fold((Amount::zero(), Amount::zero()), |acc, pos| {
58 (
59 acc.0
60 + pos
61 .reserves_for(pair.start.id())
62 .expect("start is part of position"),
63 acc.1
64 + pos
65 .reserves_for(pair.end.id())
66 .expect("end is part of position"),
67 )
68 });
69 let amount_start = pair.start.format_value(amount_start);
70 let amount_end = pair.end.format_value(amount_end);
71
72 warning::rmm();
73
74 if !self.yes
75 && !Confirm::new()
76 .with_prompt("In the solemn voice of Mandos, he who sets the fates of all, you hear a question,\nechoing like a whisper through the Halls of Waiting:\n\"Do you, in your heart of hearts, truly wish to proceed?\"")
77 .interact()?
78 {
79 return Ok(());
80 }
81 println!("\nso it shall be...\n\n");
82 println!(
83 "#################################################################################"
84 );
85 println!(
86 "########################### LIQUIDITY SUMMARY ###################################"
87 );
88 println!(
89 "#################################################################################"
90 );
91 println!("\nYou want to provide liquidity on the pair {}", pair);
92 println!("You will need:",);
93 println!(" -> {amount_start}{}", pair.start);
94 println!(" -> {amount_end}{}", pair.end);
95 println!("You will create the following positions:");
98 let asset_cache = app.view().assets().await?;
99 println!(
100 "{}",
101 crate::command::utils::render_positions(&asset_cache, &positions),
102 );
103
104 if let Some(debug_file) = &self.debug_file {
105 Self::write_debug_data(
106 debug_file.clone(),
107 self.pair.clone(),
108 self.input.clone(),
109 current_price,
110 positions.clone(),
111 )?;
112 return Ok(());
113 }
114
115 if !self.yes
116 && !Confirm::new()
117 .with_prompt("Do you want to open those liquidity positions on-chain?")
118 .interact()?
119 {
120 return Ok(());
121 }
122
123 let gas_prices = app
124 .view
125 .as_mut()
126 .context("view service must be initialized")?
127 .gas_prices(GasPricesRequest {})
128 .await?
129 .into_inner()
130 .gas_prices
131 .expect("gas prices must be available")
132 .try_into()?;
133
134 let mut planner = Planner::new(OsRng);
135 planner.set_gas_prices(gas_prices);
136 positions.iter().for_each(|position| {
137 planner.position_open(position.clone());
138 });
139
140 let plan = planner
141 .plan(
142 app.view
143 .as_mut()
144 .context("view service must be initialized")?,
145 AddressIndex::new(self.source),
146 )
147 .await?;
148 let tx_id = app.build_and_submit_transaction(plan).await?;
149 println!("posted with transaction id: {tx_id}");
150
151 Ok(())
152 }
153
154 fn validate(&self) -> anyhow::Result<()> {
155 if self.input.asset_id != self.pair.start.id() && self.input.asset_id != self.pair.end.id()
156 {
157 anyhow::bail!("you must supply liquidity with an asset that's part of the market")
158 } else if self.input.amount == 0u64.into() {
159 anyhow::bail!("the quantity of liquidity supplied must be non-zero.",)
160 } else if self.fee_bps > 5000 {
161 anyhow::bail!("the maximum fee is 5000bps (50%)")
162 } else if self.current_price.is_some()
163 && self.current_price.expect("current price is Some") <= 0.0
164 {
165 anyhow::bail!("the supplied current price must be positive")
166 } else {
167 Ok(())
168 }
169 }
170
171 pub(crate) fn write_debug_data(
172 file: PathBuf,
173 pair: DirectedUnitPair,
174 input: Value,
175 current_price: f64,
176 positions: Vec<Position>,
177 ) -> anyhow::Result<()> {
178 let alphas = dex_utils::replicate::xyk::sample_prices(
180 current_price,
181 dex_utils::replicate::xyk::NUM_POOLS_PRECISION,
182 );
183
184 alphas
185 .iter()
186 .enumerate()
187 .for_each(|(i, alpha)| tracing::debug!(i, alpha, "sampled tick"));
188
189 let r1: f64;
190
191 {
192 let raw_r1 = input.amount.value();
193 let denom_unit = pair.start.unit_amount().value();
194 let fp_r1 = U128x128::ratio(raw_r1, denom_unit).expect("denom unit is not 0");
195 r1 = fp_r1.into();
196 }
197
198 let r2 = r1 * current_price;
199 let total_k = r1 * r2;
200 println!("Entry R1: {r1}");
201 println!("Entry R2: {r2}");
202 println!("total K: {total_k}");
203
204 let debug_positions: Vec<debug::PayoffPositionEntry> = positions
205 .iter()
206 .zip(alphas)
207 .enumerate()
208 .map(|(idx, (pos, alpha))| {
209 let payoff_entry = debug::PayoffPosition::from_position(pair.clone(), pos.clone());
210 debug::PayoffPositionEntry {
211 payoff: payoff_entry,
212 current_price,
213 index: idx,
214 pair: pair.clone(),
215 alpha,
216 total_k,
217 }
218 })
219 .collect();
220
221 let mut fd = std::fs::File::create(&file).map_err(|e| {
222 anyhow!(
223 "fs error opening debug file {}: {}",
224 file.to_string_lossy(),
225 e
226 )
227 })?;
228
229 let json_data = serde_json::to_string(&debug_positions)
230 .map_err(|e| anyhow!("error serializing PayoffPositionEntry: {}", e))?;
231
232 fd.write_all(json_data.as_bytes())
233 .map_err(|e| anyhow!("error writing {}: {}", file.to_string_lossy(), e))?;
234 Ok(())
235 }
236}