Skip to content

Commit 9d8d86c

Browse files
MarcosNicolauJuArcemaximopalopoli
authored
feat: unlock, lock and withdraw funds in sdk and cli (#2149)
Co-authored-by: JuArce <[email protected]> Co-authored-by: Maximo Palopoli <[email protected]>
1 parent c281383 commit 9d8d86c

File tree

5 files changed

+658
-3
lines changed

5 files changed

+658
-3
lines changed

crates/cli/src/main.rs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use std::io::BufReader;
44
use std::io::Write;
55
use std::path::PathBuf;
66
use std::str::FromStr;
7+
use std::time::Duration;
8+
use std::time::SystemTime;
9+
use std::time::UNIX_EPOCH;
710

811
use aligned_sdk::aggregation_layer;
912
use aligned_sdk::aggregation_layer::AggregationModeVerificationData;
@@ -19,13 +22,18 @@ use aligned_sdk::verification_layer::estimate_fee;
1922
use aligned_sdk::verification_layer::get_chain_id;
2023
use aligned_sdk::verification_layer::get_nonce_from_batcher;
2124
use aligned_sdk::verification_layer::get_nonce_from_ethereum;
25+
use aligned_sdk::verification_layer::get_unlock_block_time;
26+
use aligned_sdk::verification_layer::lock_balance_in_aligned;
27+
use aligned_sdk::verification_layer::unlock_balance_in_aligned;
28+
use aligned_sdk::verification_layer::withdraw_balance_from_aligned;
2229
use aligned_sdk::verification_layer::{deposit_to_aligned, get_balance_in_aligned};
2330
use aligned_sdk::verification_layer::{get_vk_commitment, save_response, submit_multiple};
2431
use clap::Args;
2532
use clap::Parser;
2633
use clap::Subcommand;
2734
use clap::ValueEnum;
2835
use env_logger::Env;
36+
use ethers::core::k256::pkcs8::der::asn1::UtcTime;
2937
use ethers::prelude::*;
3038
use ethers::utils::format_ether;
3139
use ethers::utils::hex;
@@ -41,8 +49,11 @@ use crate::AlignedCommands::GetUserBalance;
4149
use crate::AlignedCommands::GetUserNonce;
4250
use crate::AlignedCommands::GetUserNonceFromEthereum;
4351
use crate::AlignedCommands::GetVkCommitment;
52+
use crate::AlignedCommands::LockFunds;
4453
use crate::AlignedCommands::Submit;
54+
use crate::AlignedCommands::UnlockFunds;
4555
use crate::AlignedCommands::VerifyProofOnchain;
56+
use crate::AlignedCommands::WithdrawFunds;
4657

4758
#[derive(Parser, Debug)]
4859
#[command(version, about, long_about = None)]
@@ -65,6 +76,12 @@ pub enum AlignedCommands {
6576
name = "deposit-to-batcher"
6677
)]
6778
DepositToBatcher(DepositToBatcherArgs),
79+
#[clap(about = "Unlocks funds from the batcher", name = "unlock-funds")]
80+
UnlockFunds(LockUnlockFundsArgs),
81+
#[clap(about = "Lock funds in the batcher", name = "lock-funds")]
82+
LockFunds(LockUnlockFundsArgs),
83+
#[clap(about = "Withdraw funds from the batcher", name = "withdraw-funds")]
84+
WithdrawFunds(WithdrawFundsArgs),
6885
#[clap(about = "Get user balance from the batcher", name = "get-user-balance")]
6986
GetUserBalance(GetUserBalanceArgs),
7087
#[clap(
@@ -208,6 +225,38 @@ pub struct DepositToBatcherArgs {
208225
amount: String,
209226
}
210227

228+
#[derive(Parser, Debug)]
229+
#[command(version, about, long_about = None)]
230+
pub struct LockUnlockFundsArgs {
231+
#[command(flatten)]
232+
private_key_type: PrivateKeyType,
233+
#[arg(
234+
name = "Ethereum RPC provider address",
235+
long = "rpc_url",
236+
default_value = "http://localhost:8545"
237+
)]
238+
eth_rpc_url: String,
239+
#[clap(flatten)]
240+
network: NetworkArg,
241+
}
242+
243+
#[derive(Parser, Debug)]
244+
#[command(version, about, long_about = None)]
245+
pub struct WithdrawFundsArgs {
246+
#[command(flatten)]
247+
private_key_type: PrivateKeyType,
248+
#[arg(
249+
name = "Ethereum RPC provider address",
250+
long = "rpc_url",
251+
default_value = "http://localhost:8545"
252+
)]
253+
eth_rpc_url: String,
254+
#[clap(flatten)]
255+
network: NetworkArg,
256+
#[arg(name = "Amount to withdraw", long = "amount", required = true)]
257+
amount: String,
258+
}
259+
211260
#[derive(Parser, Debug)]
212261
#[command(version, about, long_about = None)]
213262
pub struct VerifyProofOnchainArgs {
@@ -752,6 +801,197 @@ async fn main() -> Result<(), AlignedError> {
752801
}
753802
}
754803
}
804+
UnlockFunds(args) => {
805+
let eth_rpc_url = args.eth_rpc_url;
806+
let eth_rpc_provider =
807+
Provider::<Http>::try_from(eth_rpc_url.clone()).map_err(|e| {
808+
SubmitError::EthereumProviderError(format!(
809+
"Error while connecting to Ethereum: {}",
810+
e
811+
))
812+
})?;
813+
814+
let keystore_path = &args.private_key_type.keystore_path;
815+
let private_key = &args.private_key_type.private_key;
816+
817+
let mut wallet = if let Some(keystore_path) = keystore_path {
818+
let password = rpassword::prompt_password("Please enter your keystore password:")
819+
.map_err(|e| SubmitError::GenericError(e.to_string()))?;
820+
Wallet::decrypt_keystore(keystore_path, password)
821+
.map_err(|e| SubmitError::GenericError(e.to_string()))?
822+
} else if let Some(private_key) = private_key {
823+
private_key
824+
.parse::<LocalWallet>()
825+
.map_err(|e| SubmitError::GenericError(e.to_string()))?
826+
} else {
827+
warn!("Missing keystore or private key used for payment.");
828+
return Ok(());
829+
};
830+
831+
let chain_id = get_chain_id(eth_rpc_url.as_str()).await?;
832+
wallet = wallet.with_chain_id(chain_id);
833+
834+
let client = SignerMiddleware::new(eth_rpc_provider, wallet);
835+
836+
match unlock_balance_in_aligned(&client, args.network.into()).await {
837+
Ok(receipt) => {
838+
info!(
839+
"Funds in batcher unlocked successfully. Receipt: 0x{:x}",
840+
receipt.transaction_hash
841+
);
842+
}
843+
Err(e) => {
844+
error!("Transaction failed: {:?}", e);
845+
}
846+
}
847+
}
848+
LockFunds(args) => {
849+
let eth_rpc_url = args.eth_rpc_url;
850+
let eth_rpc_provider =
851+
Provider::<Http>::try_from(eth_rpc_url.clone()).map_err(|e| {
852+
SubmitError::EthereumProviderError(format!(
853+
"Error while connecting to Ethereum: {}",
854+
e
855+
))
856+
})?;
857+
858+
let keystore_path = &args.private_key_type.keystore_path;
859+
let private_key = &args.private_key_type.private_key;
860+
861+
let mut wallet = if let Some(keystore_path) = keystore_path {
862+
let password = rpassword::prompt_password("Please enter your keystore password:")
863+
.map_err(|e| SubmitError::GenericError(e.to_string()))?;
864+
Wallet::decrypt_keystore(keystore_path, password)
865+
.map_err(|e| SubmitError::GenericError(e.to_string()))?
866+
} else if let Some(private_key) = private_key {
867+
private_key
868+
.parse::<LocalWallet>()
869+
.map_err(|e| SubmitError::GenericError(e.to_string()))?
870+
} else {
871+
warn!("Missing keystore or private key used for payment.");
872+
return Ok(());
873+
};
874+
875+
let chain_id = get_chain_id(eth_rpc_url.as_str()).await?;
876+
wallet = wallet.with_chain_id(chain_id);
877+
878+
let client = SignerMiddleware::new(eth_rpc_provider, wallet);
879+
880+
match lock_balance_in_aligned(&client, args.network.into()).await {
881+
Ok(receipt) => {
882+
info!(
883+
"Funds in batcher locked successfully. Receipt: 0x{:x}",
884+
receipt.transaction_hash
885+
);
886+
}
887+
Err(e) => {
888+
error!("Transaction failed: {:?}", e);
889+
}
890+
}
891+
}
892+
WithdrawFunds(args) => {
893+
if !args.amount.ends_with("ether") {
894+
error!("Amount should be in the format XX.XXether");
895+
return Ok(());
896+
}
897+
898+
let amount_ether = args.amount.replace("ether", "");
899+
900+
let amount_wei = parse_ether(&amount_ether).map_err(|e| {
901+
SubmitError::EthereumProviderError(format!("Error while parsing amount: {}", e))
902+
})?;
903+
904+
let eth_rpc_url = args.eth_rpc_url;
905+
let eth_rpc_provider =
906+
Provider::<Http>::try_from(eth_rpc_url.clone()).map_err(|e| {
907+
SubmitError::EthereumProviderError(format!(
908+
"Error while connecting to Ethereum: {}",
909+
e
910+
))
911+
})?;
912+
913+
let keystore_path = &args.private_key_type.keystore_path;
914+
let private_key = &args.private_key_type.private_key;
915+
916+
let mut wallet = if let Some(keystore_path) = keystore_path {
917+
let password = rpassword::prompt_password("Please enter your keystore password:")
918+
.map_err(|e| SubmitError::GenericError(e.to_string()))?;
919+
Wallet::decrypt_keystore(keystore_path, password)
920+
.map_err(|e| SubmitError::GenericError(e.to_string()))?
921+
} else if let Some(private_key) = private_key {
922+
private_key
923+
.parse::<LocalWallet>()
924+
.map_err(|e| SubmitError::GenericError(e.to_string()))?
925+
} else {
926+
warn!("Missing keystore or private key used for payment.");
927+
return Ok(());
928+
};
929+
930+
let unlock_block_time = match get_unlock_block_time(
931+
wallet.address(),
932+
&eth_rpc_url,
933+
args.network.clone().into(),
934+
)
935+
.await
936+
{
937+
Ok(value) => value,
938+
Err(e) => {
939+
error!("Failed to get unlock time: {:?}", e);
940+
return Ok(());
941+
}
942+
};
943+
944+
let current_timestamp = SystemTime::now()
945+
.duration_since(UNIX_EPOCH)
946+
.expect("Time went backwards")
947+
.as_secs();
948+
949+
let unlock_time = UtcTime::from_unix_duration(Duration::from_secs(unlock_block_time))
950+
.expect("invalid unlock time");
951+
let now_time =
952+
UtcTime::from_system_time(SystemTime::now()).expect("invalid system time");
953+
954+
let retry_after_minutes = if unlock_block_time > current_timestamp {
955+
(unlock_block_time - current_timestamp) / 60
956+
} else {
957+
0
958+
};
959+
960+
if unlock_block_time == 0 {
961+
error!("Funds are locked, you need to unlock them first.");
962+
return Ok(());
963+
}
964+
965+
if unlock_block_time > current_timestamp {
966+
warn!(
967+
"Funds are still locked. You need to wait {} minutes before being able to withdraw after unlocking the funds.\n\
968+
Unlocks in block time: {}\n\
969+
Current time: {}",
970+
retry_after_minutes,
971+
format_utc_time(unlock_time),
972+
format_utc_time(now_time)
973+
);
974+
975+
return Ok(());
976+
}
977+
978+
let chain_id = get_chain_id(eth_rpc_url.as_str()).await?;
979+
wallet = wallet.with_chain_id(chain_id);
980+
981+
let client = SignerMiddleware::new(eth_rpc_provider, wallet);
982+
983+
match withdraw_balance_from_aligned(&client, args.network.into(), amount_wei).await {
984+
Ok(receipt) => {
985+
info!(
986+
"Balance withdraw from batcher successfully. Receipt: 0x{:x}",
987+
receipt.transaction_hash
988+
);
989+
}
990+
Err(e) => {
991+
error!("Transaction failed: {:?}", e);
992+
}
993+
}
994+
}
755995
GetUserBalance(get_user_balance_args) => {
756996
let user_address = H160::from_str(&get_user_balance_args.user_address).unwrap();
757997
match get_balance_in_aligned(
@@ -1021,3 +1261,17 @@ pub async fn get_user_balance(
10211261
))
10221262
}
10231263
}
1264+
1265+
fn format_utc_time(date: UtcTime) -> String {
1266+
let dt = date.to_date_time();
1267+
1268+
format!(
1269+
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}Z",
1270+
dt.year(),
1271+
dt.month(),
1272+
dt.day(),
1273+
dt.hour(),
1274+
dt.minutes(),
1275+
dt.seconds()
1276+
)
1277+
}

crates/sdk/src/eth/batcher_payment_service.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::sync::Arc;
22

3-
use ethers::prelude::*;
3+
use ethers::{core::k256::ecdsa::SigningKey, prelude::*};
44

55
use crate::common::errors::VerificationError;
66

@@ -11,6 +11,9 @@ abigen!(
1111

1212
pub type BatcherPaymentService = BatcherPaymentServiceContract<Provider<Http>>;
1313

14+
pub type SignerMiddlewareT = SignerMiddleware<Provider<Http>, Wallet<SigningKey>>;
15+
pub type BatcherPaymentServiceWithSigner = BatcherPaymentServiceContract<SignerMiddlewareT>;
16+
1417
pub async fn batcher_payment_service(
1518
provider: Provider<Http>,
1619
contract_address: H160,

0 commit comments

Comments
 (0)