diff --git a/Cargo.lock b/Cargo.lock index 5214144..9e36686 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2053,6 +2053,7 @@ dependencies = [ "tokio", "tower-http", "tracing", + "url", "urlencoding", "webbrowser", ] diff --git a/Cargo.toml b/Cargo.toml index 8638df7..e492eff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ tracing = "0.1.34" urlencoding = "2" webbrowser = "0.8" starknet = "0.6.0" +url = "2.2.2" [[bin]] name = "slot" diff --git a/schema.json b/schema.json index babd0fc..1491ccc 100644 --- a/schema.json +++ b/schema.json @@ -18357,6 +18357,114 @@ "ofType": null } } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "blockTime", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "forkRpcUrl", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "forkBlockNumber", + "type": { + "kind": "SCALAR", + "name": "Long", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "accounts", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "invokeMaxSteps", + "type": { + "kind": "SCALAR", + "name": "Long", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "validateMaxSteps", + "type": { + "kind": "SCALAR", + "name": "Long", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "disableFee", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "gasPrice", + "type": { + "kind": "SCALAR", + "name": "Long", + "ofType": null + } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "chainId", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } } ], "inputFields": [], @@ -19262,6 +19370,85 @@ } } }, + { + "args": [ + { + "defaultValue": null, + "description": null, + "name": "name", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + { + "defaultValue": null, + "description": null, + "name": "forkName", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + { + "defaultValue": null, + "description": null, + "name": "forkBlockNumber", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Long", + "ofType": null + } + } + }, + { + "defaultValue": null, + "description": null, + "name": "tier", + "type": { + "kind": "ENUM", + "name": "DeploymentTier", + "ofType": null + } + }, + { + "defaultValue": null, + "description": null, + "name": "wait", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + } + ], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "forkDeployment", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "UNION", + "name": "DeploymentConfig", + "ofType": null + } + } + }, { "args": [ { @@ -36878,6 +37065,26 @@ "ofType": null } }, + { + "defaultValue": null, + "description": null, + "name": "forkRpcUrl", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + { + "defaultValue": null, + "description": null, + "name": "forkBlockNumber", + "type": { + "kind": "SCALAR", + "name": "Long", + "ofType": null + } + }, { "defaultValue": null, "description": null, diff --git a/src/api.rs b/src/api.rs index 02de7fd..01a5ec4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -47,7 +47,9 @@ impl ApiClient { .map_err(ApiError::ReqwestError)?; if res.status() == 403 { - return Err(ApiError::CredentialsError(anyhow::anyhow!("Invalid token, authenticate with `slot auth login`"))); + return Err(ApiError::CredentialsError(anyhow::anyhow!( + "Invalid token, authenticate with `slot auth login`" + ))); } let res: Response = res.json().await.map_err(ApiError::ReqwestError)?; diff --git a/src/command/deployments/create.rs b/src/command/deployments/create.rs index 8b9a569..04f103e 100644 --- a/src/command/deployments/create.rs +++ b/src/command/deployments/create.rs @@ -13,9 +13,7 @@ use crate::{ }, }; -use super::services::CreateServiceCommands; - -type Long = u64; +use super::{services::CreateServiceCommands, Long, Tier}; #[derive(GraphQLQuery)] #[graphql( @@ -25,11 +23,6 @@ type Long = u64; )] pub struct CreateDeployment; -#[derive(clap::ValueEnum, Clone, Debug, serde::Serialize)] -pub enum Tier { - Basic, -} - #[derive(Debug, Args)] #[command(next_help_heading = "Create options")] pub struct CreateArgs { diff --git a/src/command/deployments/fork.graphql b/src/command/deployments/fork.graphql new file mode 100644 index 0000000..7298eea --- /dev/null +++ b/src/command/deployments/fork.graphql @@ -0,0 +1,20 @@ +mutation ForkDeployment( + $project: String! + $forkName: String! + $forkBlockNumber: Long! + $tier: DeploymentTier! + $wait: Boolean +) { + forkDeployment( + name: $project + forkName: $forkName + forkBlockNumber: $forkBlockNumber + tier: $tier + wait: $wait + ) { + __typename + ... on KatanaConfig { + rpc + } + } +} diff --git a/src/command/deployments/fork.rs b/src/command/deployments/fork.rs new file mode 100644 index 0000000..50af5cb --- /dev/null +++ b/src/command/deployments/fork.rs @@ -0,0 +1,97 @@ +#![allow(clippy::enum_variant_names)] + +use anyhow::Result; +use clap::Args; +use graphql_client::{GraphQLQuery, Response}; +use starknet::providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider}; +use url::Url; + +use crate::{ + api::ApiClient, + command::deployments::fork::fork_deployment::{ + DeploymentTier, ForkDeploymentForkDeployment::KatanaConfig, Variables, + }, +}; + +use super::{services::ForkServiceCommands, Long, Tier}; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "schema.json", + query_path = "src/command/deployments/fork.graphql", + response_derives = "Debug" +)] +pub struct ForkDeployment; + +#[derive(Debug, Args)] +#[command(next_help_heading = "Fork options")] +pub struct ForkArgs { + #[arg(help = "The name of the project to fork.")] + pub project: String, + #[arg(short, long, default_value = "basic")] + #[arg(value_name = "tier")] + #[arg(help = "Deployment tier.")] + pub tier: Tier, + + #[command(subcommand)] + fork_commands: ForkServiceCommands, +} + +impl ForkArgs { + pub async fn run(&self) -> Result<()> { + let (fork_name, fork_block_number) = self.fork_config().await?; + + let tier = match &self.tier { + Tier::Basic => DeploymentTier::basic, + }; + + let request_body = ForkDeployment::build_query(Variables { + project: self.project.clone(), + fork_name: fork_name.clone(), + fork_block_number, + tier, + wait: Some(true), + }); + + let client = ApiClient::new(); + let res: Response = client.post(&request_body).await?; + if let Some(errors) = res.errors.clone() { + for err in errors { + println!("Error: {}", err.message); + } + } + + if let Some(data) = res.data { + println!("Fork success 🚀"); + if let KatanaConfig(config) = data.fork_deployment { + println!("\nEndpoints:"); + println!(" RPC: {}", config.rpc); + println!( + "\nStream logs with `slot deployments logs {} katana -f`", + fork_name + ); + } + } + + Ok(()) + } + + async fn fork_config(&self) -> Result<(String, u64)> { + match &self.fork_commands { + ForkServiceCommands::Katana(config) => { + let block_number = if let Some(block_number) = config.fork_block_number { + block_number + } else { + // Workaround to get latest block number. Perhaps Katana could default to latest if none is supplied + let rpc_client = JsonRpcClient::new(HttpTransport::new(Url::parse(&format!( + "https://api.cartridge.gg/x/{}/katana", + self.project + ))?)); + rpc_client.block_number().await? + }; + + Ok((config.fork_name.clone(), block_number)) + } + } + } +} diff --git a/src/command/deployments/mod.rs b/src/command/deployments/mod.rs index b38fbd1..6bd565f 100644 --- a/src/command/deployments/mod.rs +++ b/src/command/deployments/mod.rs @@ -2,18 +2,21 @@ use anyhow::Result; use clap::Subcommand; use self::{ - create::CreateArgs, delete::DeleteArgs, describe::DescribeArgs, list::ListArgs, logs::LogsArgs, - update::UpdateArgs, + create::CreateArgs, delete::DeleteArgs, describe::DescribeArgs, fork::ForkArgs, list::ListArgs, + logs::LogsArgs, update::UpdateArgs, }; mod create; mod delete; mod describe; +mod fork; mod list; mod logs; mod services; mod update; +type Long = u64; + #[derive(Subcommand, Debug)] pub enum Deployments { #[command(about = "Create a new deployment.")] @@ -22,6 +25,8 @@ pub enum Deployments { Delete(DeleteArgs), #[command(about = "Update a deployment.")] Update(UpdateArgs), + #[command(about = "Fork a deployment.")] + Fork(ForkArgs), #[command(about = "Describe a deployment's configuration.")] Describe(DescribeArgs), #[command(about = "List all deployments.", aliases = ["ls"])] @@ -36,9 +41,15 @@ impl Deployments { Deployments::Create(args) => args.run().await, Deployments::Delete(args) => args.run().await, Deployments::Update(args) => args.run().await, + Deployments::Fork(args) => args.run().await, Deployments::Describe(args) => args.run().await, Deployments::List(args) => args.run().await, Deployments::Logs(args) => args.run().await, } } } + +#[derive(clap::ValueEnum, Clone, Debug, serde::Serialize)] +pub enum Tier { + Basic, +} diff --git a/src/command/deployments/services/katana.rs b/src/command/deployments/services/katana.rs index 25ac618..9474db5 100644 --- a/src/command/deployments/services/katana.rs +++ b/src/command/deployments/services/katana.rs @@ -59,6 +59,14 @@ pub struct KatanaUpdateArgs { #[arg(help = "Block time.")] pub block_time: Option, + #[arg(long, short, value_name = "fork_rpc_url")] + #[arg(help = "Fork RPC URL.")] + pub fork_rpc_url: Option, + + #[arg(long, short, value_name = "fork_block_number")] + #[arg(help = "Fork Block Number.")] + pub fork_block_number: Option, + #[arg(long, value_name = "invoke_max_steps")] #[arg(help = "Invoke Max Steps.")] pub invoke_max_steps: Option, @@ -75,3 +83,14 @@ pub struct KatanaUpdateArgs { #[arg(help = "Gas Price.")] pub gas_price: Option, } + +#[derive(Debug, Args, serde::Serialize)] +#[command(next_help_heading = "Katana fork options")] +pub struct KatanaForkArgs { + #[arg(long, value_name = "fork_name")] + #[arg(help = "Specify the fork name")] + pub fork_name: String, + #[arg(long, value_name = "fork_block_number")] + #[arg(help = "Specify block number to fork. (latests if not provided)")] + pub fork_block_number: Option, +} diff --git a/src/command/deployments/services/mod.rs b/src/command/deployments/services/mod.rs index 8f9603f..b132415 100644 --- a/src/command/deployments/services/mod.rs +++ b/src/command/deployments/services/mod.rs @@ -1,7 +1,7 @@ use clap::{Subcommand, ValueEnum}; use self::{ - katana::{KatanaCreateArgs, KatanaUpdateArgs}, + katana::{KatanaCreateArgs, KatanaForkArgs, KatanaUpdateArgs}, torii::{ToriiCreateArgs, ToriiUpdateArgs}, }; @@ -27,6 +27,15 @@ pub enum UpdateServiceCommands { Torii(ToriiUpdateArgs), } +#[derive(Debug, Subcommand, serde::Serialize)] +#[serde(untagged)] +pub enum ForkServiceCommands { + #[command(about = "Katana deployment.")] + Katana(KatanaForkArgs), + // #[command(about = "Torii deployment.")] + // Torii(ToriiUpdateArgs), +} + #[derive(Clone, Debug, ValueEnum, serde::Serialize)] pub enum Service { Katana, diff --git a/src/command/deployments/update.rs b/src/command/deployments/update.rs index 5b7077f..55c98d3 100644 --- a/src/command/deployments/update.rs +++ b/src/command/deployments/update.rs @@ -54,6 +54,8 @@ impl UpdateArgs { config: Some(UpdateServiceConfigInput { katana: Some(UpdateKatanaConfigInput { block_time: config.block_time, + fork_rpc_url: config.fork_rpc_url.clone(), + fork_block_number: config.fork_block_number, disable_fee: config.disable_fee, gas_price: config.gas_price, invoke_max_steps: config.invoke_max_steps,