Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions cli/src/command/paymaster/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<String>,
#[arg(long, help = "Chain ID to use for the preset policies.")]
chain_id: Option<String>,
}

impl CreateArgs {
Expand Down Expand Up @@ -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<add_paymaster_policies::PaymasterPolicyInput> = 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(())
}
}
6 changes: 6 additions & 0 deletions cli/src/command/paymaster/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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 {
Expand All @@ -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,
}
}
}
135 changes: 135 additions & 0 deletions cli/src/command/paymaster/update.rs
Original file line number Diff line number Diff line change
@@ -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<add_paymaster_policies::PaymasterPolicyInput> = 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(())
}
}
1 change: 1 addition & 0 deletions slot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
97 changes: 97 additions & 0 deletions slot/src/presets.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub description: Option<String>,
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<String>,
pub description: Option<String>,
pub methods: Vec<Method>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct SessionPolicies {
pub contracts: HashMap<String, ContractPolicy>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ChainPolicies {
pub policies: SessionPolicies,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ControllerConfig {
pub origin: Vec<String>,
pub theme: Option<serde_json::Value>,
pub chains: HashMap<String, ChainPolicies>,
}

pub async fn load_preset(preset_name: &str) -> Result<ControllerConfig> {
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::<ControllerConfig>().await.context(format!(
"Failed to parse preset configuration: {}",
preset_name
))?;

Ok(config)
}

pub fn extract_paymaster_policies(
config: &ControllerConfig,
chain_id: &str,
) -> Vec<PaymasterPolicyInput> {
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,
}
Loading