Skip to content

Commit 8e7eb65

Browse files
authored
Merge pull request #97 from Ellipsis-Labs/keypair-vfr
Add `list-program-pdas`
2 parents 280963c + 6924d09 commit 8e7eb65

File tree

4 files changed

+145
-19
lines changed

4 files changed

+145
-19
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ solana-cli-config = "=1.18.23"
2828
solana-client = "=1.18.23"
2929
solana-sdk = "=1.18.23"
3030
tokio = { version = "1.29.1", features = ["full"] }
31+
solana-account-decoder = "1.18.23"
3132

3233
[dependencies.uuid]
3334
version = "1.2.2"

src/main.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use signal_hook::{
88
};
99
use solana_cli_config::{Config, CONFIG_FILE};
1010
use solana_client::rpc_client::RpcClient;
11+
use solana_program::get_all_pdas_available;
1112
use solana_sdk::{
1213
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
1314
pubkey::Pubkey,
@@ -178,6 +179,11 @@ async fn main() -> anyhow::Result<()> {
178179
.short("y")
179180
.long("skip-prompt")
180181
.help("Skip the prompt to upload a new program"))
182+
.arg(Arg::with_name("keypair")
183+
.short("k")
184+
.long("keypair")
185+
.takes_value(true)
186+
.help("Optionally specify a keypair to use for uploading the program verification args"))
181187
.arg(Arg::with_name("cargo-args")
182188
.multiple(true)
183189
.last(true)
@@ -189,6 +195,13 @@ async fn main() -> anyhow::Result<()> {
189195
.required(true)
190196
.takes_value(true)
191197
.help("The address of the program to close the PDA")))
198+
.subcommand(SubCommand::with_name("list-program-pdas")
199+
.about("List all the PDA information associated with a program ID")
200+
.arg(Arg::with_name("program-id")
201+
.long("program-id")
202+
.required(true)
203+
.takes_value(true)
204+
.help("Program ID of the program to list PDAs for")))
192205
.get_matches();
193206

194207
let res = match matches.subcommand() {
@@ -261,12 +274,14 @@ async fn main() -> anyhow::Result<()> {
261274
let bpf_flag = sub_m.is_present("bpf");
262275
let current_dir = sub_m.is_present("current-dir");
263276
let skip_prompt = sub_m.is_present("skip-prompt");
277+
let path_to_keypair = sub_m.value_of("keypair").map(|s| s.to_string());
264278
let cargo_args: Vec<String> = sub_m
265279
.values_of("cargo-args")
266280
.unwrap_or_default()
267281
.map(|s| s.to_string())
268282
.collect();
269283

284+
println!(" Skipping prompt: {}", skip_prompt);
270285
verify_from_repo(
271286
remote,
272287
mount_path,
@@ -280,6 +295,7 @@ async fn main() -> anyhow::Result<()> {
280295
cargo_args,
281296
current_dir,
282297
skip_prompt,
298+
path_to_keypair,
283299
&mut container_id,
284300
&mut temp_dir,
285301
)
@@ -289,6 +305,11 @@ async fn main() -> anyhow::Result<()> {
289305
let program_id = sub_m.value_of("program-id").unwrap();
290306
process_close(Pubkey::try_from(program_id)?).await
291307
}
308+
("list-program-pdas", Some(sub_m)) => {
309+
let program_id = sub_m.value_of("program-id").unwrap();
310+
let url = matches.value_of("url").map(|s| s.to_string());
311+
list_program_pdas(Pubkey::try_from(program_id)?, url).await
312+
}
292313
// Handle other subcommands in a similar manner, for now let's panic
293314
_ => panic!(
294315
"Unknown subcommand: {:?}\nUse '--help' to see available commands",
@@ -730,6 +751,7 @@ pub async fn verify_from_repo(
730751
cargo_args: Vec<String>,
731752
current_dir: bool,
732753
skip_prompt: bool,
754+
path_to_keypair: Option<String>,
733755
container_id_opt: &mut Option<String>,
734756
temp_dir_opt: &mut Option<String>,
735757
) -> anyhow::Result<()> {
@@ -760,7 +782,7 @@ pub async fn verify_from_repo(
760782
// Get the absolute build path to the solana program directory to build inside docker
761783
let mount_path = PathBuf::from(relative_mount_path.clone());
762784
println!("Build path: {:?}", mount_path);
763-
785+
764786
args.push("--library-name");
765787
let library_name = match library_name_opt {
766788
Some(p) => p,
@@ -805,16 +827,16 @@ pub async fn verify_from_repo(
805827
};
806828
args.push(&library_name);
807829
println!("Verifying program: {}", library_name);
808-
830+
809831
if let Some(base_image) = &base_image {
810832
args.push("--base-image");
811833
args.push(base_image);
812834
}
813-
835+
814836
if bpf_flag {
815837
args.push("--bpf");
816838
}
817-
839+
818840
if !cargo_args.clone().is_empty() {
819841
args.push("--");
820842
for arg in &cargo_args {
@@ -829,6 +851,7 @@ pub async fn verify_from_repo(
829851
program_id,
830852
connection_url,
831853
skip_prompt,
854+
path_to_keypair,
832855
)
833856
.await;
834857
if x.is_err() {
@@ -989,6 +1012,7 @@ pub async fn verify_from_repo(
9891012
program_id,
9901013
connection_url,
9911014
skip_prompt,
1015+
path_to_keypair,
9921016
)
9931017
.await;
9941018
if x.is_err() {
@@ -1097,3 +1121,15 @@ pub fn get_pkg_name_from_cargo_toml(cargo_toml_file: &str) -> Option<String> {
10971121
let pkg = manifest.package?;
10981122
Some(pkg.name)
10991123
}
1124+
1125+
pub async fn list_program_pdas(program_id: Pubkey, url: Option<String>) -> anyhow::Result<()> {
1126+
let client = get_client(url);
1127+
let pdas = get_all_pdas_available(&client, &program_id).await?;
1128+
for (pda, build_params) in pdas {
1129+
println!("----------------------------------------------------------------");
1130+
println!("PDA: {:?}", pda);
1131+
println!("----------------------------------------------------------------");
1132+
println!("{}", build_params);
1133+
}
1134+
Ok(())
1135+
}

src/solana_program.rs

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use anyhow::anyhow;
22
use solana_cli_config::Config;
3-
use solana_client::rpc_client::RpcClient;
3+
use solana_client::{
4+
rpc_client::RpcClient,
5+
rpc_config::RpcProgramAccountsConfig,
6+
rpc_filter::{Memcmp, RpcFilterType},
7+
};
48
use std::{
59
io::{self, Read, Write},
610
str::FromStr,
@@ -12,11 +16,38 @@ use solana_sdk::{
1216
system_program, transaction::Transaction,
1317
};
1418

19+
use solana_account_decoder::UiAccountEncoding;
20+
use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel};
21+
1522
use crate::api::get_last_deployed_slot;
1623

1724
const OTTER_VERIFY_PROGRAMID: &str = "verifycLy8mB96wd9wqq3WDXQwM4oU6r42Th37Db9fC";
1825
const OTTER_SIGNER: &str = "9VWiUUhgNoRwTH5NVehYJEDwcotwYX3VgW4MChiHPAqU";
1926

27+
#[derive(BorshDeserialize, BorshSerialize, Debug)]
28+
pub struct OtterBuildParams {
29+
pub address: Pubkey,
30+
pub signer: Pubkey,
31+
pub version: String,
32+
pub git_url: String,
33+
pub commit: String,
34+
pub args: Vec<String>,
35+
pub deployed_slot: u64,
36+
bump: u8,
37+
}
38+
impl std::fmt::Display for OtterBuildParams {
39+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40+
writeln!(f, "Program Id: {}", self.address)?;
41+
writeln!(f, "Signer: {}", self.signer)?;
42+
writeln!(f, "Git Url: {}", self.git_url)?;
43+
writeln!(f, "Commit: {}", self.commit)?;
44+
writeln!(f, "Deployed Slot: {}", self.deployed_slot)?;
45+
writeln!(f, "Args: {:?}", self.args)?;
46+
writeln!(f, "Version: {}", self.version)?;
47+
Ok(())
48+
}
49+
}
50+
2051
pub fn prompt_user_input(message: &str) -> bool {
2152
let mut buffer = [0; 1];
2253
print!("{}", message);
@@ -60,19 +91,18 @@ fn create_ix_data(params: &InputParams, ix: &OtterVerifyInstructions) -> Vec<u8>
6091
data
6192
}
6293

94+
fn get_keypair_from_path(path: &str) -> anyhow::Result<Keypair> {
95+
solana_clap_utils::keypair::keypair_from_path(&Default::default(), &path, "keypair", false)
96+
.map_err(|err| anyhow!("Unable to get signer from path: {}", err))
97+
}
98+
6399
fn get_user_config() -> anyhow::Result<(Keypair, RpcClient)> {
64100
let config_file = solana_cli_config::CONFIG_FILE
65101
.as_ref()
66102
.ok_or_else(|| anyhow!("Unable to get config file path"))?;
67103
let cli_config: Config = Config::load(config_file)?;
68104

69-
let signer = solana_clap_utils::keypair::keypair_from_path(
70-
&Default::default(),
71-
&cli_config.keypair_path,
72-
"keypair",
73-
false,
74-
)
75-
.map_err(|err| anyhow!("Unable to get signer from path: {}", err))?;
105+
let signer = get_keypair_from_path(&cli_config.keypair_path)?;
76106

77107
let rpc_client = RpcClient::new(cli_config.json_rpc_url.clone());
78108
Ok((signer, rpc_client))
@@ -84,9 +114,14 @@ fn process_otter_verify_ixs(
84114
program_address: Pubkey,
85115
instruction: OtterVerifyInstructions,
86116
rpc_client: RpcClient,
117+
path_to_keypair: Option<String>,
87118
) -> anyhow::Result<()> {
88119
let user_config = get_user_config()?;
89-
let signer = user_config.0;
120+
let signer = if let Some(path_to_keypair) = path_to_keypair {
121+
get_keypair_from_path(&path_to_keypair)?
122+
} else {
123+
user_config.0
124+
};
90125
let signer_pubkey = signer.pubkey();
91126
let connection = rpc_client;
92127

@@ -135,15 +170,23 @@ pub async fn upload_program(
135170
program_address: Pubkey,
136171
connection_url: Option<String>,
137172
skip_prompt: bool,
173+
path_to_keypair: Option<String>,
138174
) -> anyhow::Result<()> {
139-
if skip_prompt || prompt_user_input(
140-
"Do you want to upload the program verification to the Solana Blockchain? (y/n) ",
141-
) {
175+
if skip_prompt
176+
|| prompt_user_input(
177+
"Do you want to upload the program verification to the Solana Blockchain? (y/n) ",
178+
)
179+
{
142180
println!("Uploading the program verification params to the Solana blockchain...");
143181

144182
let cli_config = get_user_config()?;
145183

146-
let signer_pubkey = cli_config.0.pubkey();
184+
let signer_pubkey: Pubkey = if let Some(ref path_to_keypair) = path_to_keypair {
185+
get_keypair_from_path(&path_to_keypair)?.pubkey()
186+
} else {
187+
cli_config.0.pubkey()
188+
};
189+
147190
let connection = match connection_url.as_deref() {
148191
Some("m") => RpcClient::new("https://api.mainnet-beta.solana.com"),
149192
Some("d") => RpcClient::new("https://api.devnet.solana.com"),
@@ -194,9 +237,10 @@ pub async fn upload_program(
194237
program_address,
195238
OtterVerifyInstructions::Update,
196239
connection,
240+
path_to_keypair,
197241
)?;
198242
} else if connection.get_account(&pda_account_2).is_ok() {
199-
let wanna_create_new_pda = prompt_user_input(
243+
let wanna_create_new_pda = skip_prompt || prompt_user_input(
200244
"Program already uploaded by another signer. Do you want to upload a new program? (Y/n)"
201245
);
202246
if wanna_create_new_pda {
@@ -206,6 +250,7 @@ pub async fn upload_program(
206250
program_address,
207251
OtterVerifyInstructions::Initialize,
208252
connection,
253+
path_to_keypair,
209254
)?;
210255
}
211256
return Ok(());
@@ -217,6 +262,7 @@ pub async fn upload_program(
217262
program_address,
218263
OtterVerifyInstructions::Initialize,
219264
connection,
265+
path_to_keypair,
220266
)?;
221267
}
222268
} else {
@@ -260,12 +306,54 @@ pub async fn process_close(program_address: Pubkey) -> anyhow::Result<()> {
260306
program_address,
261307
OtterVerifyInstructions::Close,
262308
connection,
309+
None,
263310
)?;
264311
} else {
265312
return Err(anyhow!(
266-
"Program account does not exist. Please provide the program address not PDA address."
313+
"No PDA found for signer {:?} and program address {:?}. Make sure you are providing the program address, not the PDA address. Check that a signer exists for the program by running `solana-verify list-program-pdas --program-id {:?}`",
314+
signer_pubkey,
315+
program_address,
316+
program_address
267317
));
268318
}
269319

270320
Ok(())
271321
}
322+
323+
pub async fn get_all_pdas_available(
324+
client: &RpcClient,
325+
program_id_pubkey: &Pubkey,
326+
) -> anyhow::Result<Vec<(Pubkey, OtterBuildParams)>> {
327+
let filter = vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
328+
8,
329+
&program_id_pubkey.to_bytes(),
330+
))];
331+
332+
let config = RpcProgramAccountsConfig {
333+
filters: Some(filter),
334+
account_config: solana_client::rpc_config::RpcAccountInfoConfig {
335+
encoding: Some(UiAccountEncoding::Base64),
336+
data_slice: None,
337+
commitment: Some(CommitmentConfig {
338+
commitment: CommitmentLevel::Confirmed,
339+
}),
340+
min_context_slot: None,
341+
},
342+
with_context: None,
343+
};
344+
345+
let accounts = client.get_program_accounts_with_config(
346+
&Pubkey::from_str(OTTER_VERIFY_PROGRAMID).unwrap(),
347+
config,
348+
)?;
349+
350+
let mut pdas = vec![];
351+
for account in accounts {
352+
let otter_build_params = OtterBuildParams::try_from_slice(&account.1.data[8..]);
353+
if let Ok(otter_build_params) = otter_build_params {
354+
pdas.push((account.0, otter_build_params));
355+
}
356+
}
357+
358+
Ok(pdas)
359+
}

0 commit comments

Comments
 (0)