Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(katana): non-interactive init #2988

Merged
merged 1 commit into from
Feb 3, 2025
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin/katana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ strum_macros.workspace = true
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
url.workspace = true

[dev-dependencies]
assert_matches.workspace = true
Expand Down
291 changes: 163 additions & 128 deletions bin/katana/src/cli/init/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
mod deployment;

use std::str::FromStr;
use std::sync::Arc;

use anyhow::{Context, Result};
use anyhow::Context;
use clap::Args;
use inquire::{Confirm, CustomType, Select};
use katana_chain_spec::rollup::FeeContract;
use katana_chain_spec::{rollup, SettlementLayer};
use katana_primitives::chain::ChainId;
Expand All @@ -14,35 +11,58 @@
use katana_primitives::genesis::Genesis;
use katana_primitives::{ContractAddress, Felt, U256};
use lazy_static::lazy_static;
use prompt::CARTRIDGE_SN_SEPOLIA_PROVIDER;
use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount};
use starknet::core::types::{BlockId, BlockTag};
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::signers::{LocalWallet, SigningKey};
use tokio::runtime::Runtime as AsyncRuntime;
use starknet::providers::{JsonRpcClient, Provider};
use starknet::signers::SigningKey;
use url::Url;

const CARTRIDGE_SN_SEPOLIA_PROVIDER: &str = "https://api.cartridge.gg/x/starknet/sepolia";
mod deployment;
mod prompt;

#[derive(Debug, Args)]
pub struct InitArgs;
pub struct InitArgs {
#[arg(long)]
#[arg(requires_all = ["settlement_chain", "settlement_account", "settlement_account_private_key"])]
id: Option<String>,

#[arg(long = "settlement-chain")]
#[arg(requires_all = ["id", "settlement_account", "settlement_account_private_key"])]
settlement_chain: Option<SettlementChain>,

#[arg(long = "settlement-account-address")]
#[arg(requires_all = ["id", "settlement_chain", "settlement_account_private_key"])]
settlement_account: Option<ContractAddress>,

#[arg(long = "settlement-account-private-key")]
#[arg(requires_all = ["id", "settlement_chain", "settlement_account"])]
settlement_account_private_key: Option<Felt>,

#[arg(long = "settlement-contract")]
#[arg(requires_all = ["id", "settlement_chain", "settlement_account", "settlement_account_private_key"])]
settlement_contract: Option<ContractAddress>,
}

impl InitArgs {
// TODO:
// - deploy bridge contract
// - generate the genesis
pub(crate) fn execute(self) -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
let input = self.prompt(&rt)?;
pub(crate) async fn execute(self) -> anyhow::Result<()> {
let output = if let Some(output) = self.process_args().await {
output?

Check warning on line 53 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L51-L53

Added lines #L51 - L53 were not covered by tests
} else {
prompt::prompt().await?

Check warning on line 55 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L55

Added line #L55 was not covered by tests
};

let settlement = SettlementLayer::Starknet {
account: input.account,
rpc_url: input.rpc_url,
id: ChainId::parse(&input.settlement_id)?,
core_contract: input.settlement_contract,
account: output.account,
rpc_url: output.rpc_url,
id: ChainId::parse(&output.settlement_id)?,
core_contract: output.settlement_contract,

Check warning on line 62 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L59-L62

Added lines #L59 - L62 were not covered by tests
};

let id = ChainId::parse(&input.id)?;
let id = ChainId::parse(&output.id)?;

Check warning on line 65 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L65

Added line #L65 was not covered by tests
let genesis = GENESIS.clone();
// At the moment, the fee token is limited to a predefined token.
let fee_contract = FeeContract::default();
Expand All @@ -53,134 +73,75 @@
Ok(())
}

fn prompt(&self, rt: &AsyncRuntime) -> Result<PromptOutcome> {
let chain_id = CustomType::<String>::new("Id")
.with_help_message("This will be the id of your rollup chain.")
// checks that the input is a valid ascii string.
.with_parser(&|input| {
if input.is_ascii() {
Ok(input.to_string())
} else {
Err(())
}
})
.with_error_message("Must be valid ASCII characters")
.prompt()?;

#[derive(Debug, strum_macros::Display)]
enum SettlementChainOpt {
Sepolia,
#[cfg(feature = "init-custom-settlement-chain")]
Custom,
}

// Right now we only support settling on Starknet Sepolia because we're limited to what
// network the Atlantic service could settle the proofs to. Supporting a custom
// network here (eg local devnet) would require that the proving service we're using
// be able to settle the proofs there.
let network_opts = vec![
SettlementChainOpt::Sepolia,
#[cfg(feature = "init-custom-settlement-chain")]
SettlementChainOpt::Custom,
];

let network_type = Select::new("Settlement chain", network_opts).prompt()?;

let settlement_url = match network_type {
SettlementChainOpt::Sepolia => Url::parse(CARTRIDGE_SN_SEPOLIA_PROVIDER)?,

// Useful for testing the program flow without having to run it against actual network.
#[cfg(feature = "init-custom-settlement-chain")]
SettlementChainOpt::Custom => CustomType::<Url>::new("Settlement RPC URL")
.with_default(Url::parse("http://localhost:5050")?)
.with_error_message("Please enter a valid URL")
.prompt()?,
};

let l1_provider = Arc::new(JsonRpcClient::new(HttpTransport::new(settlement_url.clone())));

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));

match result {
Ok(..) => Ok(ContractAddress::from(address)),
Err(..) => Err(()),
}
};
async fn process_args(&self) -> Option<anyhow::Result<Outcome>> {

Check warning on line 76 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L76

Added line #L76 was not covered by tests
// Here we just check that if `id` is present, then all the other required* arguments must
// be present as well. This is guaranteed by `clap`.
if let Some(id) = self.id.clone() {

Check warning on line 79 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L79

Added line #L79 was not covered by tests
// These args are all required if at least one of them are specified (incl chain id) and
// `clap` has already handled that for us, so it's safe to unwrap here.
let settlement_chain = self.settlement_chain.clone().expect("must present");
let settlement_account_address = self.settlement_account.expect("must present");
let settlement_private_key = self.settlement_account_private_key.expect("must present");

Check warning on line 84 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L82-L84

Added lines #L82 - L84 were not covered by tests

let settlement_url = match settlement_chain {
SettlementChain::Sepolia => Url::parse(CARTRIDGE_SN_SEPOLIA_PROVIDER).unwrap(),

Check warning on line 87 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L86-L87

Added lines #L86 - L87 were not covered by tests
#[cfg(feature = "init-custom-settlement-chain")]
SettlementChain::Custom(url) => url,

Check warning on line 89 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L89

Added line #L89 was not covered by tests
};

let account_address = CustomType::<ContractAddress>::new("Account")
.with_error_message("Please enter a valid account address")
.with_parser(contract_exist_parser)
.prompt()?;

let private_key = CustomType::<Felt>::new("Private key")
.with_formatter(&|input: Felt| format!("{input:#x}"))
.prompt()?;

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,
);
let l1_provider =
Arc::new(JsonRpcClient::new(HttpTransport::new(settlement_url.clone())));
let l1_chain_id = l1_provider.chain_id().await.unwrap();

Check warning on line 94 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L92-L94

Added lines #L92 - L94 were not covered by tests

// The core settlement contract on L1c.
// Prompt the user whether to deploy the settlement contract or not.
let settlement_contract =
if Confirm::new("Deploy settlement contract?").with_default(true).prompt()? {
let chain_id = cairo_short_string_to_felt(&chain_id)?;
let initialize = deployment::deploy_settlement_contract(account, chain_id);
let result = rt.block_on(initialize);
result?
let settlement_contract = if let Some(contract) = self.settlement_contract {
let chain_id = cairo_short_string_to_felt(&id).unwrap();
deployment::check_program_info(chain_id, contract.into(), &l1_provider)
.await
.unwrap();
contract

Check warning on line 101 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L96-L101

Added lines #L96 - L101 were not covered by tests
}
// If denied, prompt the user for an already deployed contract.
// If settlement contract is not provided, then we will deploy it.
else {
let address = CustomType::<ContractAddress>::new("Settlement contract")
.with_parser(contract_exist_parser)
.prompt()?;

// Check that the settlement contract has been initialized with the correct program
// info.
let chain_id = cairo_short_string_to_felt(&chain_id)?;
rt.block_on(deployment::check_program_info(chain_id, address.into(), &l1_provider))
.context(
"Invalid settlement contract. The contract might have been configured \
incorrectly.",
)?;

address
let account = SingleOwnerAccount::new(
l1_provider,
SigningKey::from_secret_scalar(settlement_private_key).into(),
settlement_account_address.into(),
l1_chain_id,
ExecutionEncoding::New,
);

deployment::deploy_settlement_contract(account, l1_chain_id).await.unwrap()

Check warning on line 113 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L105-L113

Added lines #L105 - L113 were not covered by tests
};

Ok(PromptOutcome {
account: account_address,
settlement_contract,
settlement_id: parse_cairo_short_string(&l1_chain_id)?,
id: chain_id,
rpc_url: settlement_url,
})
Some(Ok(Outcome {
id,
settlement_contract,
rpc_url: settlement_url,
account: settlement_account_address,
settlement_id: parse_cairo_short_string(&l1_chain_id).unwrap(),
}))

Check warning on line 122 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L116-L122

Added lines #L116 - L122 were not covered by tests
} else {
None

Check warning on line 124 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L124

Added line #L124 was not covered by tests
}
}
}

#[derive(Debug)]
struct PromptOutcome {
struct Outcome {
/// the account address that is used to send the transactions for contract
/// deployment/initialization.
account: ContractAddress,
pub account: ContractAddress,

// the id of the new chain to be initialized.
id: String,
pub id: String,

// the chain id of the settlement layer.
settlement_id: String,
pub settlement_id: String,

// the rpc url for the settlement layer.
rpc_url: Url,
pub rpc_url: Url,

settlement_contract: ContractAddress,
pub settlement_contract: ContractAddress,
}

lazy_static! {
Expand All @@ -192,3 +153,77 @@
genesis
};
}

#[derive(Debug, thiserror::Error)]

Check warning on line 157 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L157

Added line #L157 was not covered by tests
#[error("Unsupported settlement chain: {id}")]
struct SettlementChainTryFromStrError {
id: String,
}

#[derive(Debug, Clone, strum_macros::Display)]

Check warning on line 163 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L163

Added line #L163 was not covered by tests
enum SettlementChain {
Sepolia,
#[cfg(feature = "init-custom-settlement-chain")]
Custom(Url),
}

impl std::str::FromStr for SettlementChain {
type Err = SettlementChainTryFromStrError;
fn from_str(s: &str) -> Result<SettlementChain, <Self as ::core::str::FromStr>::Err> {
let id = s.to_lowercase();
if &id == "sepolia" || &id == "sn_sepolia" {
return Ok(SettlementChain::Sepolia);
}

#[cfg(feature = "init-custom-settlement-chain")]
if let Ok(url) = Url::parse(s) {
return Ok(SettlementChain::Custom(url));
};

Err(SettlementChainTryFromStrError { id: s.to_string() })
}
}

impl TryFrom<&str> for SettlementChain {
type Error = SettlementChainTryFromStrError;
fn try_from(s: &str) -> Result<SettlementChain, <Self as TryFrom<&str>>::Error> {
SettlementChain::from_str(s)
}
}

#[cfg(test)]
mod tests {
use assert_matches::assert_matches;

use super::*;

#[test]
fn sepolia_from_str() {
assert_matches!(SettlementChain::from_str("sepolia"), Ok(SettlementChain::Sepolia));
assert_matches!(SettlementChain::from_str("SEPOLIA"), Ok(SettlementChain::Sepolia));
assert_matches!(SettlementChain::from_str("sn_sepolia"), Ok(SettlementChain::Sepolia));
assert_matches!(SettlementChain::from_str("SN_SEPOLIA"), Ok(SettlementChain::Sepolia));
}

#[test]
fn invalid_chain() {
assert!(SettlementChain::from_str("invalid_chain").is_err());
}

#[test]
fn try_from_str() {
assert!(matches!(SettlementChain::try_from("sepolia"), Ok(SettlementChain::Sepolia)));
assert!(SettlementChain::try_from("invalid").is_err(),);
}

#[test]
#[cfg(feature = "init-custom-settlement-chain")]
fn custom_settlement_chain() {
assert_matches!(

Check warning on line 222 in bin/katana/src/cli/init/mod.rs

View check run for this annotation

Codecov / codecov/patch

bin/katana/src/cli/init/mod.rs#L222

Added line #L222 was not covered by tests
SettlementChain::from_str("http://localhost:5050"),
Ok(SettlementChain::Custom(actual_url)) => {
assert_eq!(actual_url, Url::parse("http://localhost:5050").unwrap());
}
);
}
}
Loading
Loading