From 06fec9e31f08cf1fa8e9a4ddb7db1483aa6c4275 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:00:38 +0900 Subject: [PATCH 1/3] Add `cow solver add` to the test CLI Drives the new `AddSolver` instruction from the dev CLI. The manager must sign, so `--manager` takes a keypair path rather than an address, and defaults to the payer that funds the state PDA's growth. Co-Authored-By: Claude Opus 5 (1M context) --- test-cli/src/cmd/mod.rs | 1 + test-cli/src/cmd/solver.rs | 93 ++++++++++++++++++++++++++++++++++++++ test-cli/src/main.rs | 3 ++ 3 files changed, 97 insertions(+) create mode 100644 test-cli/src/cmd/solver.rs diff --git a/test-cli/src/cmd/mod.rs b/test-cli/src/cmd/mod.rs index d93c6fe..73c28e7 100644 --- a/test-cli/src/cmd/mod.rs +++ b/test-cli/src/cmd/mod.rs @@ -9,6 +9,7 @@ use crate::Cli; pub mod create_order; pub mod initialize; pub mod settle; +pub mod solver; /// Shared context threaded through every subcommand. pub struct Context { diff --git a/test-cli/src/cmd/solver.rs b/test-cli/src/cmd/solver.rs new file mode 100644 index 0000000..e36ce55 --- /dev/null +++ b/test-cli/src/cmd/solver.rs @@ -0,0 +1,93 @@ +use anyhow::Context as _; +use clap::{Args as ClapArgs, Parser, Subcommand}; +use cow_settlement_client::{ + cow_settlement_interface::{pda::state::find_state_pda, Pubkey}, + instructions::AddSolver, +}; +use solana_sdk::{ + signature::{read_keypair_file, Signer}, + signer::keypair::Keypair, + transaction::Transaction, +}; + +use crate::helpers::print_summary; + +use super::Context; + +#[derive(Parser)] +pub struct SolverArgs { + #[command(subcommand)] + command: SolverCommand, +} + +#[derive(Subcommand)] +enum SolverCommand { + #[command(about = "Authorize a solver to settle orders")] + Add(AddArgs), +} + +#[derive(ClapArgs)] +pub struct AddArgs { + /// Address of the solver to authorize + solver: Pubkey, + + /// Path to the manager keypair, which authorizes the change and must sign + /// it (defaults to the payer keypair, which always funds the state PDA's + /// growth) + #[arg(long)] + manager: Option, +} + +pub fn run(ctx: Context, args: SolverArgs) -> anyhow::Result<()> { + match args.command { + SolverCommand::Add(args) => add(ctx, args), + } +} + +fn add(ctx: Context, args: AddArgs) -> anyhow::Result<()> { + let payer = ctx.payer.pubkey(); + // Without `--manager` the payer manages the program, so reuse the keypair + // the context already holds instead of reading a file again. + let from_file = args + .manager + .map(|path| { + read_keypair_file(&path) + .map_err(|e| anyhow::anyhow!("failed to read manager keypair from {path}: {e}")) + }) + .transpose()?; + let manager = from_file.as_ref().unwrap_or(&ctx.payer); + let manager_pubkey = manager.pubkey(); + + let ix = AddSolver { + program_id: ctx.program_id, + manager: manager_pubkey, + payer, + solver: args.solver, + }; + + // A manager that is the payer signs a single slot, so don't pass it twice. + let mut signers: Vec<&Keypair> = vec![&ctx.payer]; + if manager_pubkey != payer { + signers.push(manager); + } + + let blockhash = ctx + .rpc + .get_latest_blockhash() + .context("failed to fetch blockhash")?; + let tx = Transaction::new_signed_with_payer(&[ix.into()], Some(&payer), &signers, blockhash); + let sig = ctx + .rpc + .send_and_confirm_transaction(&tx) + .context("transaction failed")?; + + let (state_pda, _) = find_state_pda(&ctx.program_id); + print_summary(&[ + ("signature", &sig), + ("solver", &args.solver), + ("manager", &manager_pubkey), + ("statePda", &state_pda), + ]); + + Ok(()) +} diff --git a/test-cli/src/main.rs b/test-cli/src/main.rs index 352c309..e045a95 100644 --- a/test-cli/src/main.rs +++ b/test-cli/src/main.rs @@ -73,6 +73,8 @@ enum Commands { Buy(cmd::create_order::BuyOrSellArgs), #[command(about = "Settle one or more orders")] Settle(cmd::settle::SettleArgs), + #[command(about = "Manage the program's solvers (e.g. `cow solver add
`)")] + Solver(cmd::solver::SolverArgs), } fn main() -> anyhow::Result<()> { @@ -83,5 +85,6 @@ fn main() -> anyhow::Result<()> { Commands::Sell(args) => cmd::create_order::run_sell(ctx, args), Commands::Buy(args) => cmd::create_order::run_buy(ctx, args), Commands::Settle(args) => cmd::settle::run(ctx, args), + Commands::Solver(args) => cmd::solver::run(ctx, args), } } From 6124ad0d2994a828c4a6fdce305fec66ad932e8e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:26:25 +0900 Subject: [PATCH 2/3] Update test-cli/src/cmd/solver.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- test-cli/src/cmd/solver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-cli/src/cmd/solver.rs b/test-cli/src/cmd/solver.rs index e36ce55..2ae3bd2 100644 --- a/test-cli/src/cmd/solver.rs +++ b/test-cli/src/cmd/solver.rs @@ -84,7 +84,7 @@ fn add(ctx: Context, args: AddArgs) -> anyhow::Result<()> { let (state_pda, _) = find_state_pda(&ctx.program_id); print_summary(&[ ("signature", &sig), - ("solver", &args.solver), + ("added solver", &args.solver), ("manager", &manager_pubkey), ("statePda", &state_pda), ]); From c25c91b68f77bf9eea7417c7e89fe5976f84ff0e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:29:34 +0900 Subject: [PATCH 3/3] simplify signer handling because specifying signer twice doesn't seem to be a problem --- test-cli/src/cmd/solver.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test-cli/src/cmd/solver.rs b/test-cli/src/cmd/solver.rs index e36ce55..8a25c46 100644 --- a/test-cli/src/cmd/solver.rs +++ b/test-cli/src/cmd/solver.rs @@ -6,7 +6,6 @@ use cow_settlement_client::{ }; use solana_sdk::{ signature::{read_keypair_file, Signer}, - signer::keypair::Keypair, transaction::Transaction, }; @@ -65,17 +64,16 @@ fn add(ctx: Context, args: AddArgs) -> anyhow::Result<()> { solver: args.solver, }; - // A manager that is the payer signs a single slot, so don't pass it twice. - let mut signers: Vec<&Keypair> = vec![&ctx.payer]; - if manager_pubkey != payer { - signers.push(manager); - } - let blockhash = ctx .rpc .get_latest_blockhash() .context("failed to fetch blockhash")?; - let tx = Transaction::new_signed_with_payer(&[ix.into()], Some(&payer), &signers, blockhash); + let tx = Transaction::new_signed_with_payer( + &[ix.into()], + Some(&payer), + &[&ctx.payer, manager], + blockhash, + ); let sig = ctx .rpc .send_and_confirm_transaction(&tx)