diff --git a/cli/src/command/paymaster/create.rs b/cli/src/command/paymaster/create.rs index f550fcbf..ad79696a 100644 --- a/cli/src/command/paymaster/create.rs +++ b/cli/src/command/paymaster/create.rs @@ -3,8 +3,8 @@ use clap::Args; use num_bigint::BigInt; use slot::api::Client; use slot::credential::Credentials; -use slot::graphql::paymaster::create_paymaster; -use slot::graphql::paymaster::CreatePaymaster; +use slot::graphql::paymaster::{add_paymaster_policies, create_paymaster}; +use slot::graphql::paymaster::{AddPaymasterPolicies, CreatePaymaster}; use slot::graphql::GraphQLQuery; #[derive(Debug, Args)] @@ -16,6 +16,13 @@ pub struct CreateArgs { team: String, #[arg(long, help = "Initial budget for the paymaster (in wei).")] budget: BigInt, + #[arg( + long, + help = "Preset name to use for configuring the paymaster (e.g., 'dopewars')." + )] + preset: Option, + #[arg(long, help = "Chain ID to use for the preset policies.")] + chain_id: Option, } impl CreateArgs { @@ -49,6 +56,51 @@ impl CreateArgs { data.create_paymaster.id, ); + if let (Some(preset), Some(chain_id)) = (&self.preset, &self.chain_id) { + println!("Loading preset '{}' for chain '{}'...", preset, chain_id); + + let config = slot::presets::load_preset(preset) + .await + .map_err(|e| anyhow::anyhow!("Failed to load preset: {}", e))?; + + let policies = slot::presets::extract_paymaster_policies(&config, chain_id); + + if policies.is_empty() { + println!("No paymaster policies found in preset"); + return Ok(()); + } + + println!( + "Applying {} paymaster policies from preset...", + policies.len() + ); + + let policies_gql: Vec = policies + .into_iter() + .map(|p| add_paymaster_policies::PaymasterPolicyInput { + contract_address: p.contract_address, + entry_point: p.entry_point, + selector: p.selector, + }) + .collect(); + + let variables = add_paymaster_policies::Variables { + paymaster_id: data.create_paymaster.id.clone(), + policies: policies_gql, + }; + + let request_body = AddPaymasterPolicies::build_query(variables); + let result: add_paymaster_policies::ResponseData = client.query(&request_body).await?; + + println!( + "Successfully added {} policies from preset", + result + .add_paymaster_policies + .as_ref() + .map_or(0, |v| v.len()) + ); + } + Ok(()) } } diff --git a/cli/src/command/paymaster/mod.rs b/cli/src/command/paymaster/mod.rs index 5e6377df..c07f2b68 100644 --- a/cli/src/command/paymaster/mod.rs +++ b/cli/src/command/paymaster/mod.rs @@ -7,12 +7,14 @@ use self::create::CreateArgs; use self::get::GetArgs; use self::list::ListArgs; use self::policy::PolicyCmd; +use self::update::UpdateArgs; mod budget; mod create; mod get; mod list; mod policy; +mod update; /// Command group for managing Paymasters #[derive(Debug, Args)] @@ -39,6 +41,9 @@ enum PaymasterSubcommand { #[command(about = "Manage paymaster budget.")] Budget(BudgetCmd), + + #[command(about = "Update paymaster configuration from a preset.")] + Update(UpdateArgs), } impl PaymasterCmd { @@ -50,6 +55,7 @@ impl PaymasterCmd { PaymasterSubcommand::Get(args) => args.run().await, PaymasterSubcommand::Policy(cmd) => cmd.run().await, PaymasterSubcommand::Budget(cmd) => cmd.run().await, + PaymasterSubcommand::Update(args) => args.run().await, } } } diff --git a/cli/src/command/paymaster/update.rs b/cli/src/command/paymaster/update.rs new file mode 100644 index 00000000..919860dc --- /dev/null +++ b/cli/src/command/paymaster/update.rs @@ -0,0 +1,135 @@ +use anyhow::Result; +use clap::Args; +use slot::api::Client; +use slot::credential::Credentials; +use slot::graphql::paymaster::{add_paymaster_policies, get_paymaster, remove_paymaster_policies}; +use slot::graphql::paymaster::{AddPaymasterPolicies, GetPaymaster, RemovePaymasterPolicies}; +use slot::graphql::GraphQLQuery; + +#[derive(Debug, Args)] +#[command(next_help_heading = "Update paymaster options")] +pub struct UpdateArgs { + #[arg(long, help = "ID of the paymaster to update.")] + paymaster_id: String, + + #[arg( + long, + help = "Preset name to use for configuring the paymaster (e.g., 'dopewars')." + )] + preset: String, + + #[arg(long, help = "Chain ID to use for the preset policies.")] + chain_id: String, + + #[arg( + long, + help = "Remove existing policies before applying preset policies.", + default_value = "false" + )] + replace: bool, +} + +impl UpdateArgs { + pub async fn run(&self) -> Result<()> { + // 1. Load Credentials + let credentials = Credentials::load()?; + + // 2. Create Client + let client = Client::new_with_token(credentials.access_token); + + println!( + "Updating paymaster '{}' with preset '{}'...", + self.paymaster_id, self.preset + ); + + // 3. Load preset configuration + let config = slot::presets::load_preset(&self.preset) + .await + .map_err(|e| anyhow::anyhow!("Failed to load preset: {}", e))?; + + // 4. Extract paymaster policies + let policies = slot::presets::extract_paymaster_policies(&config, &self.chain_id); + + if policies.is_empty() { + println!("No paymaster policies found in preset"); + return Ok(()); + } + + // 5. Remove existing policies if replace is true + if self.replace { + println!("Removing existing policies..."); + + // First get the paymaster to retrieve existing policy IDs + let variables = get_paymaster::Variables { + id: self.paymaster_id.clone(), + }; + + let request_body = GetPaymaster::build_query(variables); + let data: get_paymaster::ResponseData = client.query(&request_body).await?; + + // Extract policy IDs + let mut policy_ids = Vec::new(); + + if let Some(paymaster) = data.paymaster { + // Use if let instead of for loop for Option + if let Some(edges) = paymaster.policies.edges { + // Use flatten to simplify the nested Option handling + for edge in edges.into_iter().flatten() { + if let Some(node) = edge.node { + policy_ids.push(node.id); + } + } + } + } + + if !policy_ids.is_empty() { + // Remove existing policies + let variables = remove_paymaster_policies::Variables { + paymaster_id: self.paymaster_id.clone(), + policy_ids, + }; + + let request_body = RemovePaymasterPolicies::build_query(variables); + let _: remove_paymaster_policies::ResponseData = + client.query(&request_body).await?; + + println!("Existing policies removed"); + } + } + + // 6. Add new policies from preset + println!( + "Applying {} paymaster policies from preset...", + policies.len() + ); + + // Convert to GraphQL input type + let policies_gql: Vec = policies + .into_iter() + .map(|p| add_paymaster_policies::PaymasterPolicyInput { + contract_address: p.contract_address, + entry_point: p.entry_point, + selector: p.selector, + }) + .collect(); + + // Add policies to paymaster + let variables = add_paymaster_policies::Variables { + paymaster_id: self.paymaster_id.clone(), + policies: policies_gql, + }; + + let request_body = AddPaymasterPolicies::build_query(variables); + let result: add_paymaster_policies::ResponseData = client.query(&request_body).await?; + + println!( + "Successfully updated paymaster with {} policies from preset", + result + .add_paymaster_policies + .as_ref() + .map_or(0, |v| v.len()) + ); + + Ok(()) + } +} diff --git a/slot/src/lib.rs b/slot/src/lib.rs index 29a2bb57..0e7c95a7 100644 --- a/slot/src/lib.rs +++ b/slot/src/lib.rs @@ -6,6 +6,7 @@ pub mod bigint; pub mod browser; pub mod credential; pub mod graphql; +pub mod presets; pub mod read; pub mod server; pub mod session; diff --git a/slot/src/presets.rs b/slot/src/presets.rs new file mode 100644 index 00000000..56d5fdba --- /dev/null +++ b/slot/src/presets.rs @@ -0,0 +1,97 @@ +use anyhow::{Context, Result}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +const CONFIG_BASE_URL: &str = "https://static.cartridge.gg/presets"; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Method { + pub name: Option, + pub description: Option, + pub entrypoint: String, + #[serde(default)] + pub is_required: bool, + #[serde(default)] + pub is_paymastered: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ContractPolicy { + pub name: Option, + pub description: Option, + pub methods: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SessionPolicies { + pub contracts: HashMap, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ChainPolicies { + pub policies: SessionPolicies, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ControllerConfig { + pub origin: Vec, + pub theme: Option, + pub chains: HashMap, +} + +pub async fn load_preset(preset_name: &str) -> Result { + let client = Client::new(); + let url = format!("{}/{}/config.json", CONFIG_BASE_URL, preset_name); + + let response = client + .get(&url) + .send() + .await + .context(format!("Failed to fetch preset: {}", preset_name))?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!( + "Failed to fetch preset {}: HTTP Status {}", + preset_name, + response.status() + )); + } + + let config = response.json::().await.context(format!( + "Failed to parse preset configuration: {}", + preset_name + ))?; + + Ok(config) +} + +pub fn extract_paymaster_policies( + config: &ControllerConfig, + chain_id: &str, +) -> Vec { + let mut policies = Vec::new(); + + if let Some(chain_policies) = config.chains.get(chain_id) { + for (contract_address, contract_policy) in &chain_policies.policies.contracts { + for method in &contract_policy.methods { + if method.is_paymastered { + policies.push(PaymasterPolicyInput { + contract_address: contract_address.clone(), + entry_point: method.entrypoint.clone(), + selector: String::new(), // Leave empty as it will be calculated by the backend + }); + } + } + } + } + + policies +} + +#[derive(Debug, Clone)] +pub struct PaymasterPolicyInput { + pub contract_address: String, + pub entry_point: String, + pub selector: String, +}