diff --git a/Cargo.lock b/Cargo.lock index 7f4bc06..01d4ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.5.3" @@ -635,6 +641,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -3364,7 +3379,7 @@ dependencies = [ "Inflector", "base64 0.21.7", "bincode", - "bs58", + "bs58 0.4.0", "bv", "lazy_static", "serde", @@ -3489,7 +3504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bfcde2fc6946c99c7e3400fadd04d1628d675bfd66cb34d461c0f3224bd27d1" dependencies = [ "block-buffer 0.10.4", - "bs58", + "bs58 0.4.0", "bv", "either", "generic-array", @@ -3623,7 +3638,7 @@ dependencies = [ "borsh 0.10.4", "borsh 0.9.3", "borsh 1.5.0", - "bs58", + "bs58 0.4.0", "bv", "bytemuck", "cc", @@ -3779,7 +3794,7 @@ dependencies = [ "async-trait", "base64 0.21.7", "bincode", - "bs58", + "bs58 0.4.0", "indicatif", "log", "reqwest", @@ -3803,7 +3818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617df2c53f948c821cefca6824e376aac04ff0d844bb27f4d3ada9e211bcffe7" dependencies = [ "base64 0.21.7", - "bs58", + "bs58 0.4.0", "jsonrpc-core", "reqwest", "semver", @@ -3842,7 +3857,7 @@ dependencies = [ "bincode", "bitflags 2.6.0", "borsh 1.5.0", - "bs58", + "bs58 0.4.0", "bytemuck", "byteorder", "chrono", @@ -3892,7 +3907,7 @@ version = "1.18.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a8613ca80150f7e277e773620ba65d2c5fcc3a08eb8026627d601421ab43aef" dependencies = [ - "bs58", + "bs58 0.4.0", "proc-macro2", "quote", "rustversion", @@ -3987,7 +4002,7 @@ dependencies = [ "base64 0.21.7", "bincode", "borsh 0.10.4", - "bs58", + "bs58 0.4.0", "lazy_static", "log", "serde", @@ -4022,7 +4037,10 @@ name = "solana-verify" version = "0.3.1" dependencies = [ "anyhow", + "base64 0.22.1", + "bincode", "borsh 1.5.0", + "bs58 0.5.1", "cargo-lock", "cargo_toml", "clap 2.34.0", @@ -4042,6 +4060,7 @@ dependencies = [ "solana-cli-config", "solana-client", "solana-sdk", + "solana-transaction-status", "tokio", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index c5ab689..c2cd603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,10 @@ solana-client = "=1.18.23" solana-sdk = "=1.18.23" tokio = { version = "1.29.1", features = ["full"] } solana-account-decoder = "1.18.23" +bincode = "1.3.3" +bs58 = "0.5.1" +base64 = "0.22.1" +solana-transaction-status = "=1.18.23" [dependencies.uuid] version = "1.2.2" diff --git a/src/api/client.rs b/src/api/client.rs index 0ef73c1..4207d20 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -5,7 +5,7 @@ use reqwest::{Client, Response}; use serde_json::json; use solana_client::rpc_client::RpcClient; use solana_sdk::pubkey::Pubkey; -use std::sync::atomic::{Ordering}; +use std::sync::atomic::Ordering; use std::thread; use std::time::{Duration, Instant}; @@ -190,7 +190,7 @@ pub async fn handle_submission_response( break; // Exit the loop and continue with normal error handling } - let status = check_job_status(&client, &request_id).await?; + let status = check_job_status(client, &request_id).await?; match status.status { JobStatus::InProgress => { if SIGNAL_RECEIVED.load(Ordering::Relaxed) { @@ -266,7 +266,7 @@ pub async fn handle_submission_response( async fn check_job_status(client: &Client, request_id: &str) -> anyhow::Result { // Get /job/:id let response = client - .get(&format!("{}/job/{}", REMOTE_SERVER_URL, request_id)) + .get(format!("{}/job/{}", REMOTE_SERVER_URL, request_id)) .send() .await .unwrap(); @@ -309,11 +309,7 @@ pub async fn get_remote_status(program_id: Pubkey) -> anyhow::Result<()> { .build()?; let response = client - .get(format!( - "{}/status-all/{}", - REMOTE_SERVER_URL, - program_id.to_string() - )) + .get(format!("{}/status-all/{}", REMOTE_SERVER_URL, program_id,)) .send() .await?; diff --git a/src/main.rs b/src/main.rs index 0564465..3840bcf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,23 @@ use anyhow::anyhow; -use api::{get_remote_job, get_remote_status, send_job_with_uploader_to_remote}; +use api::{ + get_last_deployed_slot, get_remote_job, get_remote_status, send_job_with_uploader_to_remote, +}; +use base64::{prelude::BASE64_STANDARD, Engine}; +use bincode::serialize; use cargo_lock::Lockfile; use cargo_toml::Manifest; -use clap::{App, AppSettings, Arg, SubCommand}; +use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use signal_hook::{ consts::{SIGINT, SIGTERM}, iterator::Signals, }; use solana_cli_config::{Config, CONFIG_FILE}; use solana_client::rpc_client::RpcClient; -use solana_program::{get_all_pdas_available, get_program_pda, resolve_rpc_url, OtterBuildParams}; use solana_sdk::{ bpf_loader_upgradeable::{self, UpgradeableLoaderState}, pubkey::Pubkey, }; +use solana_transaction_status::UiTransactionEncoding; use std::{ io::Read, path::PathBuf, @@ -35,7 +39,11 @@ mod test; use crate::{ api::send_job_to_remote, - solana_program::{process_close, upload_program}, + solana_program::{ + compose_transaction, find_build_params_pda, get_all_pdas_available, get_program_pda, + process_close, resolve_rpc_url, upload_program_verification_data, InputParams, + OtterBuildParams, OtterVerifyInstructions, + }, }; const MAINNET_GENESIS_HASH: &str = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d"; @@ -219,7 +227,7 @@ async fn main() -> anyhow::Result<()> { .arg(Arg::with_name("skip-prompt") .short("y") .long("skip-prompt") - .help("Skip the prompt to upload a new program")) + .help("Skip the prompt to write verify data on chain without user confirmation")) .arg(Arg::with_name("keypair") .short("k") .long("keypair") @@ -233,6 +241,51 @@ async fn main() -> anyhow::Result<()> { .long("skip-build") .help("Skip building and verification, only upload the PDA") .takes_value(false))) + .subcommand(SubCommand::with_name("export-pda-tx") + .about("Export the transaction as base58 for use with Squads") + .arg(Arg::with_name("uploader") + .long("uploader") + .takes_value(true) + .required(true) + .help("Specifies an address to use for uploading the program verification args (should be the program authority)")) + .arg(Arg::with_name("encoding") + .long("encoding") + .takes_value(true) + .default_value("base58") + .possible_values(&["base58", "base64"]) + .help("The encoding to use for the transaction")) + .arg(Arg::with_name("mount-path") + .long("mount-path") + .takes_value(true) + .default_value("") + .help("Relative path to the root directory or the source code repository from which to build the program")) + .arg(Arg::with_name("repo-url") + .required(true) + .help("The HTTPS URL of the repo to clone")) + .arg(Arg::with_name("commit-hash") + .long("commit-hash") + .takes_value(true) + .help("Commit hash to checkout. Required to know the correct program snapshot. Will fallback to HEAD if not provided")) + .arg(Arg::with_name("program-id") + .long("program-id") + .required(true) + .takes_value(true) + .help("The Program ID of the program to verify")) + .arg(Arg::with_name("base-image") + .short("b") + .long("base-image") + .takes_value(true) + .help("Optionally specify a custom base docker image to use for building")) + .arg(Arg::with_name("library-name") + .long("library-name") + .takes_value(true) + .help("Specify the name of the library to build and verify")) + .arg(Arg::with_name("bpf") + .long("bpf") + .help("If the program requires cargo build-bpf (instead of cargo build-sbf), set this flag")) + .arg(Arg::with_name("current-dir") + .long("current-dir") + .help("Verify in current directory"))) .subcommand(SubCommand::with_name("close") .about("Close the otter-verify PDA account associated with the given program ID") .arg(Arg::with_name("program-id") @@ -240,6 +293,10 @@ async fn main() -> anyhow::Result<()> { .required(true) .takes_value(true) .help("The address of the program to close the PDA"))) + .arg(Arg::with_name("export") + .long("export") + .required(false) + .help("Print the transaction as base58 for use with Squads")) .subcommand(SubCommand::with_name("list-program-pdas") .about("List all the PDA information associated with a program ID. Requires custom RPC endpoint") .arg(Arg::with_name("program-id") @@ -372,19 +429,7 @@ async fn main() -> anyhow::Result<()> { .map(|s| s.to_string()) .collect(); - let commit_hash = sub_m - .value_of("commit-hash") - .map(String::from) - .or_else(|| { - get_commit_hash_from_remote(&repo_url).ok() // Dynamically determine commit hash from remote - }) - .ok_or_else(|| { - anyhow::anyhow!( - "Commit hash must be provided or inferred from the remote repository" - ) - })?; - - println!("Commit hash from remote: {}", commit_hash); + let commit_hash = get_commit_hash(sub_m, &repo_url)?; println!("Skipping prompt: {}", skip_prompt); verify_from_repo( @@ -423,6 +468,57 @@ async fn main() -> anyhow::Result<()> { ) .await } + ("export-pda-tx", Some(sub_m)) => { + let uploader = sub_m.value_of("uploader").unwrap(); + let mount_path = sub_m.value_of("mount-path").map(|s| s.to_string()).unwrap(); + let repo_url = sub_m.value_of("repo-url").map(|s| s.to_string()).unwrap(); + let program_id = sub_m.value_of("program-id").unwrap(); + let base_image = sub_m.value_of("base-image").map(|s| s.to_string()); + let library_name = sub_m.value_of("library-name").map(|s| s.to_string()); + let bpf_flag = sub_m.is_present("bpf"); + let encoding = sub_m.value_of("encoding").unwrap(); + + let encoding: UiTransactionEncoding = match encoding { + "base58" => UiTransactionEncoding::Base58, + "base64" => UiTransactionEncoding::Base64, + _ => { + return Err(anyhow!("Unsupported encoding: {}", encoding)); + } + }; + + let compute_unit_price = matches + .value_of("compute-unit-price") + .unwrap() + .parse::() + .unwrap_or(100000); + + let commit_hash = get_commit_hash(sub_m, &repo_url)?; + let cargo_args: Vec = sub_m + .values_of("cargo-args") + .unwrap_or_default() + .map(|s| s.to_string()) + .collect(); + + let connection = resolve_rpc_url(matches.value_of("url").map(|s| s.to_string()))?; + println!("Using connection url: {}", connection.url()); + + export_pda_tx( + &connection, + Pubkey::try_from(program_id)?, + Pubkey::try_from(uploader)?, + repo_url, + commit_hash, + mount_path, + library_name, + base_image, + bpf_flag, + &mut temp_dir, + encoding, + cargo_args, + compute_unit_price, + ) + .await + } ("list-program-pdas", Some(sub_m)) => { let program_id = sub_m.value_of("program-id").unwrap(); list_program_pdas(Pubkey::try_from(program_id)?, &connection).await @@ -935,97 +1031,23 @@ pub fn verify_from_image( Ok(()) } -#[allow(clippy::too_many_arguments)] -pub async fn verify_from_repo( - remote: bool, - relative_mount_path: String, - connection: &RpcClient, - repo_url: String, - commit_hash: Option, - program_id: Pubkey, - base_image: Option, +fn build_args( + relative_mount_path: &str, library_name_opt: Option, + verify_tmp_root_path: &str, + base_image: Option, bpf_flag: bool, cargo_args: Vec, - current_dir: bool, - skip_prompt: bool, - path_to_keypair: Option, - compute_unit_price: u64, - mut skip_build: bool, - container_id_opt: &mut Option, - temp_dir_opt: &mut Option, - check_signal: &dyn Fn(&mut Option, &mut Option), -) -> anyhow::Result<()> { - // Set skip_build to true if remote is true - skip_build |= remote; - - // Create a Vec to store solana-verify args - let mut args: Vec<&str> = Vec::new(); - - // Get source code from repo_url - let base_name = std::process::Command::new("basename") - .arg(&repo_url) - .output() - .map_err(|e| anyhow!("Failed to get basename of repo_url: {:?}", e)) - .and_then(|output| parse_output(output.stdout))?; - - let uuid = Uuid::new_v4().to_string(); - - // Create a temporary directory to clone the repo into - let verify_dir = if current_dir { - format!( - "{}/.{}", - std::env::current_dir()? - .as_os_str() - .to_str() - .ok_or_else(|| anyhow::Error::msg("Invalid path string"))?, - uuid.clone() - ) - } else { - format!("/tmp/solana-verify/{}", uuid) - }; - - temp_dir_opt.replace(verify_dir.clone()); - - let verify_tmp_root_path = format!("{}/{}", verify_dir, base_name); - println!("Cloning repo into: {}", verify_tmp_root_path); - - check_signal(container_id_opt, temp_dir_opt); - std::process::Command::new("git") - .args(["clone", &repo_url, &verify_tmp_root_path]) - .stdout(Stdio::inherit()) - .output()?; - - check_signal(container_id_opt, temp_dir_opt); - - // Checkout a specific commit hash, if provided - if let Some(commit_hash) = commit_hash.as_ref() { - let result = std::process::Command::new("git") - .args(["-C", &verify_tmp_root_path]) - .args(["checkout", commit_hash]) - .output() - .map_err(|e| anyhow!("Failed to checkout commit hash: {:?}", e)); - if result.is_ok() { - println!("Checked out commit hash: {}", commit_hash); - } else { - std::process::Command::new("rm") - .args(["-rf", verify_dir.as_str()]) - .output()?; - Err(anyhow!("Encountered error in git setup: {:?}", result))?; - } - } - - check_signal(container_id_opt, temp_dir_opt); - +) -> anyhow::Result<(Vec, String, String)> { + let mut args: Vec = Vec::new(); if !relative_mount_path.is_empty() { - args.push("--mount-path"); - args.push(&relative_mount_path); + args.push("--mount-path".to_string()); + args.push(relative_mount_path.to_string()); } // Get the absolute build path to the solana program directory to build inside docker - let mount_path = PathBuf::from(verify_tmp_root_path.clone()).join(&relative_mount_path); - println!("Build path: {:?}", mount_path); + let mount_path = PathBuf::from(verify_tmp_root_path).join(relative_mount_path); - args.push("--library-name"); + args.push("--library-name".to_string()); let library_name = match library_name_opt.clone() { Some(p) => p, None => { @@ -1067,28 +1089,143 @@ pub async fn verify_from_repo( })? } }; - args.push(&library_name); - println!("Verifying program: {}", library_name); + args.push(library_name.clone()); if let Some(base_image) = &base_image { - args.push("--base-image"); - args.push(base_image); + args.push("--base-image".to_string()); + args.push(base_image.clone()); } if bpf_flag { - args.push("--bpf"); + args.push("--bpf".to_string()); } if !cargo_args.is_empty() { - args.push("--"); + args.push("--".to_string()); for arg in &cargo_args { - args.push(arg); + args.push(arg.clone()); + } + } + + Ok((args, mount_path.to_str().unwrap().to_string(), library_name)) +} + +fn clone_repo_and_checkout( + repo_url: &str, + current_dir: bool, + base_name: &str, + commit_hash: Option, + temp_dir_opt: &mut Option, +) -> anyhow::Result<(String, String)> { + let uuid = Uuid::new_v4().to_string(); + + // Create a temporary directory to clone the repo into + let verify_dir = if current_dir { + format!( + "{}/.{}", + std::env::current_dir()? + .as_os_str() + .to_str() + .ok_or_else(|| anyhow::Error::msg("Invalid path string"))?, + uuid.clone() + ) + } else { + format!("/tmp/solana-verify/{}", uuid) + }; + + temp_dir_opt.replace(verify_dir.clone()); + + let verify_tmp_root_path = format!("{}/{}", verify_dir, base_name); + println!("Cloning repo into: {}", verify_tmp_root_path); + + std::process::Command::new("git") + .args(["clone", repo_url, &verify_tmp_root_path]) + .stdout(Stdio::inherit()) + .output()?; + + if let Some(commit_hash) = commit_hash.as_ref() { + let result = std::process::Command::new("git") + .args(["-C", &verify_tmp_root_path]) + .args(["checkout", commit_hash]) + .output() + .map_err(|e| anyhow!("Failed to checkout commit hash: {:?}", e)); + if result.is_ok() { + println!("Checked out commit hash: {}", commit_hash); + } else { + std::process::Command::new("rm") + .args(["-rf", verify_dir.as_str()]) + .output()?; + Err(anyhow!("Encountered error in git setup: {:?}", result))?; } } + Ok((verify_tmp_root_path, verify_dir)) +} + +fn get_basename(repo_url: &str) -> anyhow::Result { + let base_name = std::process::Command::new("basename") + .arg(repo_url) + .output() + .map_err(|e| anyhow!("Failed to get basename of repo_url: {:?}", e)) + .and_then(|output| parse_output(output.stdout))?; + Ok(base_name) +} + +#[allow(clippy::too_many_arguments)] +pub async fn verify_from_repo( + remote: bool, + relative_mount_path: String, + connection: &RpcClient, + repo_url: String, + commit_hash: Option, + program_id: Pubkey, + base_image: Option, + library_name_opt: Option, + bpf_flag: bool, + cargo_args: Vec, + current_dir: bool, + skip_prompt: bool, + path_to_keypair: Option, + compute_unit_price: u64, + mut skip_build: bool, + container_id_opt: &mut Option, + temp_dir_opt: &mut Option, + check_signal: &dyn Fn(&mut Option, &mut Option), +) -> anyhow::Result<()> { + // Set skip_build to true if remote is true + skip_build |= remote; + + // Get source code from repo_url + let base_name = get_basename(&repo_url)?; + + check_signal(container_id_opt, temp_dir_opt); + + let (verify_tmp_root_path, verify_dir) = clone_repo_and_checkout( + &repo_url, + current_dir, + &base_name, + commit_hash.clone(), + temp_dir_opt, + )?; + + check_signal(container_id_opt, temp_dir_opt); + + let (args, mount_path, library_name) = build_args( + &relative_mount_path, + library_name_opt.clone(), + &verify_tmp_root_path, + base_image.clone(), + bpf_flag, + cargo_args.clone(), + )?; + println!("Build path: {:?}", mount_path); + println!("Verifying program: {}", library_name); + + check_signal(container_id_opt, temp_dir_opt); + let result: Result<(String, String), anyhow::Error> = if !skip_build { build_and_verify_repo( - mount_path.to_str().unwrap().to_string(), + mount_path, base_image.clone(), bpf_flag, library_name.clone(), @@ -1102,11 +1239,9 @@ pub async fn verify_from_repo( }; // Cleanup no matter the result - if !skip_build { - std::process::Command::new("rm") - .args(["-rf", &verify_dir]) - .output()?; - } + std::process::Command::new("rm") + .args(["-rf", &verify_dir]) + .output()?; // Handle the result match result { @@ -1118,15 +1253,15 @@ pub async fn verify_from_repo( if skip_build || build_hash == program_hash { if skip_build { - println!("Skipping local build and uploading program"); + println!("Skipping local build and writing verify data on chain"); } else { println!("Program hash matches ✅"); } - upload_program( + upload_program_verification_data( repo_url.clone(), &commit_hash.clone(), - args.iter().map(|&s| s.into()).collect(), + args.iter().map(|s| s.to_string()).collect(), program_id, connection, skip_prompt, @@ -1269,7 +1404,7 @@ pub fn print_build_params(pubkey: &Pubkey, build_params: &OtterBuildParams) { } pub async fn list_program_pdas(program_id: Pubkey, client: &RpcClient) -> anyhow::Result<()> { - let pdas = get_all_pdas_available(&client, &program_id).await?; + let pdas = get_all_pdas_available(client, &program_id).await?; for (pda, build_params) in pdas { print_build_params(&pda, &build_params); } @@ -1281,7 +1416,109 @@ pub async fn print_program_pda( signer: Option, client: &RpcClient, ) -> anyhow::Result<()> { - let (pda, build_params) = get_program_pda(&client, &program_id, signer).await?; + let (pda, build_params) = get_program_pda(client, &program_id, signer).await?; print_build_params(&pda, &build_params); Ok(()) } + +pub fn get_commit_hash(sub_m: &ArgMatches, repo_url: &str) -> anyhow::Result { + let commit_hash = sub_m + .value_of("commit-hash") + .map(String::from) + .or_else(|| { + get_commit_hash_from_remote(repo_url).ok() // Dynamically determine commit hash from remote + }) + .ok_or_else(|| { + anyhow::anyhow!("Commit hash must be provided or inferred from the remote repository") + })?; + + println!("Commit hash from remote: {}", commit_hash); + Ok(commit_hash) +} + +#[allow(clippy::too_many_arguments)] +async fn export_pda_tx( + connection: &RpcClient, + program_id: Pubkey, + uploader: Pubkey, + repo_url: String, + commit_hash: String, + mount_path: String, + library_name: Option, + base_image: Option, + bpf_flag: bool, + temp_dir: &mut Option, + encoding: UiTransactionEncoding, + cargo_args: Vec, + compute_unit_price: u64, +) -> anyhow::Result<()> { + let last_deployed_slot = get_last_deployed_slot(connection, &program_id.to_string()) + .await + .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?; + + let (temp_root_path, verify_dir) = clone_repo_and_checkout( + &repo_url, + true, + &get_basename(&repo_url)?, + Some(commit_hash.clone()), + temp_dir, + )?; + + let input_params = InputParams { + version: env!("CARGO_PKG_VERSION").to_string(), + git_url: repo_url, + commit: commit_hash.clone(), + args: build_args( + &mount_path, + library_name.clone(), + &temp_root_path, + base_image.clone(), + bpf_flag, + cargo_args, + )? + .0, + deployed_slot: last_deployed_slot, + }; + + std::process::Command::new("rm") + .args(["-rf", &verify_dir]) + .output()?; + + let (pda, _) = find_build_params_pda(&program_id, &uploader); + + // check if account already exists + let instruction = match connection.get_account(&pda) { + Ok(account_info) => { + if !account_info.data.is_empty() { + println!("PDA already exists, creating update transaction"); + OtterVerifyInstructions::Update + } else { + println!("PDA does not exist, creating initialize transaction"); + OtterVerifyInstructions::Initialize + } + } + Err(_) => OtterVerifyInstructions::Initialize, + }; + + let tx = compose_transaction( + &input_params, + uploader, + pda, + program_id, + instruction, + compute_unit_price, + ); + + // serialize the transaction to base58 + match encoding { + UiTransactionEncoding::Base58 => { + println!("{}", bs58::encode(serialize(&tx)?).into_string()); + } + UiTransactionEncoding::Base64 => { + println!("{}", BASE64_STANDARD.encode(serialize(&tx)?)); + } + _ => unreachable!(), + } + + Ok(()) +} diff --git a/src/solana_program.rs b/src/solana_program.rs index 76e52f8..3aa12f9 100644 --- a/src/solana_program.rs +++ b/src/solana_program.rs @@ -12,7 +12,8 @@ use std::{ use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::AccountMeta, message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer, system_program, transaction::Transaction + compute_budget::ComputeBudgetInstruction, instruction::AccountMeta, message::Message, + pubkey::Pubkey, signature::Keypair, signer::Signer, system_program, transaction::Transaction, }; use solana_account_decoder::UiAccountEncoding; @@ -92,7 +93,7 @@ fn create_ix_data(params: &InputParams, ix: &OtterVerifyInstructions) -> Vec } fn get_keypair_from_path(path: &str) -> anyhow::Result { - solana_clap_utils::keypair::keypair_from_path(&Default::default(), &path, "keypair", false) + solana_clap_utils::keypair::keypair_from_path(&Default::default(), path, "keypair", false) .map_err(|err| anyhow!("Unable to get signer from path: {}", err)) } @@ -108,24 +109,14 @@ fn get_user_config() -> anyhow::Result<(Keypair, RpcClient)> { Ok((signer, rpc_client)) } -fn process_otter_verify_ixs( +pub fn compose_transaction( params: &InputParams, + signer_pubkey: Pubkey, pda_account: Pubkey, program_address: Pubkey, instruction: OtterVerifyInstructions, - rpc_client: &RpcClient, - path_to_keypair: Option, compute_unit_price: u64, -) -> anyhow::Result<()> { - let user_config = get_user_config()?; - let signer = if let Some(path_to_keypair) = path_to_keypair { - get_keypair_from_path(&path_to_keypair)? - } else { - user_config.0 - }; - let signer_pubkey = signer.pubkey(); - let connection = rpc_client; - +) -> Transaction { let ix_data = if instruction != OtterVerifyInstructions::Close { create_ix_data(params, &instruction) } else { @@ -150,13 +141,41 @@ fn process_otter_verify_ixs( let message = if compute_unit_price > 0 { // Add compute budget instruction for priority fees only if price > 0 - let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price); + let compute_budget_ix = + ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price); Message::new(&[compute_budget_ix, ix], Some(&signer_pubkey)) } else { Message::new(&[ix], Some(&signer_pubkey)) }; - let mut tx = Transaction::new_unsigned(message); + Transaction::new_unsigned(message) +} + +fn process_otter_verify_ixs( + params: &InputParams, + pda_account: Pubkey, + program_address: Pubkey, + instruction: OtterVerifyInstructions, + rpc_client: &RpcClient, + path_to_keypair: Option, + compute_unit_price: u64, +) -> anyhow::Result<()> { + let user_config = get_user_config()?; + let signer = if let Some(path_to_keypair) = path_to_keypair { + get_keypair_from_path(&path_to_keypair)? + } else { + user_config.0 + }; + let connection = rpc_client; + + let mut tx = compose_transaction( + params, + signer.pubkey(), + pda_account, + program_address, + instruction, + compute_unit_price, + ); tx.sign(&[&signer], connection.get_latest_blockhash()?); @@ -189,7 +208,8 @@ pub fn resolve_rpc_url(url: Option) -> anyhow::Result { Ok(connection) } -pub async fn upload_program( +#[allow(clippy::too_many_arguments)] +pub async fn upload_program_verification_data( git_url: String, commit: &Option, args: Vec, @@ -209,7 +229,7 @@ pub async fn upload_program( let cli_config = get_user_config()?; let signer_pubkey: Pubkey = if let Some(ref path_to_keypair) = path_to_keypair { - get_keypair_from_path(&path_to_keypair)?.pubkey() + get_keypair_from_path(path_to_keypair)?.pubkey() } else { cli_config.0.pubkey() }; @@ -217,7 +237,7 @@ pub async fn upload_program( // let rpc_url = connection.url(); println!("Using connection url: {}", connection.url()); - let last_deployed_slot = get_last_deployed_slot(&connection, &program_address.to_string()) + let last_deployed_slot = get_last_deployed_slot(connection, &program_address.to_string()) .await .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?; @@ -282,7 +302,7 @@ pub async fn upload_program( Ok(()) } -fn find_build_params_pda(program_id: &Pubkey, signer: &Pubkey) -> (Pubkey, u8) { +pub fn find_build_params_pda(program_id: &Pubkey, signer: &Pubkey) -> (Pubkey, u8) { let seeds: &[&[u8]; 3] = &[b"otter_verify", &signer.to_bytes(), &program_id.to_bytes()]; Pubkey::find_program_address(seeds, &OTTER_VERIFY_PROGRAM_ID) } @@ -296,7 +316,7 @@ pub async fn process_close( let signer = user_config.0; let signer_pubkey = signer.pubkey(); - let last_deployed_slot = get_last_deployed_slot(&connection, &program_address.to_string()) + let last_deployed_slot = get_last_deployed_slot(connection, &program_address.to_string()) .await .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?;