diff --git a/Cargo.lock b/Cargo.lock index 76c96f6929..94fb35b9cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8427,12 +8427,12 @@ dependencies = [ "anyhow", "assert_matches", "byte-unit", + "cainome 0.4.8", "clap", "clap_complete", "comfy-table", "dirs 5.0.1", "dojo-utils", - "futures", "inquire", "katana-cairo", "katana-cli", @@ -8442,8 +8442,8 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "spinoff", "starknet 0.12.0", - "thiserror 1.0.63", "tokio", "toml 0.8.19", ] diff --git a/bin/katana/Cargo.toml b/bin/katana/Cargo.toml index de353c9d92..04f8c76f57 100644 --- a/bin/katana/Cargo.toml +++ b/bin/katana/Cargo.toml @@ -15,20 +15,20 @@ katana-primitives.workspace = true anyhow.workspace = true byte-unit = "5.1.4" +cainome.workspace = true clap.workspace = true clap_complete.workspace = true comfy-table = "7.1.1" dirs = "5.0.1" dojo-utils.workspace = true +inquire = "0.7.5" +serde.workspace = true serde_json.workspace = true shellexpand = "3.1.0" +spinoff.workspace = true starknet.workspace = true tokio.workspace = true toml.workspace = true -serde.workspace = true -inquire = "0.7.5" -thiserror.workspace = true -futures.workspace = true [dev-dependencies] assert_matches.workspace = true diff --git a/bin/katana/src/cli/init/mod.rs b/bin/katana/src/cli/init/mod.rs index 57dc1fe5ec..8c7a9ed941 100644 --- a/bin/katana/src/cli/init/mod.rs +++ b/bin/katana/src/cli/init/mod.rs @@ -1,36 +1,60 @@ +use std::fmt::Display; use std::fs; use std::path::PathBuf; -use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; use anyhow::{anyhow, Context, Result}; +use cainome::rs::abigen; use clap::Args; use dojo_utils::TransactionWaiter; -use inquire::parser::CustomTypeParser; -use inquire::validator::{CustomTypeValidator, StringValidator, Validation}; -use inquire::{CustomType, CustomUserError, Text}; +use inquire::{Confirm, CustomType, Text}; use katana_cairo::lang::starknet_classes::casm_contract_class::CasmContractClass; use katana_cairo::lang::starknet_classes::contract_class::ContractClass; -use katana_primitives::{ContractAddress, Felt}; +use katana_primitives::{felt, ContractAddress, Felt}; use serde::{Deserialize, Serialize}; use starknet::accounts::{Account, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount}; use starknet::contract::ContractFactory; use starknet::core::types::contract::{CompiledClass, SierraClass}; -use starknet::core::types::{BlockId, BlockTag, FlattenedSierraClass}; +use starknet::core::types::{BlockId, BlockTag, FlattenedSierraClass, StarknetError}; use starknet::core::utils::{cairo_short_string_to_felt, parse_cairo_short_string}; use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::{JsonRpcClient, Provider, Url}; +use starknet::providers::{JsonRpcClient, Provider, ProviderError, Url}; use starknet::signers::{LocalWallet, SigningKey}; +use tokio::runtime::Runtime; + +#[derive(Debug)] +struct InitInput { + /// the account address that is used to send the transactions for contract + /// deployment/initialization. + account: ContractAddress, + + // the id of the new chain to be initialized. + id: String, + // the chain id of the settlement layer. + l1_id: String, + // the rpc url for the settlement layer. + l1_rpc_url: Url, + fee_token: ContractAddress, + settlement_contract: ContractAddress, + // path at which the config file will be written at. + output_path: PathBuf, +} #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct L1 { + // the account address that was used to initialized the l1 deployments + pub account: ContractAddress, + // The id of the settlement chain. pub id: String, + pub rpc_url: Url, + // - The token that will be used to pay for tx fee in the appchain. - // - For now, this must be the native token that is used to pay for tx fee in the settlement chain. + // - For now, this must be the native token that is used to pay for tx fee in the settlement + // chain. pub fee_token: ContractAddress, // - The bridge contract for bridging the fee token from L1 to the appchain @@ -44,7 +68,7 @@ pub struct L1 { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] -pub struct InitOutcome { +pub struct InitConfiguration { // the initialized chain id pub id: String, @@ -56,151 +80,199 @@ pub struct InitOutcome { pub l1: L1, } -#[derive(Args)] +#[derive(Debug, Args)] pub struct InitArgs { - // #[arg(long = "account", value_name = "ADDRESS")] - // pub sender_address: ContractAddress, - - // pub private_key: Felt, - - // #[arg(long = "l1.provider", value_name = "URL")] - // pub l1_provider_url: Url, - - // /// The id of the chain to be initialized. - // #[arg(long = "id", value_name = "ID")] - // pub chain_id: String, - - // pub l1_fee_token: ContractAddress, - - // /// If not specified, will be deployed on-demand. - // pub settlement_contract: Option, #[arg(value_name = "PATH")] pub output_path: Option, } impl InitArgs { + // TODO: + // - deploy bridge contract + // - generate the genesis pub(crate) fn execute(self) -> Result<()> { - tokio::runtime::Builder::new_multi_thread().enable_all().build()?.block_on(async move { - // let account = self.account(); - // let l1_chain_id = account.provider().chain_id().await?; - - // let core_contract = init_core_contract(&account).await?; - - self.prompt()?; - - todo!(); + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; + let input = self.prompt(&rt)?; + + let output = InitConfiguration { + id: input.id, + fee_token: ContractAddress::default(), + l1: L1 { + account: input.account, + id: input.l1_id, + rpc_url: input.l1_rpc_url, + fee_token: input.fee_token, + bridge_contract: ContractAddress::default(), + settlement_contract: input.settlement_contract, + }, + }; + + let content = toml::to_string_pretty(&output)?; + fs::write(input.output_path, content)?; - // let output_path = - // if let Some(path) = self.output_path { path } else { config_path(&self.chain_id)? }; - - // TODO: - // - deploy bridge contract - // - generate the genesis + Ok(()) + } - // let l1 = L1 { - // id: parse_cairo_short_string(&l1_chain_id)?, - // settlement_contract: ContractAddress::default(), - // bridge_contract: ContractAddress::default(), - // fee_token: ContractAddress::default(), - // }; + fn prompt(&self, rt: &Runtime) -> Result { + let chain_id = Text::new("Id").prompt()?; - // let output = - // InitOutcome { l1, id: self.chain_id, fee_token: ContractAddress::default() }; + let url = CustomType::::new("L1 RPC URL") + .with_default(Url::parse("http://localhost:5050")?) + .with_error_message("Please enter a valid URL") + .prompt()?; - // let content = toml::to_string_pretty(&output)?; - // std::fs::write(dbg!(output_path), content)?; + let l1_provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url.clone()))); - Result::<(), anyhow::Error>::Ok(()) - })?; + let contract_exist_parser = &|input: &str| { + let block_id = BlockId::Tag(BlockTag::Pending); + let address = Felt::from_str(input).map_err(|_| ())?; + let result = rt.block_on(l1_provider.clone().get_class_hash_at(block_id, address)); - Ok(()) - } + match result { + Ok(..) => Ok(ContractAddress::from(address)), + Err(..) => Err(()), + } + }; - fn prompt(&self) -> Result<()> { - let chain_id = Text::new("Chain id").prompt()?; + let account_address = CustomType::::new("Account") + .with_error_message("Please enter a valid account address") + .with_parser(contract_exist_parser) + .prompt()?; - let url = CustomType::::new("L1 RPC URL") - .with_error_message("Please enter a valid URL") + let private_key = CustomType::::new("Private key") + .with_formatter(&|input: Felt| format!("{input:#x}")) .prompt()?; - let l1_provider = JsonRpcClient::new(HttpTransport::new(url)); - let l1_provider = Arc::new(l1_provider); + let l1_chain_id = rt.block_on(l1_provider.chain_id())?; + let account = SingleOwnerAccount::new( + l1_provider.clone(), + LocalWallet::from_signing_key(SigningKey::from_secret_scalar(private_key)), + account_address.into(), + l1_chain_id, + ExecutionEncoding::New, + ); + // The L1 fee token. Must be an existing token. let fee_token = CustomType::::new("Fee token") - .with_parser(fee_token_parser(l1_provider.clone())) - .with_error_message("Please enter a valid fee token") - // .with_validator(fee_token_parser(l1_provider.clone())) + .with_parser(contract_exist_parser) + .with_error_message("Please enter a valid fee token (the token must exist on L1)") .prompt()?; - // If skipped, we deploy on demand. - let settlement_contract = Text::new("Settlement contract").prompt_skippable()?; - - Ok(()) + // The core settlement contract on L1 + let settlement_contract = + // Prompt the user whether to deploy the settlement contract or not. + if Confirm::new("Deploy settlement contract?").with_default(true).prompt()? { + let result = rt.block_on(init_core_contract(&account)); + result.context("Failed to deploy settlement contract")? + } + // If denied, prompt the user for an already deployed contract. + else { + // TODO: add a check to make sure the contract is indeed a valid settlement contract. + CustomType::::new("Settlement contract") + .with_parser(contract_exist_parser) + .prompt()? + }; + + let output_path = if let Some(path) = self.output_path.clone() { + path + } else { + CustomType::::new("Output path") + .with_default(config_path(&chain_id).map(Path)?) + .prompt()? + .0 + }; + + Ok(InitInput { + account: account_address, + settlement_contract, + l1_id: parse_cairo_short_string(&l1_chain_id)?, + id: chain_id, + fee_token, + l1_rpc_url: url, + output_path, + }) } - - // fn account(&self) -> SingleOwnerAccount, LocalWallet> { - // let provider = JsonRpcClient::new(HttpTransport::new(self.l1_provider_url.clone())); - // let private_key = SigningKey::from_secret_scalar(self.private_key); - - // SingleOwnerAccount::new( - // provider, - // LocalWallet::from_signing_key(private_key), - // self.sender_address.into(), - // Felt::ONE, - // ExecutionEncoding::New, - // ) - // } } -#[derive(Debug, thiserror::Error)] -#[error("Fee token doesn't exist")] -pub struct FeeTokenNotExist; +async fn init_core_contract

( + account: &SingleOwnerAccount, +) -> Result +where + P: Provider + Send + Sync, +{ + use spinoff::{spinners, Color, Spinner}; + + let mut sp = Spinner::new(spinners::Dots, "", Color::Blue); + + let result = async { + let class = include_str!( + "../../../../../crates/katana/contracts/build/appchain_core_contract.json" + ); + + abigen!( + AppchainContract, + "[{\"type\":\"function\",\"name\":\"set_program_info\",\"inputs\":[{\"name\":\"\ + program_hash\",\"type\":\"core::felt252\"},{\"name\":\"config_hash\",\"type\":\"\ + core::felt252\"}],\"outputs\":[],\"state_mutability\":\"external\"}]" + ); + + let (contract, compiled_class_hash) = prepare_contract_declaration_params(class)?; + let class_hash = contract.class_hash(); + + // Check if the class has already been declared, + match account.provider().get_class(BlockId::Tag(BlockTag::Pending), class_hash).await { + Ok(..) => { + // Class has already been declared, no need to do anything... + } + + Err(ProviderError::StarknetError(StarknetError::ClassHashNotFound)) => { + sp.update_text("Declaring contract..."); + let res = account.declare_v2(contract.into(), compiled_class_hash).send().await?; + let _ = TransactionWaiter::new(res.transaction_hash, account.provider()).await?; + } + + Err(err) => return Err(anyhow!(err)), + } + + sp.update_text("Deploying contract..."); -// pub type CustomTypeParser<'a, T> = &'a dyn Fn(&str) -> Result; + let factory = ContractFactory::new(class_hash, &account); + // appchain::constructor() https://github.com/cartridge-gg/piltover/blob/d373a844c3428383a48518adf468bf83249dec3a/src/appchain.cairo#L119-L125 + let request = factory.deploy_v1( + vec![ + account.address(), // owner + Felt::ZERO, // state_root + Felt::ZERO, // block_number + Felt::ZERO, // block_hash + ], + Felt::ZERO, + true, + ); -fn fee_token_parser(provider: impl Provider + Clone) -> impl CustomTypeParser<'a, T> { - move |input: &str| { - let block_id = BlockId::Tag(BlockTag::Pending); - let result = futures::executor::block_on(provider.get_class_hash_at(block_id, input)); + let res = request.send().await?; + let _ = TransactionWaiter::new(res.transaction_hash, account.provider()).await?; - match result { - Ok(..) => Ok(Validation::Valid), - Err(..) => Err(Box::new(FeeTokenNotExist) as CustomUserError), - } + sp.update_text("Initializing..."); + + let deployed_contract_address = request.deployed_address(); + let appchain = AppchainContract::new(deployed_contract_address, account); + + const PROGRAM_HASH: Felt = + felt!("0x5ab580b04e3532b6b18f81cfa654a05e29dd8e2352d88df1e765a84072db07"); + const CONFIG_HASH: Felt = + felt!("0x504fa6e5eb930c0d8329d4a77d98391f2730dab8516600aeaf733a6123432"); + + appchain.set_program_info(&PROGRAM_HASH, &CONFIG_HASH).send().await?; + + Ok(deployed_contract_address.into()) } -} + .await; -async fn init_core_contract( - account: &SingleOwnerAccount, LocalWallet>, -) -> Result { - let class = - include_str!("../../../../../crates/katana/contracts/build/appchain_core_contract.json"); - let (contract, compiled_class_hash) = prepare_contract_declaration_params(class)?; - - let class_hash = contract.class_hash(); - let res = account.declare_v2(contract.into(), compiled_class_hash).send().await?; - let _ = TransactionWaiter::new(res.transaction_hash, account.provider()).await?; - - let factory = ContractFactory::new(class_hash, &account); - - // appchain::constructor() https://github.com/cartridge-gg/piltover/blob/d373a844c3428383a48518adf468bf83249dec3a/src/appchain.cairo#L119-L125 - let request = factory.deploy_v3( - vec![ - account.address(), // owner - Felt::ZERO, // state_root - Felt::ZERO, // block_number - Felt::ZERO, // block_hash - ], - Felt::ZERO, - false, - ); - - let res = request.send().await?; - let _ = TransactionWaiter::new(res.transaction_hash, account.provider()).await?; - - // TODO: initialize the core contract with the right program info - - Ok(request.deployed_address().into()) + match result { + Ok(addr) => sp.success(&format!("Deployment successful ( {addr} )")), + Err(..) => sp.fail("Deployment failed"), + } + result } fn prepare_contract_declaration_params(artifact: &str) -> Result<(FlattenedSierraClass, Felt)> { @@ -240,3 +312,19 @@ fn config_dir(id: &str) -> Result { Ok(path) } + +#[derive(Debug, Clone)] +struct Path(PathBuf); + +impl FromStr for Path { + type Err = ::Err; + fn from_str(s: &str) -> std::result::Result { + PathBuf::from_str(s).map(Self) + } +} + +impl Display for Path { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +}