Skip to content

feat(sdk): keystore full example #144

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

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8e316a8
Init
Mechanix97 Jun 18, 2025
cc29521
Merge branch 'main' into mecha/add-examples
Mechanix97 Jun 18, 2025
7c9e1b3
Add solidity contract
Mechanix97 Jun 18, 2025
619d447
Deploy contract
Mechanix97 Jun 18, 2025
e2652f1
Try deploy contract
Mechanix97 Jun 18, 2025
55ea9be
Add transfer
Mechanix97 Jun 19, 2025
3293c71
Fix gas
Mechanix97 Jun 19, 2025
1bfec25
Add call params and example
Mechanix97 Jun 19, 2025
e25a609
Add git ignore
Mechanix97 Jun 19, 2025
b3900ba
Change call data
Mechanix97 Jun 19, 2025
d714e98
Remove comment
Mechanix97 Jun 19, 2025
2d986cb
Last commit
Mechanix97 Jun 19, 2025
0af470c
Untrack `solc_out`
ilitteri Jun 19, 2025
8fc17b5
Rename contract file
ilitteri Jun 19, 2025
7f2c418
Add `.gitignore` to SDK
ilitteri Jun 19, 2025
09167bd
Reorder keystore example structure + download deps and compile contract
ilitteri Jun 19, 2025
764342f
Fix transfer impl
ilitteri Jun 19, 2025
03327f2
Fix deployment
ilitteri Jun 19, 2025
2deb488
add call
Mechanix97 Jun 23, 2025
6638278
Use send tx
Mechanix97 Jun 23, 2025
0053423
fix gitignore
Mechanix97 Jun 23, 2025
b3dcd42
Add comments
Mechanix97 Jun 23, 2025
2a7b330
Fix simple usage
Mechanix97 Jun 23, 2025
c11f069
Add RPC url
Mechanix97 Jun 23, 2025
c86157a
Fix docs
Mechanix97 Jun 23, 2025
9db0d66
Merge branch 'main' into mecha/add-examples
Mechanix97 Jun 23, 2025
5ba6d87
Add cargo lock
Mechanix97 Jun 23, 2025
3afe6b3
Remove dep
Mechanix97 Jun 23, 2025
466c044
Recover address
Mechanix97 Jun 23, 2025
9ed9e1f
Improve comments
Mechanix97 Jun 23, 2025
ed1f35f
add private key in docs
Mechanix97 Jun 23, 2025
ef901a7
Have mandatory pk
Mechanix97 Jun 24, 2025
c58dda8
strip prefix
Mechanix97 Jun 25, 2025
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
117 changes: 59 additions & 58 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PRIVATE_KEY> --rpc-url <RPC_URL>
cargo run --release --example keystore -- --private-key <PRIVATE_KEY> --rpc-url <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).

Expand Down
2 changes: 2 additions & 0 deletions sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
examples/keystore/contracts/lib
examples/keystore/contracts/solc_out
5 changes: 5 additions & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ path = "./src/sdk.rs"
[[example]]
name = "simple_usage"
path = "./examples/simple_usage.rs"


[[example]]
name = "keystore"
path = "./examples/keystore/main.rs"
24 changes: 24 additions & 0 deletions sdk/examples/keystore/contracts/RecoverSigner.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
305 changes: 305 additions & 0 deletions sdk/examples/keystore/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
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<dyn std::error::Error>> {
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,
&eth_client,
Overrides::default(),
)
.await?;

let transfer_receipt =
wait_for_transaction_receipt(transfer_tx_hash, &eth_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, &eth_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()
},
10,
)
.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, &eth_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_signature(
from_block,
to_block,
deployed_address,
"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}");
}
}
6 changes: 2 additions & 4 deletions sdk/examples/simple_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,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,
}

Expand All @@ -33,9 +33,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);
let eth_client = EthClient::new(&args.rpc_url);

let account_balance = eth_client.get_balance(account).await.unwrap();

Expand Down
Loading