yellowstone_shield_cli/
lib.rs

1mod command;
2
3use anyhow::{Context, Result};
4use bs58::decode;
5use clap_derive::{Parser as DeriveParser, Subcommand};
6use serde_json::from_str as parse_json_str;
7use solana_cli_config::Config;
8use solana_client::nonblocking::rpc_client::RpcClient;
9use solana_sdk::commitment_config::CommitmentConfig;
10use solana_sdk::pubkey::Pubkey;
11use solana_sdk::signature::Keypair;
12use std::fs::read_to_string as read_path;
13use std::path::PathBuf;
14use std::sync::Arc;
15use std::{str::FromStr, time::Duration};
16use yellowstone_shield_client::types::PermissionStrategy;
17
18pub use command::*;
19
20#[derive(Debug, DeriveParser)]
21#[command(
22    author,
23    version,
24    name = "Yellowstone Shield CLI",
25    about = "CLI for managing Yellowstone shield policies"
26)]
27pub struct Args {
28    /// RPC endpoint url to override using the Solana config
29    #[arg(short, long, global = true)]
30    pub rpc: Option<String>,
31
32    /// Log level
33    #[arg(short, long, global = true, default_value = "off")]
34    pub log_level: String,
35
36    /// Path to the owner keypair file
37    #[arg(short, long, global = true)]
38    pub keypair: Option<String>,
39
40    #[command(subcommand)]
41    pub command: Command,
42}
43
44#[derive(Subcommand, Debug)]
45pub enum Command {
46    /// Manage policies
47    Policy {
48        #[command(subcommand)]
49        action: PolicyAction,
50    },
51    /// Manage identities
52    Identities {
53        #[command(subcommand)]
54        action: IdentitiesAction,
55    },
56}
57
58#[derive(Subcommand, Debug)]
59pub enum PolicyAction {
60    /// Create a new policy
61    Create {
62        /// The strategy to use for the policy
63        #[arg(long)]
64        strategy: PermissionStrategy,
65
66        /// The name of the policy
67        #[arg(long)]
68        name: String,
69
70        /// The symbol of the policy
71        #[arg(long)]
72        symbol: String,
73
74        /// The URI of the policy
75        #[arg(long)]
76        uri: String,
77    },
78    /// Delete a policy
79    Delete {
80        /// The mint address associated with the policy
81        #[arg(long)]
82        mint: Pubkey,
83    },
84    /// Show policy details
85    Show {
86        /// The mint address associated with the policy
87        #[arg(long)]
88        mint: Pubkey,
89    },
90}
91
92#[derive(Subcommand, Debug)]
93pub enum IdentitiesAction {
94    /// Add identities to a policy
95    Add {
96        /// The mint address associated with the policy
97        #[arg(long)]
98        mint: Pubkey,
99        /// The identities to add to the policy
100        #[arg(long)]
101        identities_path: PathBuf,
102    },
103    /// Remove identities from a policy
104    Remove {
105        /// The mint address associated with the policy
106        #[arg(long)]
107        mint: Pubkey,
108        /// The identities to remove from the policy
109        #[arg(long)]
110        identities_path: PathBuf,
111    },
112}
113
114#[derive(thiserror::Error, Debug)]
115pub enum CliError {
116    #[error("unable to get config file path")]
117    ConfigFilePathError,
118    #[error(transparent)]
119    Io(#[from] std::io::Error),
120    #[error(transparent)]
121    Other(#[from] anyhow::Error),
122    #[error(transparent)]
123    ParseCommitmentLevelError(#[from] solana_sdk::commitment_config::ParseCommitmentLevelError),
124    #[error("unable to parse keypair")]
125    Keypair,
126}
127
128pub async fn run(config: Arc<Config>, command: Command) -> RunResult {
129    let client = RpcClient::new_with_timeout_and_commitment(
130        config.json_rpc_url.clone(),
131        Duration::from_secs(90),
132        CommitmentConfig::from_str(&config.commitment).map_err::<CliError, _>(Into::into)?,
133    );
134    let keypair = parse_keypair(&config.keypair_path)?;
135    let context = command::CommandContext { keypair, client };
136
137    match &command {
138        Command::Policy { action } => match action {
139            PolicyAction::Create {
140                strategy,
141                name,
142                symbol,
143                uri,
144            } => {
145                policy::CreateCommandBuilder::new()
146                    .strategy(*strategy)
147                    .name(name.clone())
148                    .symbol(symbol.clone())
149                    .uri(uri.clone())
150                    .run(context)
151                    .await
152            }
153            PolicyAction::Delete { mint } => {
154                policy::DeleteCommandBuilder::new()
155                    .mint(mint)
156                    .run(context)
157                    .await
158            }
159            PolicyAction::Show { mint } => {
160                policy::ShowCommandBuilder::new()
161                    .mint(mint)
162                    .run(context)
163                    .await
164            }
165        },
166        Command::Identities { action } => match action {
167            IdentitiesAction::Add {
168                mint,
169                identities_path,
170            } => {
171                let identities: Vec<Pubkey> = read_path(identities_path)?
172                    .lines()
173                    .filter_map(|s| Pubkey::from_str(s.trim()).ok())
174                    .collect();
175
176                identity::AddBatchCommandBuilder::new()
177                    .mint(mint)
178                    .identities(identities)
179                    .run(context)
180                    .await
181            }
182            IdentitiesAction::Remove {
183                mint,
184                identities_path,
185            } => {
186                let identities: Vec<Pubkey> = read_path(identities_path)?
187                    .lines()
188                    .filter_map(|s| Pubkey::from_str(s.trim()).ok())
189                    .collect();
190
191                identity::RemoveBatchCommandBuilder::new()
192                    .mint(mint)
193                    .identities(identities)
194                    .run(context)
195                    .await
196            }
197        },
198    }
199}
200
201fn parse_keypair(keypair_path: &str) -> Result<Keypair, CliError> {
202    let secret_string = read_path(keypair_path).context("Can't find key file")?;
203    let secret_bytes = parse_json_str(&secret_string)
204        .or_else(|_| decode(&secret_string.trim()).into_vec())
205        .map_err(|_| CliError::ConfigFilePathError)?;
206
207    Keypair::from_bytes(&secret_bytes).map_err(|_| CliError::Keypair)
208}