diff --git a/README.md b/README.md index ba5e038..ca65276 100644 --- a/README.md +++ b/README.md @@ -252,19 +252,20 @@ async fn main() { } ``` -#### Running the example +#### Running the examples > [!WARNING] -> Before running the example, make sure you have an Ethereum node running or override the default RPC URL with the `--rpc-url` flag to point to a public node. +> Before running the examples, make sure you have an Ethereum node running or override the default RPC URL with the `--rpc-url` flag to point to a public node. > The account associated to the private key must have some funds in the network you are connecting to. ```Shell cd sdk cargo run --release --example simple_usage -- --private-key --rpc-url +cargo run --release --example keystore -- --private-key --rpc-url ``` > [!NOTE] -> You can find the code for this example in `sdk/examples/simple_usage.rs`. +> You can find the code for these examples in `sdk/examples/`. You can find the SDK documentation [here](sdk/README.md). diff --git a/cli/src/cli.rs b/cli/src/cli.rs index daab90d..7676c3d 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -331,8 +331,15 @@ impl Command { let client = EthClient::new(&rpc_url)?; - let tx_hash = - transfer(args.amount, from, args.to, &args.private_key, &client).await?; + let tx_hash = transfer( + args.amount, + from, + args.to, + &args.private_key, + &client, + Overrides::default(), + ) + .await?; println!("{tx_hash:#x}"); diff --git a/sdk/.gitignore b/sdk/.gitignore new file mode 100644 index 0000000..841de3d --- /dev/null +++ b/sdk/.gitignore @@ -0,0 +1,2 @@ +examples/keystore/contracts/lib +examples/keystore/contracts/solc_out diff --git a/sdk/examples/keystore/contracts/RecoverSigner.sol b/sdk/examples/keystore/contracts/RecoverSigner.sol new file mode 100644 index 0000000..8aa2f50 --- /dev/null +++ b/sdk/examples/keystore/contracts/RecoverSigner.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +contract RecoverSigner { + using MessageHashUtils for bytes32; + using Address for address; + + event RecoveredSigner( + address signer + ); + + function recoverSigner( + bytes32 message, + bytes memory signature + ) public { + bytes32 hash = message.toEthSignedMessageHash(); + address signer = ECDSA.recover(hash, signature); + emit RecoveredSigner(signer); + } +} diff --git a/sdk/examples/keystore/main.rs b/sdk/examples/keystore/main.rs new file mode 100644 index 0000000..b472310 --- /dev/null +++ b/sdk/examples/keystore/main.rs @@ -0,0 +1,304 @@ +use clap::Parser; +use ethrex_common::{Bytes, H160, H256, U256}; +use keccak_hash::keccak; +use rex_sdk::calldata::{Value, encode_calldata}; +use rex_sdk::client::eth::get_address_from_secret_key; +use rex_sdk::client::{EthClient, Overrides}; +use rex_sdk::{ + keystore::{create_new_keystore, load_keystore_from_path}, + sign::sign_hash, + transfer, wait_for_transaction_receipt, +}; +use secp256k1::SecretKey; +use std::fs::read_to_string; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; + +#[derive(Parser)] +struct ExampleArgs { + #[arg( + long, + env = "PRIVATE_KEY", + help = "The private key to derive the address from." + )] + private_key: String, + #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] + rpc_url: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = ExampleArgs::parse(); + + // 1. Download contract deps and compile contract. + setup(); + + // 2. Create a new keystore named "RexTest" in the "ContractKeystores" directory. + create_new_keystore(None, Some("RexTest"), "LambdaClass")?; + + // 3. Load the keystore with the password. + let keystore_secret_key = load_keystore_from_path(None, "RexTest", "LambdaClass")?; + let keystore_address = get_address_from_secret_key(&keystore_secret_key)?; + + println!("\nKeystore loaded successfully:"); + println!( + "\tPrivate Key: 0x{}", + hex::encode(keystore_secret_key.secret_bytes()) + ); + println!("\tAddress: {keystore_address:#x}"); + + // Connect the client to a node + let eth_client = EthClient::new(&args.rpc_url)?; + + // 4. Fund the keystore account. + let pk = &args + .private_key + .strip_prefix("0x") + .unwrap_or(&args.private_key); + let rich_wallet_pk = SecretKey::from_str(pk)?; + let rich_wallet_address = get_address_from_secret_key(&rich_wallet_pk)?; + let amount = U256::from_dec_str("1000000000000000000").expect("Failed to parse amount"); + let transfer_tx_hash = transfer( + amount, + rich_wallet_address, + keystore_address, + &rich_wallet_pk, + ð_client, + Overrides::default(), + ) + .await?; + + let transfer_receipt = + wait_for_transaction_receipt(transfer_tx_hash, ð_client, 10, true).await?; + + println!("\nFunds transferred successfully:"); + println!("\tTransfer tx hash: {transfer_tx_hash:#x}"); + println!("\tTransfer receipt: {transfer_receipt:?}"); + + // 5. Deploy the signer recovery example contract with the keystore account. + let bytecode_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("examples/keystore/contracts/solc_out") + .join("RecoverSigner.bin"); + let bytecode = hex::decode(read_to_string(bytecode_path)?)?; + let (contract_tx_hash, deployed_address) = eth_client + .deploy( + keystore_address, + keystore_secret_key, + Bytes::from(bytecode), + Overrides::default(), + ) + .await?; + + let contract_deploy_receipt = + wait_for_transaction_receipt(contract_tx_hash, ð_client, 10, true).await?; + + println!("\nContract deployed successfully:"); + println!("\tContract deployment tx hash: {contract_tx_hash:#x}"); + println!("\tContract deployment address: {deployed_address:#x}"); + println!("\tContract deployment receipt: {contract_deploy_receipt:?}"); + + // Get the current block (for later). + let from_block = eth_client.get_block_number().await?; + + // 6. Prepare the calldata to call the example contract. + // i. Prepare a message. + let message = H256::random(); + let prefix = "\x19Ethereum Signed Message:\n32"; + let mut hash_input = Vec::new(); + hash_input.extend_from_slice(prefix.as_bytes()); + hash_input.extend_from_slice(message.as_bytes()); + let hash = keccak(&hash_input); + + // ii. Sign the hash of the message with the keystore private key. + let signature = sign_hash(hash, keystore_secret_key); + + // iii. ABI-encode the parameters. + let raw_function_signature = "recoverSigner(bytes32,bytes)"; + let arguments = vec![ + Value::FixedBytes(Bytes::from(message.to_fixed_bytes().to_vec())), + Value::Bytes(Bytes::from(signature)), + ]; + let calldata = encode_calldata(raw_function_signature, &arguments).unwrap(); + + // 7. Prepare and send the transaction for calling the example contract. + let tx = eth_client + .build_eip1559_transaction( + deployed_address, + keystore_address, + calldata.into(), + Overrides { + value: Some(U256::from_dec_str("0")?), + nonce: Some(1), + chain_id: Some(9), + gas_limit: Some(2000000), + max_fee_per_gas: Some(2000000), + max_priority_fee_per_gas: Some(20000), + ..Default::default() + }, + ) + .await?; + + let sent_tx_hash = eth_client + .send_eip1559_transaction(&tx, &keystore_secret_key) + .await?; + + let sent_tx_receipt = + wait_for_transaction_receipt(sent_tx_hash, ð_client, 100, true).await?; + + println!("\nTx sent successfully:"); + println!("\tTx hash: {sent_tx_hash:#x}"); + println!("\tTx receipt: {sent_tx_receipt:?}"); + + // Get the new current block. + let to_block = eth_client.get_block_number().await?; + + // 8. Get the log emitted by the contract call execution. + let logs = eth_client + .get_logs( + from_block, + to_block, + deployed_address, + keccak("RecoveredSigner(address)"), + ) + .await?; + + println!("\tTx Logs: {:?}", logs); + + // 9. Compare it with the expected one. + let address_bytes = &logs[0].log.data[logs[0].log.data.len() - 20..]; + let recovered_address = H160::from_str(&hex::encode(address_bytes))?; + assert_eq!(recovered_address, keystore_address); + + println!("\nAddress recovered successfully!"); + println!("\tRecovered address: {recovered_address:#x}"); + + Ok(()) +} + +fn setup() { + download_contract_deps(); + compile_contracts(); +} + +fn download_contract_deps() { + println!("Downloading contract dependencies"); + + let root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + let lib_path = root_path.join("examples/keystore/contracts/lib"); + + if !lib_path.exists() { + std::fs::create_dir_all(&lib_path).expect("Failed to create lib directory"); + } + + git_clone( + "https://github.com/OpenZeppelin/openzeppelin-contracts.git", + lib_path + .join("openzeppelin-contracts") + .to_str() + .expect("Failed to get str from path"), + None, + true, + ); + + println!("Contract dependencies downloaded"); +} + +pub fn git_clone(repository_url: &str, outdir: &str, branch: Option<&str>, submodules: bool) { + println!("Cloning repository: {repository_url} into {outdir}"); + + let mut git_cmd = Command::new("git"); + + let git_clone_cmd = git_cmd.arg("clone").arg(repository_url); + + if let Some(branch) = branch { + git_clone_cmd.arg("--branch").arg(branch); + } + + if submodules { + git_clone_cmd.arg("--recurse-submodules"); + } + + git_clone_cmd + .arg(outdir) + .spawn() + .expect("Failed to spawn git clone command") + .wait() + .expect("Failed to wait for git clone command"); + + println!("Repository cloned successfully"); +} + +fn compile_contracts() { + println!("Compiling contracts"); + + let root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + let contracts_path = root_path.join("examples/keystore/contracts"); + + compile_contract(contracts_path, "RecoverSigner.sol", false); + + println!("Contracts compiled"); +} + +pub fn compile_contract(general_contracts_path: PathBuf, contract_path: &str, runtime_bin: bool) { + let bin_flag = if runtime_bin { + "--bin-runtime" + } else { + "--bin" + }; + + // Both the contract path and the output path are relative to where the Makefile is. + if !Command::new("solc") + .arg(bin_flag) + .arg( + "@openzeppelin/contracts=".to_string() + + general_contracts_path + .join("lib") + .join("openzeppelin-contracts") + .join("lib") + .join("openzeppelin-contracts") + .join("contracts") + .to_str() + .expect("Failed to get str from path"), + ) + .arg( + "@openzeppelin/contracts=".to_string() + + general_contracts_path + .join("lib") + .join("openzeppelin-contracts") + .join("contracts") + .to_str() + .expect("Failed to get str from path"), + ) + .arg( + general_contracts_path + .join(contract_path) + .to_str() + .expect("Failed to get str from path"), + ) + .arg("--via-ir") + .arg("-o") + .arg( + general_contracts_path + .join("solc_out") + .to_str() + .expect("Failed to get str from path"), + ) + .arg("--overwrite") + .arg("--allow-paths") + .arg( + general_contracts_path + .to_str() + .expect("Failed to get str from path"), + ) + .spawn() + .expect("Failed to spawn solc command") + .wait() + .expect("Failed to wait for solc command") + .success() + { + panic!("Failed to compile {contract_path}"); + } +} diff --git a/sdk/examples/simple_usage.rs b/sdk/examples/simple_usage.rs index 728ca18..cb0d48c 100644 --- a/sdk/examples/simple_usage.rs +++ b/sdk/examples/simple_usage.rs @@ -2,7 +2,10 @@ use clap::Parser; use ethrex_common::{Address, Bytes, U256}; use hex::FromHexError; use rex_sdk::{ - client::{EthClient, eth::BlockByNumber, eth::get_address_from_secret_key}, + client::{ + EthClient, Overrides, + eth::{BlockByNumber, get_address_from_secret_key}, + }, transfer, wait_for_transaction_receipt, }; use secp256k1::SecretKey; @@ -12,7 +15,7 @@ use std::str::FromStr; struct SimpleUsageArgs { #[arg(long, value_parser = parse_private_key, env = "PRIVATE_KEY", help = "The private key to derive the address from.")] private_key: SecretKey, - #[arg(default_value = "http://localhost:8545", env = "RPC_URL")] + #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] rpc_url: String, } @@ -33,9 +36,7 @@ async fn main() { let account = get_address_from_secret_key(&args.private_key).unwrap(); - let rpc_url = "http://localhost:8545"; - - let eth_client = EthClient::new(rpc_url).unwrap(); + let eth_client = EthClient::new(&args.rpc_url).unwrap(); let account_balance = eth_client .get_balance(account, BlockByNumber::Latest) @@ -57,9 +58,16 @@ async fn main() { let from = account; let to = Address::from_str("0x4852f44fd706e34cb906b399b729798665f64a83").unwrap(); - let tx_hash = transfer(amount, from, to, &args.private_key, ð_client) - .await - .unwrap(); + let tx_hash = transfer( + amount, + from, + to, + &args.private_key, + ð_client, + Overrides::default(), + ) + .await + .unwrap(); // Wait for the transaction to be finalized wait_for_transaction_receipt(tx_hash, ð_client, 100, false) diff --git a/sdk/src/l2/deposit.rs b/sdk/src/l2/deposit.rs index b8c43e0..a6b10a5 100644 --- a/sdk/src/l2/deposit.rs +++ b/sdk/src/l2/deposit.rs @@ -17,7 +17,15 @@ pub async fn deposit_through_transfer( bridge_address: Address, eth_client: &EthClient, ) -> Result { - transfer(amount, from, bridge_address, from_pk, eth_client).await + transfer( + amount, + from, + bridge_address, + from_pk, + eth_client, + Overrides::default(), + ) + .await } pub async fn deposit_through_contract_call( diff --git a/sdk/src/sdk.rs b/sdk/src/sdk.rs index 44e7f23..a6ee4b4 100644 --- a/sdk/src/sdk.rs +++ b/sdk/src/sdk.rs @@ -1,5 +1,4 @@ use crate::client::{EthClient, EthClientError, Overrides}; -use ethrex_common::types::GenericTransaction; use ethrex_common::{Address, H256, U256}; use ethrex_rpc::types::receipt::RpcReceipt; use secp256k1::SecretKey; @@ -26,33 +25,13 @@ pub async fn transfer( to: Address, private_key: &SecretKey, client: &EthClient, + mut overrides: Overrides, ) -> Result { - let gas_price = client - .get_gas_price_with_extra(20) - .await? - .try_into() - .map_err(|_| { - EthClientError::InternalError("Failed to convert gas_price to a u64".to_owned()) - })?; + overrides.value = Some(amount); - let mut tx = client - .build_eip1559_transaction( - to, - from, - Default::default(), - Overrides { - value: Some(amount), - max_fee_per_gas: Some(gas_price), - max_priority_fee_per_gas: Some(gas_price), - ..Default::default() - }, - ) + let tx = client + .build_eip1559_transaction(to, from, Default::default(), overrides) .await?; - - let mut tx_generic: GenericTransaction = tx.clone().into(); - tx_generic.from = from; - let gas_limit = client.estimate_gas(tx_generic).await?; - tx.gas_limit = gas_limit; client.send_eip1559_transaction(&tx, private_key).await } diff --git a/sdk/tests/tests.rs b/sdk/tests/tests.rs index 74e96c2..39b5392 100644 --- a/sdk/tests/tests.rs +++ b/sdk/tests/tests.rs @@ -695,6 +695,7 @@ async fn perform_transfer( transfer_recipient_address, transferer_private_key, proposer_client, + Overrides::default(), ) .await?;