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

test: starkli in test environment #826

Closed
wants to merge 1 commit into from
Closed
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,117 changes: 1,016 additions & 101 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ web-time = "1.1.0"
anyhow = "1.0.92"
alloy-primitives = { version = "0.8.5", default-features = false }
chrono = "0.4.38"
clap = "4.5.20"
katana-core = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.9" }
katana-executor = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.9" }
katana-node = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.9" }
Expand All @@ -81,6 +82,8 @@ katana-rpc = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.9
katana-rpc-api = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.9" }
scarb = { git = "https://github.com/software-mansion/scarb/", tag = "v2.8.3" }
semver = { version = "1", features = ["serde"] }
starkli = { git = "https://github.com/ICavlek/starkli", branch = "ic/starkli_as_lib" }
starknet = { git = "https://github.com/xJonathanLEI/starknet-rs", rev = "db1fa598232f0698d942cc974f481b5d888ac080", features = ["ledger"] }
wiremock = "0.6.2"

[patch.crates-io]
Expand Down
75 changes: 69 additions & 6 deletions tests/account_katana.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::{thread, time};
use std::{sync::Arc, thread, time};

use ::starknet::core::types::PriceUnit;
use beerus::{
client::Http,
client::{Http, State},
gen::{
client::Client, Address, BlockId, BlockTag, BroadcastedDeclareTxn,
BroadcastedDeployAccountTxn, BroadcastedInvokeTxn, BroadcastedTxn,
Expand All @@ -10,12 +11,20 @@ use beerus::{
InvokeTxnV1Type, InvokeTxnV1Version, Rpc, SimulationFlagForEstimateFee,
TxnHash,
},
rpc::{serve, Server},
};
use starknet::constants::{
CLASS_HASH, COMPILED_ACCOUNT_CONTRACT_V2, COMPILED_ACCOUNT_CONTRACT_V3,
CONTRACT_ADDRESS, DECLARE_ACCOUNT_V2, DECLARE_ACCOUNT_V3, SENDER_ADDRESS,
use common::err::Error;
use starknet::{
constants::{
CLASS_HASH, COMPILED_ACCOUNT_CONTRACT_V2, COMPILED_ACCOUNT_CONTRACT_V3,
CONTRACT_ADDRESS, DECLARE_ACCOUNT_V2, DECLARE_ACCOUNT_V3,
SENDER_ADDRESS,
},
starkli::{PreFundedAccount, Starkli},
utils,
};
use starknet::katana::Katana;
use starknet::{katana::Katana, scarb};
use tokio::sync::RwLock;

mod common;
mod starknet;
Expand All @@ -27,6 +36,22 @@ async fn setup() -> (Katana, Client<Http>) {
(katana, client)
}

async fn setup_beerus_with_katana() -> Result<(Server, Katana), Error> {
let katana = Katana::init("http://127.0.0.1:0").await?;
let state = State {
block_number: 0,
block_hash: Felt::try_new("0x0")?,
root: Felt::try_new("0x0")?,
};
let beerus = serve(
&format!("http://127.0.0.1:{}", katana.port()),
"127.0.0.1:0",
Arc::new(RwLock::new(state)),
)
.await?;
Ok((beerus, katana))
}

#[tokio::test]
async fn declare_account_v3() {
let (_katana, client) = setup().await;
Expand All @@ -42,6 +67,44 @@ async fn declare_deploy_account_v2() {
deploy(client).await;
}

#[tokio::test]
async fn deploy_account_on_katana() -> Result<(), Error> {
let (beerus, katana) = setup_beerus_with_katana().await?;

let (account_folder, toml, id) = utils::prepare_account()?;
scarb::compile_blocking(toml).await?;

let mut starkli = Starkli::new(
&format!("http://127.0.0.1:{}/rpc", beerus.port()),
&account_folder,
PreFundedAccount::Katana,
);
let key = starkli.create_keystore()?;
let class_hash = starkli.extract_class_hash()?;
let address = starkli.create_account(key.clone(), class_hash).await?;
starkli.declare_account().await?;
starkli
.invoke_eth_transfer(address, 5000000000000000000, PriceUnit::Wei)
.await?;
starkli.deploy_account().await?;

// Redirect starkli to katana in verification because katana does not support
// pathfinder methods which are being called in beerus stateless call execution
// TODO: Use beerus when test node with supported pathfinder methods is used
starkli.rpc = format!("http://127.0.0.1:{}", katana.port());

let res_id = starkli.call(address, "id").await?;
assert_eq!(res_id.len(), 2);
assert_eq!(res_id[0].to_string(), id);
assert_eq!(res_id[1], starknet_crypto::Felt::ZERO);

let res_public_key = starkli.call(address, "public_key").await?;
assert_eq!(res_public_key.len(), 1);
assert_eq!(res_public_key[0], key.verifying_key().scalar());

Ok(())
}

async fn declare(
client: &Client<Http>,
compiled_contract: &str,
Expand Down
23 changes: 15 additions & 8 deletions tests/starknet/contract/account/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ trait IAccount<T> {
fn is_valid_signature(self: @T, hash: felt252, signature: Array<felt252>) -> felt252;
fn supports_interface(self: @T, interface_id: felt252) -> bool;
fn public_key(self: @T) -> felt252;
fn id(self: @T) -> u128;
fn id(self: @T) -> u256;
}

#[starknet::contract]
#[starknet::interface]
trait ProtocolTrait<T> {
fn __execute__(ref self: T, calls: Array<Call>) -> Array<Span<felt252>>;
fn __validate__(self: @T, calls: Array<Call>) -> felt252;
fn __validate_declare__(self: @T, class_hash: felt252) -> felt252;
fn __validate_deploy__(self: @T, class_hash: felt252, salt: felt252, public_key: felt252) -> felt252;
}

#[starknet::contract(account)]
mod Account {
use super::{Call, IAccount, SUPPORTED_TX_VERSION};
use super::{Call, IAccount, ProtocolTrait, SUPPORTED_TX_VERSION};
use starknet::{get_caller_address, call_contract_syscall, get_tx_info, VALIDATED};
use zeroable::Zeroable;
use array::{ArrayTrait, SpanTrait};
Expand All @@ -29,7 +37,7 @@ mod Account {
#[storage]
struct Storage {
public_key: felt252,
id: u128
id: u256,
}

#[constructor]
Expand All @@ -53,14 +61,13 @@ mod Account {
self.public_key.read()
}

fn id(self: @ContractState) -> u128 {
fn id(self: @ContractState) -> u256 {
self.id.read()
}
}

#[abi(per_item)]
#[generate_trait]
impl ProtocolImpl of ProtocolTrait {
#[abi(embed_v0)]
impl ProtocolImpl of ProtocolTrait<ContractState> {
fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
self.only_protocol();
self.only_supported_tx_version(SUPPORTED_TX_VERSION::INVOKE);
Expand Down
1 change: 1 addition & 0 deletions tests/starknet/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod constants;
pub mod katana;
pub mod scarb;
pub mod starkli;
pub mod utils;
6 changes: 6 additions & 0 deletions tests/starknet/scarb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ use scarb::{
use semver::Version;
use std::{fs::canonicalize, path::PathBuf};

#[allow(dead_code)]
pub async fn compile_blocking(toml: String) -> Result<(), Error> {
tokio::task::spawn_blocking(move || -> Result<(), Error> { compile(toml) })
.await?
}

#[allow(dead_code)]
pub fn compile(toml: String) -> Result<(), Error> {
let toml_absolute = canonicalize(PathBuf::from(toml))?;
Expand Down
178 changes: 178 additions & 0 deletions tests/starknet/starkli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use std::io::Write;

use anyhow::{anyhow, Error};
use clap::Parser;
use starkli::{
account::{
AccountConfig, AccountVariant, DeploymentStatus, OzAccountConfig,
UndeployedStatus,
},
signer::AnySigner,
utils::{Cli, Subcommands},
};
use starknet::{
core::types::{contract::SierraClass, PriceUnit},
signers::{LocalWallet, Signer, SigningKey},
};
use starknet_crypto::Felt;

#[allow(dead_code)]
pub struct Starkli {
pub rpc: String,
account_folder: String,
prefunded_account: String,
}

#[allow(dead_code)]
pub enum PreFundedAccount {
Katana,
Sepolia,
}

const ACCOUNT: &str = "account.json";
const COMPILED_ACCOUNT: &str = "target/dev/account_Account.contract_class.json";
const KEY: &str = "key.json";
const PASSWORD: &str = "password";

#[allow(dead_code)]
impl Starkli {
pub fn new(
rpc: &str,
account_folder: &str,
prefunded_account: PreFundedAccount,
) -> Self {
let prefunded_account = match prefunded_account {
PreFundedAccount::Katana => "katana-0".to_string(),
PreFundedAccount::Sepolia => "TODO".to_string(),
};
Self {
rpc: rpc.into(),
account_folder: account_folder.into(),
prefunded_account,
}
}

pub fn create_keystore(&self) -> Result<SigningKey, Error> {
let key = SigningKey::from_random();
let key_file = self.account_folder.clone() + KEY;
key.save_as_keystore(key_file, PASSWORD)?;
Ok(key)
}

pub fn extract_class_hash(&self) -> Result<Felt, Error> {
let compiled = self.account_folder.clone() + COMPILED_ACCOUNT;
let class = serde_json::from_reader::<_, SierraClass>(
std::fs::File::open(compiled)?,
)?;
Ok(class.class_hash()?)
}

pub async fn create_account(
&self,
key: SigningKey,
class_hash: Felt,
) -> Result<Felt, Error> {
let signer = AnySigner::LocalWallet(LocalWallet::from_signing_key(key));
let salt = SigningKey::from_random().secret_scalar();
let account_config = AccountConfig {
version: 1,
variant: AccountVariant::OpenZeppelin(OzAccountConfig {
version: 1,
public_key: signer.get_public_key().await?.scalar(),
legacy: false,
}),
deployment: DeploymentStatus::Undeployed(UndeployedStatus {
class_hash,
salt,
context: None,
}),
};
let target_deployment_address =
account_config.deploy_account_address()?;
let mut file =
std::fs::File::create(self.account_folder.clone() + ACCOUNT)?;
serde_json::to_writer_pretty(&mut file, &account_config)?;
file.write_all(b"\n")?;
Ok(target_deployment_address)
}

pub async fn declare_account(&self) -> Result<(), Error> {
let compiled_contract = self.account_folder.clone() + COMPILED_ACCOUNT;
let input = vec![
"starkli",
"declare",
&compiled_contract,
"--compiler-version",
"2.8.2",
"--rpc",
&self.rpc,
"--account",
&self.prefunded_account,
];
starkli::utils::run_command(Cli::parse_from(input)).await
}

pub async fn invoke_eth_transfer(
&self,
to_address: Felt,
amount: u64,
unit: PriceUnit,
) -> Result<(), Error> {
let address = &format!("{:#064x}", to_address);
let amount = &format!("u256:{amount}");
let unit = match unit {
PriceUnit::Wei => "--eth",
PriceUnit::Fri => "--strk",
};
let input = vec![
"starkli",
"invoke",
unit,
"eth",
"transfer",
address,
amount,
"--rpc",
&self.rpc,
"--account",
&self.prefunded_account,
];
starkli::utils::run_command(Cli::parse_from(input)).await
}

pub async fn deploy_account(&self) -> Result<(), Error> {
let account = self.account_folder.clone() + "account.json";
let key = self.account_folder.clone() + "key.json";
let input = vec![
"starkli",
"account",
"deploy",
&account,
"--rpc",
&self.rpc,
"--keystore",
&key,
"--keystore-password",
"password",
];
starkli::utils::run_command(Cli::parse_from(input)).await
}

pub async fn call(
&self,
address: Felt,
func: &str,
) -> Result<Vec<Felt>, Error> {
let address = &format!("{:#064x}", address);
let input = vec!["starkli", "call", address, func, "--rpc", &self.rpc];
let cli = Cli::parse_from(input);
let cmd = match cli.command {
Some(command) => match command {
Subcommands::Call(cmd) => cmd,
_ => return Err(anyhow!("Wrong subcommand")),
},
None => return Err(anyhow!("Wrong command")),
};
cmd.call().await
}
}
14 changes: 7 additions & 7 deletions tests/starknet/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ const SOURCE_LIB: &str = "./tests/starknet/contract/account/src/lib.cairo";
const SOURCE_SCARB: &str = "./tests/starknet/contract/account/Scarb.toml";

#[allow(dead_code)]
pub fn prepare_account() -> Result<String, Error> {
pub fn prepare_account() -> Result<(String, String, String), Error> {
let now = chrono::offset::Local::now();
let id = now.format("%Y%m%y%H%M%S").to_string();
let target = "./target/account-".to_string() + &id;
let target_lib = target.clone() + "/src/lib.cairo";
let target_scarb = target.clone() + "/Scarb.toml";
let target_src = target.clone() + "/src";
let target = "./target/account-".to_string() + &id + "/";
let target_lib = target.clone() + "src/lib.cairo";
let target_scarb = target.clone() + "Scarb.toml";
let target_src = target.clone() + "src";

fs::create_dir(target)?;
fs::create_dir(target.clone())?;
fs::create_dir(target_src)?;
fs::copy(SOURCE_LIB, target_lib.clone())?;
fs::copy(SOURCE_SCARB, target_scarb.clone())?;
Expand All @@ -24,5 +24,5 @@ pub fn prepare_account() -> Result<String, Error> {
let account_new = account_template.replace("<ID>", &id);
fs::write(target_lib, account_new)?;

Ok(target_scarb)
Ok((target, target_scarb, id))
}
Loading