1mod account;
2pub mod args;
3mod block;
4mod book;
5mod highlight;
6mod mms;
7mod snapshot;
8mod trace;
9mod trades;
10mod tx;
11
12use std::time::Duration;
13
14use alloy::{
15 providers::{Provider, ProviderBuilder},
16 rpc::{client::RpcClient, types::BlockId},
17 transports::layers::{RetryBackoffLayer, ThrottleLayer},
18};
19use anyhow::Context;
20use args::Cli;
21use perpl_sdk::{Chain, abi::dex, state::SnapshotBuilder, types};
22use tokio_util::sync::CancellationToken;
23
24use crate::{
25 args::{Commands, MarketMaker, ShowCommands},
26 highlight::Highlights,
27 mms::Maker,
28};
29
30pub async fn run(cli: Cli) -> anyhow::Result<()> {
31 let chain = if cli.testnet { Chain::testnet() } else { Chain::mainnet() };
32 let (rpc, default) = cli.rpc.map(|rpc| (rpc, false)).unwrap_or(if cli.testnet {
33 (args::DEFAULT_TESTNET_RPC_PROVIDER.to_string(), true)
34 } else {
35 (args::DEFAULT_MAINNET_RPC_PROVIDER.to_string(), true)
36 });
37 let client = if default || cli.rpc_throttle.is_some() {
38 RpcClient::builder()
40 .layer(ThrottleLayer::new(cli.rpc_throttle.unwrap_or(args::DEFAULT_RPC_THROTTLING)))
41 .layer(RetryBackoffLayer::new(10, 100, 200))
42 .connect(&rpc)
43 .await
44 .context("connecting to RPC")?
45 } else {
46 RpcClient::builder()
47 .layer(RetryBackoffLayer::new(10, 100, 200))
48 .connect(&rpc)
49 .await
50 .context("connecting to RPC")?
51 };
52 client.set_poll_interval(Duration::from_millis(100));
53 let provider = ProviderBuilder::new().connect_client(client);
54
55 let chain = Chain::custom(
58 provider.get_chain_id().await?,
59 chain.collateral_token(),
60 chain.deployed_at_block(),
61 cli.exchange.unwrap_or(chain.exchange()),
62 cli.perp.clone(),
63 )
64 .with_excluded_perpetuals(chain.excluded_perpetuals().to_vec());
67
68 let block_id = cli.block.map(BlockId::number).unwrap_or(BlockId::safe());
69
70 if !cli.perp.is_empty() {
71 let listed = perpl_sdk::state::listed_perpetuals(&chain, provider.clone(), block_id)
72 .await
73 .context("discovering listed perpetuals")?;
74 if let Some(unknown_perp) = cli.perp.iter().find(|perp_id| !listed.contains(perp_id)) {
75 if chain.excluded_perpetuals().contains(unknown_perp) {
78 return Err(anyhow::anyhow!(
79 "perpetual ID {} is excluded from indexing for this chain",
80 unknown_perp,
81 ));
82 }
83 return Err(anyhow::anyhow!(
84 "unknown perpetual ID: {}, listed: {:?}",
85 unknown_perp,
86 listed,
87 ));
88 }
89 }
90
91 let mut builder = SnapshotBuilder::new(&chain, provider.clone());
92 if let Some(block) = cli.block {
93 builder = builder.at_block(BlockId::number(block));
94 }
95
96 if !cli.account.is_empty() {
97 builder = builder.with_accounts(cli.account.clone());
98 } else {
99 builder = builder.with_all_positions();
100 }
101
102 let builder = match &cli.command {
103 Commands::Block { block_number: _ } => None,
104 Commands::Snapshot | Commands::Trace => Some(builder),
105 Commands::Show { command } => match command {
106 ShowCommands::Account { num_trades: _ } => {
107 if cli.account.len() != 1 {
108 return Err(anyhow::anyhow!(
109 "exactly one account should be provided, see `--account`"
110 ));
111 }
112 Some(builder)
113 },
114 ShowCommands::Book { .. } | ShowCommands::Mms { .. } => {
115 if cli.perp.len() != 1 {
116 return Err(anyhow::anyhow!(
117 "exactly one perp should be provided, see `--perp`"
118 ));
119 }
120 Some(builder)
121 },
122 ShowCommands::Trades => None,
123 },
124 Commands::Tx { tx_hash: _ } => None,
125 };
126
127 let exchange = if let Some(builder) = builder {
128 Some(
129 builder
130 .build()
131 .await
132 .context("building exchange snapshot")?,
133 )
134 } else {
135 None
136 };
137
138 let mut highlights = Highlights::default();
141 let makers = match &cli.command {
142 Commands::Show { command: ShowCommands::Mms { makers, .. } } => {
143 resolve_makers(&chain, provider.clone(), block_id, makers, &mut highlights).await?
144 },
145 _ => vec![],
146 };
147 if let Some(account) = cli.highlight {
148 highlights.track(resolve_account_id(&chain, provider.clone(), block_id, account).await?);
149 }
150
151 let cancellation_signal = CancellationToken::new();
152 let cancellation_token = cancellation_signal.child_token();
153 tokio::spawn(async move {
154 tokio::signal::ctrl_c()
155 .await
156 .expect("failed to install CTRL+C signal handler");
157 cancellation_signal.cancel();
158 });
159
160 match &cli.command {
161 Commands::Block { block_number } => {
162 block::render(&chain, provider, *block_number, &highlights).await?
163 },
164 Commands::Snapshot => snapshot::render(exchange.unwrap()),
165 Commands::Show { command } => match command {
166 ShowCommands::Account { num_trades } => {
167 account::render(
168 chain,
169 provider,
170 exchange.unwrap(),
171 cli.num_blocks,
172 *num_trades,
173 cancellation_token,
174 )
175 .await?
176 },
177 ShowCommands::Book { book } => {
178 book::render(
179 chain,
180 provider,
181 exchange.unwrap(),
182 book,
183 &highlights,
184 cli.num_blocks,
185 cancellation_token,
186 )
187 .await?
188 },
189 ShowCommands::Mms { makers: _, book } => {
190 mms::render(
191 chain,
192 provider,
193 exchange.unwrap(),
194 makers,
195 highlights,
196 book,
197 cli.num_blocks,
198 cancellation_token,
199 )
200 .await?
201 },
202 ShowCommands::Trades => {
203 trades::render(chain, provider, &highlights, cli.num_blocks, cancellation_token)
204 .await?
205 },
206 },
207 Commands::Trace => {
208 trace::render(
209 chain,
210 provider,
211 exchange.unwrap(),
212 &highlights,
213 cli.num_blocks,
214 cancellation_token,
215 )
216 .await?
217 },
218 Commands::Tx { tx_hash } => tx::render(&chain, provider, *tx_hash, &highlights).await?,
219 }
220
221 Ok(())
222}
223
224async fn resolve_makers<P: Provider + Clone>(
227 chain: &Chain,
228 provider: P,
229 block_id: BlockId,
230 makers: &[MarketMaker],
231 highlights: &mut Highlights,
232) -> anyhow::Result<Vec<Maker>> {
233 let mut resolved: Vec<Maker> = Vec::with_capacity(makers.len());
234 for maker in makers {
235 let account_id =
236 resolve_account_id(chain, provider.clone(), block_id, maker.account).await?;
237 if let Some(existing) = resolved.iter().find(|m| m.account_id == account_id) {
238 return Err(anyhow::anyhow!(
239 "market makers {} and {} are the same account #{}",
240 existing.label,
241 maker.label.as_deref().unwrap_or("<unlabelled>"),
242 account_id,
243 ));
244 }
245 highlights.add(account_id);
246 resolved.push(Maker {
247 account_id,
248 label: maker
249 .label
250 .clone()
251 .unwrap_or_else(|| format!("#{}", account_id)),
252 });
253 }
254 Ok(resolved)
255}
256
257async fn resolve_account_id<P: Provider + Clone>(
260 chain: &Chain,
261 provider: P,
262 block_id: BlockId,
263 account: types::AccountAddressOrID,
264) -> anyhow::Result<types::AccountId> {
265 match account {
266 types::AccountAddressOrID::ID(id) => Ok(id),
267 types::AccountAddressOrID::Address(address) => {
268 Ok(dex::Exchange::new(chain.exchange(), provider)
269 .getAccountByAddr(address)
270 .block(block_id)
271 .call()
272 .await
273 .with_context(|| format!("resolving account address {}", address))?
274 .accountId
275 .to())
276 },
277 }
278}