Skip to main content

perpl_cli/
lib.rs

1mod account;
2pub mod args;
3mod block;
4mod book;
5mod snapshot;
6mod trace;
7mod trades;
8mod tx;
9
10use std::time::Duration;
11
12use alloy::{
13    providers::{Provider, ProviderBuilder},
14    rpc::{client::RpcClient, types::BlockId},
15    transports::layers::{RetryBackoffLayer, ThrottleLayer},
16};
17use anyhow::Context;
18use args::Cli;
19use perpl_sdk::{Chain, state::SnapshotBuilder};
20use tokio_util::sync::CancellationToken;
21
22use crate::args::{Commands, ShowCommands};
23
24pub async fn run(cli: Cli) -> anyhow::Result<()> {
25    let chain = if cli.testnet { Chain::testnet() } else { Chain::mainnet() };
26    let (rpc, default) = cli.rpc.map(|rpc| (rpc, false)).unwrap_or(if cli.testnet {
27        (args::DEFAULT_TESTNET_RPC_PROVIDER.to_string(), true)
28    } else {
29        (args::DEFAULT_MAINNET_RPC_PROVIDER.to_string(), true)
30    });
31    let client = if default || cli.rpc_throttle.is_some() {
32        // Apply throttling with default RPC
33        RpcClient::builder()
34            .layer(ThrottleLayer::new(cli.rpc_throttle.unwrap_or(args::DEFAULT_RPC_THROTTLING)))
35            .layer(RetryBackoffLayer::new(10, 100, 200))
36            .connect(&rpc)
37            .await
38            .context("connecting to RPC")?
39    } else {
40        RpcClient::builder()
41            .layer(RetryBackoffLayer::new(10, 100, 200))
42            .connect(&rpc)
43            .await
44            .context("connecting to RPC")?
45    };
46    client.set_poll_interval(Duration::from_millis(100));
47    let provider = ProviderBuilder::new().connect_client(client);
48
49    // An empty perpetual list makes the SDK track every contract listed on the
50    // exchange, discovered on-chain
51    let chain = Chain::custom(
52        provider.get_chain_id().await?,
53        chain.collateral_token(),
54        chain.deployed_at_block(),
55        cli.exchange.unwrap_or(chain.exchange()),
56        cli.perp.clone(),
57    )
58    // Carried over: `custom` starts with no exclusions, but the base chain's
59    // apply just as much to a custom exchange address on the same network
60    .with_excluded_perpetuals(chain.excluded_perpetuals().to_vec());
61
62    if !cli.perp.is_empty() {
63        let listed = perpl_sdk::state::listed_perpetuals(
64            &chain,
65            provider.clone(),
66            cli.block.map(BlockId::number).unwrap_or(BlockId::safe()),
67        )
68        .await
69        .context("discovering listed perpetuals")?;
70        if let Some(unknown_perp) = cli.perp.iter().find(|perp_id| !listed.contains(perp_id)) {
71            // Discovery leaves the chain's excluded contracts out, so say which
72            // of the two it is
73            if chain.excluded_perpetuals().contains(unknown_perp) {
74                return Err(anyhow::anyhow!(
75                    "perpetual ID {} is excluded from indexing for this chain",
76                    unknown_perp,
77                ));
78            }
79            return Err(anyhow::anyhow!(
80                "unknown perpetual ID: {}, listed: {:?}",
81                unknown_perp,
82                listed,
83            ));
84        }
85    }
86
87    let mut builder = SnapshotBuilder::new(&chain, provider.clone());
88    if let Some(block) = cli.block {
89        builder = builder.at_block(BlockId::number(block));
90    }
91
92    if !cli.account.is_empty() {
93        builder = builder.with_accounts(cli.account.clone());
94    } else {
95        builder = builder.with_all_positions();
96    }
97
98    let builder = match &cli.command {
99        Commands::Block { block_number: _ } => None,
100        Commands::Snapshot | Commands::Trace => Some(builder),
101        Commands::Show { command } => match command {
102            ShowCommands::Account { num_trades: _ } => {
103                if cli.account.len() != 1 {
104                    return Err(anyhow::anyhow!(
105                        "exactly one account should be provided, see `--account`"
106                    ));
107                }
108                Some(builder)
109            },
110            ShowCommands::Book { depth: _, orders_per_level: _, show_expired: _ } => {
111                if cli.perp.len() != 1 {
112                    return Err(anyhow::anyhow!(
113                        "exactly one perp should be provided, see `--perp`"
114                    ));
115                }
116                Some(builder)
117            },
118            ShowCommands::Trades => None,
119        },
120        Commands::Tx { tx_hash: _ } => None,
121    };
122
123    let exchange = if let Some(builder) = builder {
124        Some(
125            builder
126                .build()
127                .await
128                .context("building exchange snapshot")?,
129        )
130    } else {
131        None
132    };
133
134    let cancellation_signal = CancellationToken::new();
135    let cancellation_token = cancellation_signal.child_token();
136    tokio::spawn(async move {
137        tokio::signal::ctrl_c()
138            .await
139            .expect("failed to install CTRL+C signal handler");
140        cancellation_signal.cancel();
141    });
142
143    match &cli.command {
144        Commands::Block { block_number } => block::render(&chain, provider, *block_number).await?,
145        Commands::Snapshot => snapshot::render(exchange.unwrap()),
146        Commands::Show { command } => match command {
147            ShowCommands::Account { num_trades } => {
148                account::render(
149                    chain,
150                    provider,
151                    exchange.unwrap(),
152                    cli.num_blocks,
153                    *num_trades,
154                    cancellation_token,
155                )
156                .await?
157            },
158            ShowCommands::Book { depth, orders_per_level, show_expired } => {
159                book::render(
160                    chain,
161                    provider,
162                    exchange.unwrap(),
163                    *depth,
164                    *orders_per_level,
165                    *show_expired,
166                    cli.num_blocks,
167                    cancellation_token,
168                )
169                .await?
170            },
171            ShowCommands::Trades => {
172                trades::render(chain, provider, cli.num_blocks, cancellation_token).await?
173            },
174        },
175        Commands::Trace => {
176            trace::render(chain, provider, exchange.unwrap(), cli.num_blocks, cancellation_token)
177                .await?
178        },
179        Commands::Tx { tx_hash } => tx::render(provider, *tx_hash).await?,
180    }
181
182    Ok(())
183}