diff --git a/Cargo.lock b/Cargo.lock index 0b0eda12..746d773e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9343,6 +9343,7 @@ dependencies = [ "clap 3.2.25", "serde", "serde_derive", + "serde_json", "serde_with", "serial_test", "solana-account", diff --git a/Cargo.toml b/Cargo.toml index 432f433a..29121c61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ num-derive = "0.4.2" num-traits = "0.2.19" serde = "1.0.219" serde_derive = "1.0.219" +serde_json = "1.0.140" serde_with = "3.12.0" serial_test = "3.2.0" solana-account = "2.2.1" diff --git a/clients/cli/Cargo.toml b/clients/cli/Cargo.toml index bb213a49..7e0afdce 100644 --- a/clients/cli/Cargo.toml +++ b/clients/cli/Cargo.toml @@ -35,6 +35,7 @@ tokio = { workspace = true } [dev-dependencies] bytemuck = { workspace = true } +serde_json = { workspace = true } serial_test = { workspace = true } solana-keypair = { workspace = true } solana-sdk-ids = { workspace = true } diff --git a/clients/cli/src/cli.rs b/clients/cli/src/cli.rs index 54079fcd..d8d7841a 100644 --- a/clients/cli/src/cli.rs +++ b/clients/cli/src/cli.rs @@ -2,6 +2,7 @@ use { crate::{ config::Config, create_mint::{command_create_mint, CreateMintArgs}, + find_pdas::{command_get_pdas, FindPdasArgs}, output::parse_output_format, CommandResult, }, @@ -84,6 +85,7 @@ pub struct Cli { pub enum Command { /// Create a wrapped mint for a given SPL Token CreateMint(CreateMintArgs), + FindPdas(FindPdasArgs), // TODO: Wrap, Unwrap } @@ -96,6 +98,7 @@ impl Command { ) -> CommandResult { match self { Command::CreateMint(args) => command_create_mint(config, args).await, + Command::FindPdas(args) => command_get_pdas(config, args).await, } } } diff --git a/clients/cli/src/find_pdas.rs b/clients/cli/src/find_pdas.rs new file mode 100644 index 00000000..645bb313 --- /dev/null +++ b/clients/cli/src/find_pdas.rs @@ -0,0 +1,85 @@ +use { + crate::{ + common::{parse_pubkey, parse_token_program}, + config::Config, + output::format_output, + CommandResult, + }, + clap::Args, + serde_derive::{Deserialize, Serialize}, + serde_with::{serde_as, DisplayFromStr}, + solana_cli_output::{display::writeln_name_value, QuietDisplay, VerboseDisplay}, + solana_pubkey::Pubkey, + spl_token_wrap::{ + get_wrapped_mint_address, get_wrapped_mint_authority, get_wrapped_mint_backpointer_address, + }, + std::fmt::{Display, Formatter}, +}; + +#[derive(Clone, Debug, Args)] +pub struct FindPdasArgs { + /// The address of the mint to wrap + #[clap(value_parser = parse_pubkey)] + pub unwrapped_mint: Pubkey, + + /// The address of the token program that the wrapped mint should belong to + #[clap(value_parser = parse_token_program)] + pub wrapped_token_program: Pubkey, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PdasOutput { + #[serde_as(as = "DisplayFromStr")] + pub wrapped_mint_address: Pubkey, + #[serde_as(as = "DisplayFromStr")] + pub wrapped_mint_authority: Pubkey, + #[serde_as(as = "DisplayFromStr")] + pub wrapped_backpointer_address: Pubkey, +} + +impl Display for PdasOutput { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln_name_value( + f, + "Wrapped mint address:", + &self.wrapped_mint_address.to_string(), + )?; + writeln_name_value( + f, + "Wrapped mint authority:", + &self.wrapped_mint_authority.to_string(), + )?; + writeln_name_value( + f, + "Wrapped backpointer address:", + &self.wrapped_backpointer_address.to_string(), + )?; + + Ok(()) + } +} + +impl QuietDisplay for PdasOutput { + fn write_str(&self, _: &mut dyn std::fmt::Write) -> std::fmt::Result { + Ok(()) + } +} +impl VerboseDisplay for PdasOutput {} + +pub async fn command_get_pdas(config: &Config, args: FindPdasArgs) -> CommandResult { + let wrapped_mint_address = + get_wrapped_mint_address(&args.unwrapped_mint, &args.wrapped_token_program); + let wrapped_backpointer_address = get_wrapped_mint_backpointer_address(&wrapped_mint_address); + let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint_address); + + Ok(format_output( + config, + PdasOutput { + wrapped_mint_address, + wrapped_mint_authority, + wrapped_backpointer_address, + }, + )) +} diff --git a/clients/cli/src/main.rs b/clients/cli/src/main.rs index fd72473c..44f2abf0 100644 --- a/clients/cli/src/main.rs +++ b/clients/cli/src/main.rs @@ -2,6 +2,7 @@ mod cli; mod common; mod config; mod create_mint; +mod find_pdas; mod output; use { diff --git a/clients/cli/tests/test_pdas.rs b/clients/cli/tests/test_pdas.rs new file mode 100644 index 00000000..697374fb --- /dev/null +++ b/clients/cli/tests/test_pdas.rs @@ -0,0 +1,55 @@ +use { + crate::helpers::TOKEN_WRAP_CLI_BIN, + solana_pubkey::Pubkey, + spl_token_wrap::{ + self, get_wrapped_mint_address, get_wrapped_mint_authority, + get_wrapped_mint_backpointer_address, + }, + std::{process::Command, str::FromStr}, +}; + +pub mod helpers; + +#[test] +fn test_pdas() { + let unwrapped_mint = Pubkey::from_str("FSi5sv14NFh71zDpBx1Ee1EFtDYRnN2fNYu7ixsPKNzJ").unwrap(); + + // Execute the pdas command with JSON output + let mut command = Command::new(TOKEN_WRAP_CLI_BIN); + let output = command + .args([ + "find-pdas", + &unwrapped_mint.to_string(), + &spl_token_2022::id().to_string(), + "--output", + "json", + ]) + .output() + .unwrap(); + assert!(output.status.success()); + + // Parse the JSON output + let output_str = String::from_utf8(output.stdout).unwrap(); + let json_result: serde_json::Value = serde_json::from_str(&output_str).unwrap(); + + // Calculate the expected addresses + let expected_wrapped_mint = get_wrapped_mint_address(&unwrapped_mint, &spl_token_2022::id()); + let expected_authority = get_wrapped_mint_authority(&expected_wrapped_mint); + let expected_backpointer = get_wrapped_mint_backpointer_address(&expected_wrapped_mint); + + // Verify the JSON values match the expected addresses + assert_eq!( + json_result["wrappedMintAddress"].as_str().unwrap(), + expected_wrapped_mint.to_string(), + ); + + assert_eq!( + json_result["wrappedMintAuthority"].as_str().unwrap(), + expected_authority.to_string(), + ); + + assert_eq!( + json_result["wrappedBackpointerAddress"].as_str().unwrap(), + expected_backpointer.to_string(), + ); +}