@@ -4,6 +4,9 @@ use std::io::BufReader;
44use std:: io:: Write ;
55use std:: path:: PathBuf ;
66use std:: str:: FromStr ;
7+ use std:: time:: Duration ;
8+ use std:: time:: SystemTime ;
9+ use std:: time:: UNIX_EPOCH ;
710
811use aligned_sdk:: aggregation_layer;
912use aligned_sdk:: aggregation_layer:: AggregationModeVerificationData ;
@@ -19,13 +22,18 @@ use aligned_sdk::verification_layer::estimate_fee;
1922use aligned_sdk:: verification_layer:: get_chain_id;
2023use aligned_sdk:: verification_layer:: get_nonce_from_batcher;
2124use 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;
2229use aligned_sdk:: verification_layer:: { deposit_to_aligned, get_balance_in_aligned} ;
2330use aligned_sdk:: verification_layer:: { get_vk_commitment, save_response, submit_multiple} ;
2431use clap:: Args ;
2532use clap:: Parser ;
2633use clap:: Subcommand ;
2734use clap:: ValueEnum ;
2835use env_logger:: Env ;
36+ use ethers:: core:: k256:: pkcs8:: der:: asn1:: UtcTime ;
2937use ethers:: prelude:: * ;
3038use ethers:: utils:: format_ether;
3139use ethers:: utils:: hex;
@@ -41,8 +49,11 @@ use crate::AlignedCommands::GetUserBalance;
4149use crate :: AlignedCommands :: GetUserNonce ;
4250use crate :: AlignedCommands :: GetUserNonceFromEthereum ;
4351use crate :: AlignedCommands :: GetVkCommitment ;
52+ use crate :: AlignedCommands :: LockFunds ;
4453use crate :: AlignedCommands :: Submit ;
54+ use crate :: AlignedCommands :: UnlockFunds ;
4555use 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 ) ]
213262pub 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+ }
0 commit comments