diff --git a/Cargo.lock b/Cargo.lock index 4be717dc7e..b0081d8168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2268,6 +2268,7 @@ dependencies = [ "log", "network-defaults", "nymsphinx", + "pemstore", "rand 0.7.3", "secp256k1", "thiserror", diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 150f01c84e..928727a76c 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -121,12 +121,16 @@ impl Config { self.client.testnet_mode = testnet_mode; } - pub fn with_gateway_id>(&mut self, id: S) { - self.client.gateway_id = id.into(); + pub fn with_gateway_endpoint>(&mut self, id: S, owner: S, listener: S) { + self.client.gateway_endpoint = GatewayEndpoint { + gateway_id: id.into(), + gateway_owner: owner.into(), + gateway_listener: listener.into(), + }; } - pub fn with_gateway_listener>(&mut self, gateway_listener: S) { - self.client.gateway_listener = gateway_listener.into(); + pub fn with_gateway_id>(&mut self, id: S) { + self.client.gateway_endpoint.gateway_id = id.into(); } #[cfg(not(feature = "coconut"))] @@ -198,11 +202,15 @@ impl Config { } pub fn get_gateway_id(&self) -> String { - self.client.gateway_id.clone() + self.client.gateway_endpoint.gateway_id.clone() + } + + pub fn get_gateway_owner(&self) -> String { + self.client.gateway_endpoint.gateway_owner.clone() } pub fn get_gateway_listener(&self) -> String { - self.client.gateway_listener.clone() + self.client.gateway_endpoint.gateway_listener.clone() } #[cfg(not(feature = "coconut"))] @@ -272,6 +280,19 @@ impl Default for Config { } } +#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +struct GatewayEndpoint { + /// gateway_id specifies ID of the gateway to which the client should send messages. + /// If initially omitted, a random gateway will be chosen from the available topology. + gateway_id: String, + + /// Address of the gateway owner to which the client should send messages. + gateway_owner: String, + + /// Address of the gateway listener to which all client requests should be sent. + gateway_listener: String, +} + #[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct Client { /// Version of the client for which this configuration was created. @@ -313,12 +334,8 @@ pub struct Client { /// sent but not received back. reply_encryption_key_store_path: PathBuf, - /// gateway_id specifies ID of the gateway to which the client should send messages. - /// If initially omitted, a random gateway will be chosen from the available topology. - gateway_id: String, - - /// Address of the gateway listener to which all client requests should be sent. - gateway_listener: String, + /// Information regarding how the client should send data to gateway. + gateway_endpoint: GatewayEndpoint, /// Path to directory containing public/private keys used for bandwidth token purchase. /// Those are saved in case of emergency, to be able to reclaim bandwidth tokens. @@ -357,8 +374,7 @@ impl Default for Client { gateway_shared_key_file: Default::default(), ack_key_file: Default::default(), reply_encryption_key_store_path: Default::default(), - gateway_id: "".to_string(), - gateway_listener: "".to_string(), + gateway_endpoint: Default::default(), #[cfg(not(feature = "coconut"))] backup_bandwidth_token_keys_dir: Default::default(), #[cfg(not(feature = "coconut"))] diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index aebc3ef130..afa215577c 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -59,12 +59,6 @@ eth_endpoint = '{{ client.eth_endpoint }}' ##### additional client config options ##### -# ID of the gateway from which the client should be fetching messages. -gateway_id = '{{ client.gateway_id }}' - -# Address of the gateway listener to which all client requests should be sent. -gateway_listener = '{{ client.gateway_listener }}' - # A gateway specific, optional, base58 stringified shared key used for # communication with particular gateway. gateway_shared_key_file = '{{ client.gateway_shared_key_file }}' @@ -78,6 +72,17 @@ ack_key_file = '{{ client.ack_key_file }}' # Absolute path to the home Nym Clients directory. nym_root_directory = '{{ client.nym_root_directory }}' +[client.gateway_endpoint] +# ID of the gateway from which the client should be fetching messages. +gateway_id = '{{ client.gateway_endpoint.gateway_id }}' + +# Address of the gateway owner to which the client should send messages. +gateway_owner = '{{ client.gateway_endpoint.gateway_owner }}' + +# Address of the gateway listener to which all client requests should be sent. +gateway_listener = '{{ client.gateway_endpoint.gateway_listener }}' + + ##### socket config options ##### diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 4e95739871..d44339d324 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -160,6 +160,10 @@ impl NymClient { if gateway_id.is_empty() { panic!("The identity of the gateway is unknown - did you run `nym-client` init?") } + let gateway_owner = self.config.get_base().get_gateway_owner(); + if gateway_owner.is_empty() { + panic!("The owner of the gateway is unknown - did you run `nym-client` init?") + } let gateway_address = self.config.get_base().get_gateway_listener(); if gateway_address.is_empty() { panic!("The address of the gateway is unknown - did you run `nym-client` init?") @@ -185,6 +189,7 @@ impl NymClient { gateway_address, self.key_manager.identity_keypair(), gateway_identity, + gateway_owner, Some(self.key_manager.gateway_shared_key()), mixnet_message_sender, ack_sender, diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 8f3a5e85df..d6cbc09ba4 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -136,6 +136,7 @@ async fn register_with_gateway( let mut gateway_client = GatewayClient::new_init( gateway.clients_address(), gateway.identity_key, + gateway.owner.clone(), our_identity.clone(), timeout, ); @@ -255,15 +256,14 @@ pub async fn execute(matches: ArgMatches<'static>) { chosen_gateway_id, ) .await; - config - .get_base_mut() - .with_gateway_id(gateway_details.identity_key.to_base58_string()); let shared_keys = register_with_gateway(&gateway_details, key_manager.identity_keypair()).await; - config - .get_base_mut() - .with_gateway_listener(gateway_details.clients_address()); + config.get_base_mut().with_gateway_endpoint( + gateway_details.identity_key.to_base58_string(), + gateway_details.owner.clone(), + gateway_details.clients_address(), + ); key_manager.insert_gateway_shared_key(shared_keys); let pathfinder = ClientKeyPathfinder::new_from_config(config.get_base()); diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index fffcf47e96..c4d0f07eaa 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -58,7 +58,7 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> C #[cfg(not(feature = "coconut"))] if let Some(eth_endpoint) = matches.value_of(ETH_ENDPOINT_ARG_NAME) { config.get_base_mut().with_eth_endpoint(eth_endpoint); - } else { + } else if !cfg!(feature = "eth") { config .get_base_mut() .with_eth_endpoint(DEFAULT_ETH_ENDPOINT); @@ -66,7 +66,7 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> C #[cfg(not(feature = "coconut"))] if let Some(eth_private_key) = matches.value_of(ETH_PRIVATE_KEY_ARG_NAME) { config.get_base_mut().with_eth_private_key(eth_private_key); - } else { + } else if !cfg!(feature = "eth") { config .get_base_mut() .with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY); diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index 1b38206d5b..590fcc47c5 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -59,12 +59,6 @@ eth_endpoint = '{{ client.eth_endpoint }}' ##### additional client config options ##### -# ID of the gateway from which the client should be fetching messages. -gateway_id = '{{ client.gateway_id }}' - -# Address of the gateway listener to which all client requests should be sent. -gateway_listener = '{{ client.gateway_listener }}' - # A gateway specific, optional, base58 stringified shared key used for # communication with particular gateway. gateway_shared_key_file = '{{ client.gateway_shared_key_file }}' @@ -72,12 +66,22 @@ gateway_shared_key_file = '{{ client.gateway_shared_key_file }}' # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. ack_key_file = '{{ client.ack_key_file }}' - + ##### advanced configuration options ##### # Absolute path to the home Nym Clients directory. nym_root_directory = '{{ client.nym_root_directory }}' +[client.gateway_endpoint] +# ID of the gateway from which the client should be fetching messages. +gateway_id = '{{ client.gateway_endpoint.gateway_id }}' + +# Address of the gateway owner to which the client should send messages. +gateway_owner = '{{ client.gateway_endpoint.gateway_owner }}' + +# Address of the gateway listener to which all client requests should be sent. +gateway_listener = '{{ client.gateway_endpoint.gateway_listener }}' + ##### socket config options ##### diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 95291ace58..00cad6e0d0 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -148,6 +148,10 @@ impl NymClient { if gateway_id.is_empty() { panic!("The identity of the gateway is unknown - did you run `nym-client` init?") } + let gateway_owner = self.config.get_base().get_gateway_owner(); + if gateway_owner.is_empty() { + panic!("The owner of the gateway is unknown - did you run `nym-client` init?") + } let gateway_address = self.config.get_base().get_gateway_listener(); if gateway_address.is_empty() { panic!("The address of the gateway is unknown - did you run `nym-client` init?") @@ -173,6 +177,7 @@ impl NymClient { gateway_address, self.key_manager.identity_keypair(), gateway_identity, + gateway_owner, Some(self.key_manager.gateway_shared_key()), mixnet_message_sender, ack_sender, diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index c59f3f60c7..09254d7583 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -136,6 +136,7 @@ async fn register_with_gateway( let mut gateway_client = GatewayClient::new_init( gateway.clients_address(), gateway.identity_key, + gateway.owner.clone(), our_identity.clone(), timeout, ); @@ -256,15 +257,14 @@ pub async fn execute(matches: ArgMatches<'static>) { chosen_gateway_id, ) .await; - config - .get_base_mut() - .with_gateway_id(gateway_details.identity_key.to_base58_string()); let shared_keys = register_with_gateway(&gateway_details, key_manager.identity_keypair()).await; - config - .get_base_mut() - .with_gateway_listener(gateway_details.clients_address()); + config.get_base_mut().with_gateway_endpoint( + gateway_details.identity_key.to_base58_string(), + gateway_details.owner.clone(), + gateway_details.clients_address(), + ); key_manager.insert_gateway_shared_key(shared_keys); let pathfinder = ClientKeyPathfinder::new_from_config(config.get_base()); diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 29a2bcb76e..66813410ef 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -54,7 +54,7 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> C #[cfg(not(feature = "coconut"))] if let Some(eth_endpoint) = matches.value_of(ETH_ENDPOINT_ARG_NAME) { config.get_base_mut().with_eth_endpoint(eth_endpoint); - } else { + } else if !cfg!(feature = "eth") { config .get_base_mut() .with_eth_endpoint(DEFAULT_ETH_ENDPOINT); @@ -62,7 +62,7 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> C #[cfg(not(feature = "coconut"))] if let Some(eth_private_key) = matches.value_of(ETH_PRIVATE_KEY_ARG_NAME) { config.get_base_mut().with_eth_private_key(eth_private_key); - } else { + } else if !cfg!(feature = "eth") { config .get_base_mut() .with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY); diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 4bb0e4263b..600583f10f 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -127,6 +127,7 @@ impl NymClient { gateway.clients_address(), Arc::clone(&client.identity), gateway.identity_key, + gateway.owner.clone(), None, mixnet_messages_sender, ack_sender, diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 5f0b0730c2..a48944f975 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -23,6 +23,7 @@ credentials = { path = "../../credentials" } crypto = { path = "../../crypto" } gateway-requests = { path = "../../../gateway/gateway-requests" } nymsphinx = { path = "../../nymsphinx" } +pemstore = { path = "../../pemstore" } coconut-interface = { path = "../../coconut-interface", optional = true } network-defaults = { path = "../../network-defaults" } diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 5248b1de33..baf5db93e6 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -16,22 +16,27 @@ use crypto::asymmetric::identity::PublicKey; use network_defaults::BANDWIDTH_VALUE; #[cfg(not(feature = "coconut"))] use network_defaults::{ - eth_contract::ETH_JSON_ABI, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, ETH_MIN_BLOCK_DEPTH, - TOKENS_TO_BURN, + eth_contract::ETH_ERC20_JSON_ABI, eth_contract::ETH_JSON_ABI, ETH_BURN_FUNCTION_NAME, + ETH_CONTRACT_ADDRESS, ETH_ERC20_APPROVE_FUNCTION_NAME, ETH_ERC20_CONTRACT_ADDRESS, + ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN, UTOKENS_TO_BURN, }; #[cfg(not(feature = "coconut"))] +use pemstore::traits::PemStorableKeyPair; +#[cfg(not(feature = "coconut"))] use rand::rngs::OsRng; #[cfg(not(feature = "coconut"))] use secp256k1::SecretKey; #[cfg(not(feature = "coconut"))] -use std::io::Write; +use std::io::{Read, Write}; #[cfg(not(feature = "coconut"))] use std::str::FromStr; #[cfg(not(feature = "coconut"))] use web3::{ contract::{Contract, Options}, + ethabi::Token, + signing::{Key, SecretKeyRef}, transports::Http, - types::{Address, Bytes, U256, U64}, + types::{Address, U256, U64}, Web3, }; @@ -50,6 +55,19 @@ pub fn eth_contract(web3: Web3) -> Contract { .expect("Invalid json abi") } +#[cfg(not(feature = "coconut"))] +pub fn eth_erc20_contract(web3: Web3) -> Contract { + Contract::from_json( + web3.eth(), + Address::from(ETH_ERC20_CONTRACT_ADDRESS), + json::parse(ETH_ERC20_JSON_ABI) + .expect("Invalid json abi") + .dump() + .as_bytes(), + ) + .expect("Invalid json abi") +} + #[derive(Clone)] pub struct BandwidthController { #[cfg(feature = "coconut")] @@ -59,6 +77,8 @@ pub struct BandwidthController { #[cfg(not(feature = "coconut"))] contract: Contract, #[cfg(not(feature = "coconut"))] + erc20_contract: Contract, + #[cfg(not(feature = "coconut"))] eth_private_key: SecretKey, #[cfg(not(feature = "coconut"))] backup_bandwidth_token_keys_dir: std::path::PathBuf, @@ -84,12 +104,14 @@ impl BandwidthController { Http::new(ð_endpoint).map_err(|_| GatewayClientError::InvalidURL(eth_endpoint))?; let web3 = web3::Web3::new(transport); // Fail early, on invalid abi - let contract = eth_contract(web3); + let contract = eth_contract(web3.clone()); + let erc20_contract = eth_erc20_contract(web3); let eth_private_key = secp256k1::SecretKey::from_str(ð_private_key) .map_err(|_| GatewayClientError::InvalidEthereumPrivateKey)?; Ok(BandwidthController { contract, + erc20_contract, eth_private_key, backup_bandwidth_token_keys_dir, }) @@ -107,6 +129,45 @@ impl BandwidthController { Ok(()) } + #[cfg(not(feature = "coconut"))] + fn restore_keypair(&self) -> Result { + std::fs::create_dir_all(&self.backup_bandwidth_token_keys_dir)?; + let file = std::fs::read_dir(&self.backup_bandwidth_token_keys_dir)? + .find(|entry| { + entry + .as_ref() + .map(|entry| entry.path().is_file()) + .unwrap_or(false) + }) + .unwrap_or_else(|| Err(std::io::Error::from(std::io::ErrorKind::NotFound)))?; + let file_path = file.path(); + let pub_key = file_path + .file_name() + .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))? + .to_str() + .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?; + let mut priv_key = vec![]; + std::fs::File::open(file_path.clone())?.read_to_end(&mut priv_key)?; + Ok(identity::KeyPair::from_keys( + identity::PrivateKey::from_bytes(&priv_key).unwrap(), + identity::PublicKey::from_base58_string(pub_key).unwrap(), + )) + } + + #[cfg(not(feature = "coconut"))] + fn mark_keypair_as_spent(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> { + let mut spent_dir = self.backup_bandwidth_token_keys_dir.clone(); + spent_dir.push("spent"); + std::fs::create_dir_all(&spent_dir)?; + let file_path_old = self + .backup_bandwidth_token_keys_dir + .join(keypair.public_key().to_base58_string()); + let file_path_new = spent_dir.join(keypair.public_key().to_base58_string()); + std::fs::rename(file_path_old, file_path_new)?; + + Ok(()) + } + #[cfg(feature = "coconut")] pub async fn prepare_coconut_credential( &self, @@ -145,17 +206,25 @@ impl BandwidthController { pub async fn prepare_token_credential( &self, gateway_identity: PublicKey, + gateway_owner: String, ) -> Result { - let mut rng = OsRng; - - let kp = identity::KeyPair::new(&mut rng); - self.backup_keypair(&kp)?; + let kp = match self.restore_keypair() { + Ok(kp) => kp, + Err(_) => { + let mut rng = OsRng; + let kp = identity::KeyPair::new(&mut rng); + self.backup_keypair(&kp)?; + kp + } + }; let verification_key = *kp.public_key(); let signed_verification_key = kp.private_key().sign(&verification_key.to_bytes()); - self.buy_token_credential(verification_key, signed_verification_key) + self.buy_token_credential(verification_key, signed_verification_key, gateway_owner) .await?; + self.mark_keypair_as_spent(&kp)?; + let message: Vec = verification_key .to_bytes() .iter() @@ -177,28 +246,109 @@ impl BandwidthController { &self, verification_key: PublicKey, signed_verification_key: identity::Signature, + gateway_owner: String, ) -> Result<(), GatewayClientError> { - // 0 means a transaction failure, 1 means success let confirmations = if cfg!(debug_assertions) { 1 } else { ETH_MIN_BLOCK_DEPTH }; - // 15 seconds per confirmation block + 10 seconds of network overhead + // 15 seconds per confirmation block + 10 seconds of network overhead + 20 seconds of wait for kill log::info!( "Waiting for Ethereum transaction. This should take about {} seconds", - confirmations * 15 + 10 + (confirmations + 1) * 15 + 30 ); + let mut options = Options::default(); + let estimation = self + .erc20_contract + .estimate_gas( + ETH_ERC20_APPROVE_FUNCTION_NAME, + ( + Token::Address(Address::from(ETH_CONTRACT_ADDRESS)), + Token::Uint(U256::from(UTOKENS_TO_BURN)), + ), + SecretKeyRef::from(&self.eth_private_key).address(), + options.clone(), + ) + .await?; + options.gas = Some(estimation); + log::info!("Calling ERC20 approve in 10 seconds with an estimated gas of {}. Kill the process if you want to abort", estimation); + #[cfg(not(target_arch = "wasm32"))] + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + #[cfg(target_arch = "wasm32")] + if let Err(err) = fluvio_wasm_timer::Delay::new(std::time::Duration::from_secs(10)).await { + log::error!( + "the timer has gone away while waiting for possible kill! - {}", + err + ); + } + let recipt = self + .erc20_contract + .signed_call_with_confirmations( + ETH_ERC20_APPROVE_FUNCTION_NAME, + ( + Token::Address(Address::from(ETH_CONTRACT_ADDRESS)), + Token::Uint(U256::from(UTOKENS_TO_BURN)), + ), + options, + 1, // One confirmation is enough, as we'll be consuming the approved token next anyway + &self.eth_private_key, + ) + .await?; + if Some(U64::from(0u64)) == recipt.status { + return Err(GatewayClientError::BurnTokenError( + web3::Error::InvalidResponse(format!( + "Approve transaction status is 0 (failure): {:?}", + recipt.logs, + )), + )); + } else { + log::info!( + "Approved {} tokens for bandwidth use on Ethereum", + TOKENS_TO_BURN + ); + } + + let mut options = Options::default(); + let estimation = self + .contract + .estimate_gas( + ETH_BURN_FUNCTION_NAME, + ( + Token::Uint(U256::from(UTOKENS_TO_BURN)), + Token::Uint(U256::from(&verification_key.to_bytes())), + Token::Bytes(signed_verification_key.to_bytes().to_vec()), + Token::String(gateway_owner.clone()), + ), + SecretKeyRef::from(&self.eth_private_key).address(), + options.clone(), + ) + .await?; + options.gas = Some(estimation); + log::info!("Generating bandwidth on ETH contract in 10 seconds with an estimated gas of {}. \ + Kill the process if you want to abort. Keep in mind that if you abort now, you'll still have \ + some tokens approved for bandwidth spending from the previous action. \ + If you don't want that, you'll need to manually decreaseAllowance to revert the approval.", estimation); + #[cfg(not(target_arch = "wasm32"))] + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + #[cfg(target_arch = "wasm32")] + if let Err(err) = fluvio_wasm_timer::Delay::new(std::time::Duration::from_secs(10)).await { + log::error!( + "the timer has gone away while waiting for possible kill! - {}", + err + ); + } let recipt = self .contract .signed_call_with_confirmations( ETH_BURN_FUNCTION_NAME, ( - U256::from(TOKENS_TO_BURN), - U256::from(&verification_key.to_bytes()), - Bytes(signed_verification_key.to_bytes().to_vec()), + Token::Uint(U256::from(UTOKENS_TO_BURN)), + Token::Uint(U256::from(&verification_key.to_bytes())), + Token::Bytes(signed_verification_key.to_bytes().to_vec()), + Token::String(gateway_owner), ), - Options::default(), + options, confirmations, &self.eth_private_key, ) @@ -236,6 +386,15 @@ mod tests { eth_contract(web3); } + #[test] + fn parse_erc20_contract() { + let transport = + Http::new("https://rinkeby.infura.io/v3/00000000000000000000000000000000").unwrap(); + let web3 = web3::Web3::new(transport); + // test no panic occurs + eth_erc20_contract(web3); + } + #[test] fn check_event_name_constant_against_abi() { let transport = diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 19c17da8a5..c36c1c5979 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -20,6 +20,7 @@ use gateway_requests::iv::IV; use gateway_requests::registration::handshake::{client_handshake, SharedKeys}; use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; use log::*; +use network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN}; use nymsphinx::forwarding::packet::MixPacket; use rand::rngs::OsRng; use std::convert::TryFrom; @@ -44,6 +45,7 @@ pub struct GatewayClient { bandwidth_remaining: i64, gateway_address: String, gateway_identity: identity::PublicKey, + gateway_owner: String, local_identity: Arc, shared_key: Option>, connection: SocketState, @@ -68,6 +70,7 @@ impl GatewayClient { gateway_address: String, local_identity: Arc, gateway_identity: identity::PublicKey, + gateway_owner: String, shared_key: Option>, mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, @@ -80,6 +83,7 @@ impl GatewayClient { bandwidth_remaining: 0, gateway_address, gateway_identity, + gateway_owner, local_identity, shared_key, connection: SocketState::NotConnected, @@ -112,6 +116,7 @@ impl GatewayClient { pub fn new_init( gateway_address: String, gateway_identity: identity::PublicKey, + gateway_owner: String, local_identity: Arc, response_timeout_duration: Duration, ) -> Self { @@ -129,6 +134,7 @@ impl GatewayClient { bandwidth_remaining: 0, gateway_address, gateway_identity, + gateway_owner, local_identity, shared_key: None, connection: SocketState::NotConnected, @@ -548,6 +554,8 @@ impl GatewayClient { return self.try_claim_testnet_bandwidth().await; } + let _gateway_owner = self.gateway_owner.clone(); + #[cfg(feature = "coconut")] let credential = self .bandwidth_controller @@ -560,7 +568,7 @@ impl GatewayClient { .bandwidth_controller .as_ref() .unwrap() - .prepare_token_credential(self.gateway_identity) + .prepare_token_credential(self.gateway_identity, _gateway_owner) .await?; #[cfg(feature = "coconut")] @@ -584,15 +592,10 @@ impl GatewayClient { return Err(GatewayClientError::NotAuthenticated); } if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining { - // Try to claim more bandwidth first, and return an error only if that is still not - // enough (the current granularity for bandwidth should be sufficient) - self.claim_bandwidth().await?; - if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - self.estimate_required_bandwidth(&packets), - self.bandwidth_remaining, - )); - } + return Err(GatewayClientError::NotEnoughBandwidth( + self.estimate_required_bandwidth(&packets), + self.bandwidth_remaining, + )); } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -659,15 +662,10 @@ impl GatewayClient { return Err(GatewayClientError::NotAuthenticated); } if (mix_packet.sphinx_packet().len() as i64) > self.bandwidth_remaining { - // Try to claim more bandwidth first, and return an error only if that is still not - // enough - self.claim_bandwidth().await?; - if (mix_packet.sphinx_packet().len() as i64) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - mix_packet.sphinx_packet().len() as i64, - self.bandwidth_remaining, - )); - } + return Err(GatewayClientError::NotEnoughBandwidth( + mix_packet.sphinx_packet().len() as i64, + self.bandwidth_remaining, + )); } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -737,6 +735,12 @@ impl GatewayClient { } let shared_key = self.perform_initial_authentication().await?; + if self.bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { + info!("Claiming more bandwidth for your tokens. This will use {} token(s) from your wallet. \ + Stop the process now if you don't want that to happen.", TOKENS_TO_BURN); + self.claim_bandwidth().await?; + } + // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index f396b3e06b..c45e27002a 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -8,7 +8,7 @@ use tungstenite::Error as WsError; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsValue; #[cfg(not(feature = "coconut"))] -use web3::Error as Web3Error; +use web3::{contract::Error as Web3ContractError, Error as Web3Error}; #[derive(Debug, Error)] pub enum GatewayClientError { @@ -27,13 +27,17 @@ pub enum GatewayClientError { NetworkErrorWasm(JsValue), #[cfg(not(feature = "coconut"))] - #[error("Could not backup keypair - {0}")] + #[error("Keypair IO error - {0}")] IOError(#[from] std::io::Error), #[cfg(not(feature = "coconut"))] #[error("Could not burn ERC20 token in Ethereum smart contract - {0}")] BurnTokenError(#[from] Web3Error), + #[cfg(not(feature = "coconut"))] + #[error("Could not run web3 contract - {0}")] + Web3ContractError(#[from] Web3ContractError), + #[cfg(not(feature = "coconut"))] #[error("Invalid Ethereum private key")] InvalidEthereumPrivateKey, diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index c291f22eb1..a399085f04 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -35,6 +35,7 @@ pub struct Config { nymd_url: Url, mixnet_contract_address: Option, vesting_contract_address: Option, + erc20_bridge_contract_address: Option, mixnode_page_limit: Option, gateway_page_limit: Option, @@ -50,12 +51,14 @@ impl Config { api_url: Url, mixnet_contract_address: Option, vesting_contract_address: Option, + erc20_bridge_contract_address: Option, ) -> Self { Config { network, nymd_url, mixnet_contract_address, vesting_contract_address, + erc20_bridge_contract_address, api_url, mixnode_page_limit: None, gateway_page_limit: None, @@ -90,6 +93,7 @@ pub struct Client { network: network_defaults::all::Network, mixnet_contract_address: Option, vesting_contract_address: Option, + erc20_bridge_contract_address: Option, mnemonic: Option, mixnode_page_limit: Option, @@ -114,6 +118,7 @@ impl Client { config.nymd_url.as_str(), config.mixnet_contract_address.clone(), config.vesting_contract_address.clone(), + config.erc20_bridge_contract_address.clone(), mnemonic.clone(), None, )?; @@ -122,6 +127,7 @@ impl Client { network: config.network, mixnet_contract_address: config.mixnet_contract_address, vesting_contract_address: config.vesting_contract_address, + erc20_bridge_contract_address: config.erc20_bridge_contract_address, mnemonic: Some(mnemonic), mixnode_page_limit: config.mixnode_page_limit, gateway_page_limit: config.gateway_page_limit, @@ -138,6 +144,7 @@ impl Client { new_endpoint.as_ref(), self.mixnet_contract_address.clone(), self.vesting_contract_address.clone(), + self.erc20_bridge_contract_address.clone(), self.mnemonic.clone().unwrap(), None, )?; @@ -159,12 +166,24 @@ impl Client { cosmrs::AccountId::from_str(network_defaults::DEFAULT_VESTING_CONTRACT_ADDRESS) .unwrap() })), + Some( + config + .erc20_bridge_contract_address + .clone() + .unwrap_or_else(|| { + cosmrs::AccountId::from_str( + network_defaults::DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ) + .unwrap() + }), + ), )?; Ok(Client { network: config.network, mixnet_contract_address: config.mixnet_contract_address, vesting_contract_address: config.vesting_contract_address, + erc20_bridge_contract_address: config.erc20_bridge_contract_address, mnemonic: None, mixnode_page_limit: config.mixnode_page_limit, gateway_page_limit: config.gateway_page_limit, @@ -180,6 +199,7 @@ impl Client { new_endpoint.as_ref(), self.mixnet_contract_address.clone(), self.vesting_contract_address.clone(), + self.erc20_bridge_contract_address.clone(), )?; Ok(()) } diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index dec4edcbc7..9ae0fd863e 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -52,6 +52,7 @@ pub struct NymdClient { client: C, mixnet_contract_address: Option, vesting_contract_address: Option, + erc20_bridge_contract_address: Option, client_address: Option>, custom_gas_limits: HashMap, simulated_gas_multiplier: f32, @@ -62,6 +63,7 @@ impl NymdClient { endpoint: U, mixnet_contract_address: Option, vesting_contract_address: Option, + erc20_bridge_contract_address: Option, ) -> Result, NymdError> where U: TryInto, @@ -70,6 +72,7 @@ impl NymdClient { client: QueryNymdClient::new(endpoint)?, mixnet_contract_address, vesting_contract_address, + erc20_bridge_contract_address, client_address: None, custom_gas_limits: Default::default(), simulated_gas_multiplier: DEFAULT_SIMULATED_GAS_MULTIPLIER, @@ -83,6 +86,7 @@ impl NymdClient { endpoint: U, mixnet_contract_address: Option, vesting_contract_address: Option, + erc20_bridge_contract_address: Option, signer: DirectSecp256k1HdWallet, gas_price: Option, ) -> Result, NymdError> @@ -99,6 +103,7 @@ impl NymdClient { client: SigningNymdClient::connect_with_signer(endpoint, signer, gas_price)?, mixnet_contract_address, vesting_contract_address, + erc20_bridge_contract_address, client_address: Some(client_address), custom_gas_limits: Default::default(), simulated_gas_multiplier: DEFAULT_SIMULATED_GAS_MULTIPLIER, @@ -110,6 +115,7 @@ impl NymdClient { endpoint: U, mixnet_contract_address: Option, vesting_contract_address: Option, + erc20_bridge_contract_address: Option, mnemonic: bip39::Mnemonic, gas_price: Option, ) -> Result, NymdError> @@ -128,6 +134,7 @@ impl NymdClient { client: SigningNymdClient::connect_with_signer(endpoint, wallet, gas_price)?, mixnet_contract_address, vesting_contract_address, + erc20_bridge_contract_address, client_address: Some(client_address), custom_gas_limits: Default::default(), simulated_gas_multiplier: DEFAULT_SIMULATED_GAS_MULTIPLIER, @@ -148,6 +155,12 @@ impl NymdClient { .ok_or(NymdError::NoContractAddressAvailable) } + pub fn erc20_bridge_contract_address(&self) -> Result<&AccountId, NymdError> { + self.erc20_bridge_contract_address + .as_ref() + .ok_or(NymdError::NoContractAddressAvailable) + } + // now the question is as follows: will denom always be in the format of `u{prefix}`? pub fn denom(&self) -> Result { Ok(format!("u{}", self.mixnet_contract_address()?.prefix()) diff --git a/common/network-defaults/src/eth_contract.rs b/common/network-defaults/src/eth_contract.rs index b9d4e5fddd..53b9190caa 100644 --- a/common/network-defaults/src/eth_contract.rs +++ b/common/network-defaults/src/eth_contract.rs @@ -4,129 +4,546 @@ // This should be modified whenever an updated Ethereum contract is uploaded pub const ETH_JSON_ABI: &str = r#" [ - { - "inputs": [ - { - "internalType": "contract ERC20Burnable", - "name": "_erc20", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "Bandwidth", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "VerificationKey", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "SignedVerificationKey", - "type": "bytes" - } - ], - "name": "Burned", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "verificationKey", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "signedVerificationKey", - "type": "bytes" - } - ], - "name": "burnTokenForAccessCode", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "erc20", - "outputs": [ - { - "internalType": "contract ERC20Burnable", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] + { + "inputs": [ + { + "internalType": "contract CosmosERC20", + "name": "_erc20", + "type": "address" + }, + { + "internalType": "contract Gravity", + "name": "_gravityBridge", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "Bandwidth", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "VerificationKey", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "SignedVerificationKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "string", + "name": "CosmosRecipient", + "type": "string" + } + ], + "name": "BBCredentialPurchased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "Enabled", + "type": "bool" + } + ], + "name": "CredentialGenerationSwitch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "NewBytesPerToken", + "type": "uint256" + } + ], + "name": "RatioChanged", + "type": "event" + }, + { + "inputs": [], + "name": "BytesPerToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "bandwidthFromToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newBytesPerTokenAmount", + "type": "uint256" + } + ], + "name": "changeRatio", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "credentialGenerationEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_generation", + "type": "bool" + } + ], + "name": "credentialGenerationSwitch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "erc20", + "outputs": [ + { + "internalType": "contract CosmosERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_verificationKey", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_signedVerificationKey", + "type": "bytes" + }, + { + "internalType": "string", + "name": "_cosmosRecipient", + "type": "string" + } + ], + "name": "generateBasicBandwidthCredential", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "gravityBridge", + "outputs": [ + { + "internalType": "contract Gravity", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + "#; + +pub const ETH_ERC20_JSON_ABI: &str = r#" +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ] "#; diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index b2e5c3a514..acdd02e011 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -17,6 +17,8 @@ cfg_if::cfg_if! { pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = mainnet::MIXNET_CONTRACT_ADDRESS; pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = mainnet::VESTING_CONTRACT_ADDRESS; pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; + pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS; + pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS; pub const DEFAULT_REWARDING_VALIDATOR_ADDRESS: &str = mainnet::REWARDING_VALIDATOR_ADDRESS; pub fn default_validators() -> Vec { @@ -33,6 +35,8 @@ cfg_if::cfg_if! { pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = qa::MIXNET_CONTRACT_ADDRESS; pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = qa::VESTING_CONTRACT_ADDRESS; pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; + pub const ETH_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_CONTRACT_ADDRESS; + pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_ERC20_CONTRACT_ADDRESS; pub const DEFAULT_REWARDING_VALIDATOR: &str = qa::REWARDING_VALIDATOR_ADDRESS; pub fn default_validators() -> Vec { @@ -49,6 +53,8 @@ cfg_if::cfg_if! { pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = sandbox::MIXNET_CONTRACT_ADDRESS; pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = sandbox::VESTING_CONTRACT_ADDRESS; pub const DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = sandbox::BANDWIDTH_CLAIM_CONTRACT_ADDRESS; + pub const ETH_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_CONTRACT_ADDRESS; + pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_ERC20_CONTRACT_ADDRESS; pub const DEFAULT_REWARDING_VALIDATOR: &str = sandbox::REWARDING_VALIDATOR_ADDRESS; pub fn default_validators() -> Vec { @@ -106,18 +112,22 @@ pub fn default_api_endpoints() -> Vec { .collect() } -pub const ETH_CONTRACT_ADDRESS: [u8; 20] = - hex_literal::hex!("9fEE3e28c17dbB87310A51F13C4fbf4331A6f102"); // Name of the event triggered by the eth contract. If the event name is changed, // this would also need to be changed; It is currently tested against the json abi -pub const ETH_EVENT_NAME: &str = "Burned"; -pub const ETH_BURN_FUNCTION_NAME: &str = "burnTokenForAccessCode"; +pub const ETH_EVENT_NAME: &str = "BBCredentialPurchased"; +pub const ETH_BURN_FUNCTION_NAME: &str = "generateBasicBandwidthCredential"; +pub const ETH_ERC20_APPROVE_FUNCTION_NAME: &str = "approve"; // Ethereum constants used for token bridge /// How much bandwidth (in bytes) one token can buy const BYTES_PER_TOKEN: u64 = 1024 * 1024 * 1024; + +/// Threshold for claiming more bandwidth: 1 MB +pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024; /// How many ERC20 tokens should be burned to buy bandwidth -pub const TOKENS_TO_BURN: u64 = 10; +pub const TOKENS_TO_BURN: u64 = 1; +/// How many ERC20 utokens should be burned to buy bandwidth +pub const UTOKENS_TO_BURN: u64 = TOKENS_TO_BURN * 1000000; /// Default bandwidth (in bytes) that we try to buy pub const BANDWIDTH_VALUE: u64 = TOKENS_TO_BURN * BYTES_PER_TOKEN; diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index ebb30bc082..87e9c1e562 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -10,6 +10,10 @@ pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000000"); +pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n17zujduc46wvkwvp6f062mm5xhr7jc3fewvqu9e"; pub(crate) fn validators() -> Vec { diff --git a/common/network-defaults/src/qa.rs b/common/network-defaults/src/qa.rs index f853f4bba3..ea004fc221 100644 --- a/common/network-defaults/src/qa.rs +++ b/common/network-defaults/src/qa.rs @@ -10,6 +10,10 @@ pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt17x6pt4msccvawgxjeg5nmnygt pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt1t4dmskxea0avvrj8xtmu66hv7dkyg9s8059t3c"; pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; +pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000000"); +pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1dn52nx8wv9wkqmrvj6tcmdzh4es6jt8tr7f6j9"; pub(crate) fn validators() -> Vec { diff --git a/common/network-defaults/src/sandbox.rs b/common/network-defaults/src/sandbox.rs index 82902b6098..28c25c792b 100644 --- a/common/network-defaults/src/sandbox.rs +++ b/common/network-defaults/src/sandbox.rs @@ -10,6 +10,10 @@ pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2 pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt14ejqjyq8um4p3xfqj74yld5waqljf88fn549lh"; pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; +pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("8e0DcFF7F3085235C32E845f3667aEB3f1e83133"); +pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("E8883BAeF3869e14E4823F46662e81D4F7d2A81F"); pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1jh0s6qu6tuw9ut438836mmn7f3f2wencrnmdj4"; pub(crate) fn validators() -> Vec { diff --git a/explorer-api/src/client.rs b/explorer-api/src/client.rs index 9eb945c2a1..f118a0e40d 100644 --- a/explorer-api/src/client.rs +++ b/explorer-api/src/client.rs @@ -15,6 +15,7 @@ pub(crate) fn new_nymd_client() -> validator_client::Client { api_url, Some(mixnet_contract.parse().unwrap()), None, + None, ); validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!") diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 662d843b17..5fde529264 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -248,16 +248,26 @@ where &self.client.shared_keys, iv, )?; + if !self + .inner + .check_local_identity(&credential.gateway_identity()) + { + return Err(RequestHandlingError::InvalidBandwidthCredential); + } - debug!("Received bandwidth increase request. Verifying signature"); if !credential.verify_signature() { return Err(RequestHandlingError::InvalidBandwidthCredential); } debug!("Verifying Ethereum for token burn..."); - self.inner + let gateway_owner = self + .inner .erc20_bridge .verify_eth_events(credential.verification_key()) .await?; + self.inner + .erc20_bridge + .verify_gateway_owner(gateway_owner, &credential.gateway_identity()) + .await?; debug!("Claim the token on Cosmos, to make sure it's not spent twice..."); self.inner.erc20_bridge.claim_token(&credential).await?; @@ -276,7 +286,6 @@ where self.increase_bandwidth(bandwidth_value as i64).await?; let available_total = self.get_available_bandwidth().await?; - debug!("Increased bandwidth for client: {:?}", self.client.address); Ok(ServerResponse::Bandwidth { available_total }) } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs index 84b0de6510..53fb2f104e 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs @@ -17,14 +17,13 @@ use crate::node::client_handling::websocket::connection_handler::authenticated:: use bandwidth_claim_contract::msg::ExecuteMsg; use bandwidth_claim_contract::payment::LinkPaymentData; use credentials::token::bandwidth::TokenCredential; -use crypto::asymmetric::identity::{PublicKey, Signature}; +use crypto::asymmetric::identity::{PublicKey, Signature, SIGNATURE_LENGTH}; use gateway_client::bandwidth::eth_contract; use network_defaults::{ - DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS, DENOM, ETH_EVENT_NAME, ETH_MIN_BLOCK_DEPTH, -}; -use validator_client::nymd::{ - AccountId, CosmosCoin, Decimal, Denom, NymdClient, SigningNymdClient, + DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS, DEFAULT_MIXNET_CONTRACT_ADDRESS, ETH_EVENT_NAME, + ETH_MIN_BLOCK_DEPTH, }; +use validator_client::nymd::{AccountId, NymdClient, SigningNymdClient}; pub(crate) struct ERC20Bridge { // This is needed because web3's Contract doesn't sufficiently expose it's eth interface @@ -45,8 +44,9 @@ impl ERC20Bridge { let nymd_client = NymdClient::connect_with_mnemonic( config::defaults::default_network(), nymd_url.as_ref(), - AccountId::from_str(DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS).ok(), + AccountId::from_str(DEFAULT_MIXNET_CONTRACT_ADDRESS).ok(), None, + AccountId::from_str(DEFAULT_BANDWIDTH_CLAIM_CONTRACT_ADDRESS).ok(), mnemonic, None, ) @@ -62,7 +62,7 @@ impl ERC20Bridge { pub(crate) async fn verify_eth_events( &self, verification_key: PublicKey, - ) -> Result<(), RequestHandlingError> { + ) -> Result { // It's safe to unwrap here, as we are guarded by a unit test that checks the event // name constant against the contract abi let event = self.contract.abi().event(ETH_EVENT_NAME).unwrap(); @@ -84,7 +84,7 @@ impl ERC20Bridge { .to_block(BlockNumber::Number(check_until)) .build(); // Get only the first event that checks out. If the client burns more tokens with the - // same verification key, those token would be lost + // same verification key, those tokens would be lost for l in self.web3.eth().logs(filter).await? { let log = event.parse_log(web3::ethabi::RawLog { topics: l.topics, @@ -93,23 +93,39 @@ impl ERC20Bridge { let burned_event = Burned::from_tokens(log.params.into_iter().map(|x| x.value).collect::>())?; if burned_event.verify(verification_key) { - return Ok(()); + return Ok(burned_event.cosmos_recipient); } } Err(RequestHandlingError::InvalidBandwidthCredential) } + pub(crate) async fn verify_gateway_owner( + &self, + gateway_owner: String, + gateway_identity: &PublicKey, + ) -> Result<(), RequestHandlingError> { + let owner_address = AccountId::from_str(&gateway_owner) + .map_err(|_| RequestHandlingError::InvalidBandwidthCredential)?; + let gateway_bond = self + .nymd_client + .owns_gateway(&owner_address) + .await? + .ok_or(RequestHandlingError::InvalidBandwidthCredential)?; + if gateway_bond.gateway.identity_key == gateway_identity.to_base58_string() { + Ok(()) + } else { + Err(RequestHandlingError::InvalidBandwidthCredential) + } + } + pub(crate) async fn claim_token( &self, credential: &TokenCredential, ) -> Result<(), RequestHandlingError> { - // It's ok to unwrap here, as the cosmos contract and denom are set correctly - let contract_address = self.nymd_client.mixnet_contract_address().unwrap(); - let coin = CosmosCoin { - denom: Denom::from_str(DENOM).unwrap(), - amount: Decimal::from(100000u64), - }; + // It's ok to unwrap here, as the cosmos contract is set correctly + let erc20_bridge_contract_address = + self.nymd_client.erc20_bridge_contract_address().unwrap(); let req = ExecuteMsg::LinkPayment { data: LinkPaymentData::new( credential.verification_key().to_bytes(), @@ -120,12 +136,11 @@ impl ERC20Bridge { }; self.nymd_client .execute( - // it's ok to unwrap here, as the address is - contract_address, + erc20_bridge_contract_address, &req, Default::default(), "Linking payment", - vec![coin], + vec![], ) .await?; Ok(()) @@ -140,6 +155,8 @@ pub struct Burned { pub verification_key: PublicKey, /// Signed verification key pub signed_verification_key: Signature, + /// Address for the owner of the gateway + pub cosmos_recipient: String, } impl Burned { @@ -159,7 +176,7 @@ impl Detokenize for Burned { where Self: Sized, { - if tokens.len() != 3 { + if tokens.len() != 4 { return Err(Error::InvalidOutputType(format!( "Expected three elements, got: {:?}", tokens @@ -189,20 +206,30 @@ impl Detokenize for Burned { })?; let signed_verification_key = tokens.get(2).unwrap().clone().into_bytes().ok_or_else(|| { - Error::InvalidOutputType(String::from("Expected Bytes for signed_verification_key")) + Error::InvalidOutputType(String::from("Expected Bytes for the last two fields")) })?; let signed_verification_key = - Signature::from_bytes(&signed_verification_key).map_err(|_| { + Signature::from_bytes(&signed_verification_key[..SIGNATURE_LENGTH]).map_err(|_| { Error::InvalidOutputType(format!( - "Expected signature of 64 bytes, got: {}", + "Expected signature of {} bytes, got: {}", + SIGNATURE_LENGTH, signed_verification_key.len() )) })?; + let cosmos_recipient = tokens + .get(3) + .unwrap() + .clone() + .into_string() + .ok_or_else(|| { + Error::InvalidOutputType(String::from("Expected utf8 encoded owner address")) + })?; Ok(Burned { bandwidth, verification_key, signed_verification_key, + cosmos_recipient, }) } } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index fca260adfa..51b2729be9 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -119,6 +119,15 @@ where } } + #[cfg(not(feature = "coconut"))] + /// Check that the local identity matches a given identity. + pub(crate) fn check_local_identity( + &self, + identity: &crypto::asymmetric::identity::PublicKey, + ) -> bool { + self.local_identity.public_key().eq(identity) + } + /// Attempts to perform websocket handshake with the remote and upgrades the raw TCP socket /// to the framed WebSocket. pub(crate) async fn perform_websocket_handshake(&mut self) -> Result<(), WsError> diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 401bf07ab3..b354e4b1aa 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -108,4 +108,17 @@ impl Config { .parse() .ok() } + + pub fn get_bandwidth_claim_contract_address( + &self, + network: Network, + ) -> Option { + self + .base + .networks + .bandwidth_claim_contract_address(network.into()) + .expect("No bandwidth claim contract address found in config") + .parse() + .ok() + } } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 348f351c70..40b05196cd 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -141,6 +141,7 @@ async fn _connect_with_mnemonic( config.get_validator_api_url(network), config.get_mixnet_contract_address(network), config.get_vesting_contract_address(network), + config.get_bandwidth_claim_contract_address(network), ), mnemonic.clone(), ) { diff --git a/validator-api/src/network_monitor/monitor/preparer.rs b/validator-api/src/network_monitor/monitor/preparer.rs index 6c832721f2..52c69096d3 100644 --- a/validator-api/src/network_monitor/monitor/preparer.rs +++ b/validator-api/src/network_monitor/monitor/preparer.rs @@ -387,6 +387,7 @@ impl PacketPreparer { GatewayPackets::new( route.gateway_clients_address(), route.gateway_identity(), + route.gateway_owner(), mix_packets, ) } @@ -474,6 +475,7 @@ impl PacketPreparer { let recipient = self.create_packet_sender(test_route.gateway()); let gateway_identity = test_route.gateway_identity(); let gateway_address = test_route.gateway_clients_address(); + let gateway_owner = test_route.gateway_owner(); // it's actually going to be a tiny bit more due to gateway testing, but it's a good enough approximation let mut mix_packets = Vec::with_capacity(mixes.len() * self.per_node_test_packets); @@ -493,7 +495,9 @@ impl PacketPreparer { let gateway_packets = all_gateway_packets .entry(gateway_identity.to_bytes()) - .or_insert_with(|| GatewayPackets::empty(gateway_address, gateway_identity)); + .or_insert_with(|| { + GatewayPackets::empty(gateway_address, gateway_identity, gateway_owner) + }); gateway_packets.push_packets(mix_packets); // and for each gateway... @@ -502,6 +506,7 @@ impl PacketPreparer { let test_packet = TestPacket::from_gateway(gateway, test_route.id(), test_nonce); let gateway_identity = gateway.identity_key; let gateway_address = gateway.clients_address(); + let gateway_owner = gateway.owner.clone(); let recipient = self.create_packet_sender(gateway); let topology = test_route.substitute_gateway(gateway); // produce n mix packets @@ -516,7 +521,9 @@ impl PacketPreparer { // or create a new one let gateway_packets = all_gateway_packets .entry(gateway_identity.to_bytes()) - .or_insert_with(|| GatewayPackets::empty(gateway_address, gateway_identity)); + .or_insert_with(|| { + GatewayPackets::empty(gateway_address, gateway_identity, gateway_owner) + }); gateway_packets.push_packets(gateway_mix_packets); } } diff --git a/validator-api/src/network_monitor/monitor/sender.rs b/validator-api/src/network_monitor/monitor/sender.rs index 890f4a1943..f650cc4b04 100644 --- a/validator-api/src/network_monitor/monitor/sender.rs +++ b/validator-api/src/network_monitor/monitor/sender.rs @@ -6,6 +6,7 @@ use crate::network_monitor::monitor::gateway_clients_cache::{ }; use crate::network_monitor::monitor::gateways_pinger::GatewayPinger; use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; +use config::defaults::REMAINING_BANDWIDTH_THRESHOLD; use crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; use futures::channel::mpsc; use futures::stream::{self, FuturesUnordered, StreamExt}; @@ -27,9 +28,6 @@ use gateway_client::bandwidth::BandwidthController; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); -// If we're below 10MB of bandwidth, claim some more -const REMAINING_BANDWIDTH_THRESHOLD: i64 = 10 * 1000 * 1000; - pub(crate) struct GatewayPackets { /// Network address of the target gateway if wanted to be accessed by the client. /// It is a websocket address. @@ -38,6 +36,9 @@ pub(crate) struct GatewayPackets { /// Public key of the target gateway. pub(crate) pub_key: identity::PublicKey, + /// The address of the gateway owner. + pub(crate) gateway_owner: String, + /// All the packets that are going to get sent to the gateway. pub(crate) packets: Vec, } @@ -46,19 +47,26 @@ impl GatewayPackets { pub(crate) fn new( clients_address: String, pub_key: identity::PublicKey, + gateway_owner: String, packets: Vec, ) -> Self { GatewayPackets { clients_address, pub_key, + gateway_owner, packets, } } - pub(crate) fn empty(clients_address: String, pub_key: identity::PublicKey) -> Self { + pub(crate) fn empty( + clients_address: String, + pub_key: identity::PublicKey, + gateway_owner: String, + ) -> Self { GatewayPackets { clients_address, pub_key, + gateway_owner, packets: Vec::new(), } } @@ -182,6 +190,7 @@ impl PacketSender { fn new_gateway_client_handle( address: String, identity: identity::PublicKey, + owner: String, fresh_gateway_client_data: &FreshGatewayClientData, ) -> ( GatewayClientHandle, @@ -198,6 +207,7 @@ impl PacketSender { address, Arc::clone(&fresh_gateway_client_data.local_identity), identity, + owner, None, message_sender, ack_sender, @@ -279,6 +289,7 @@ impl PacketSender { async fn create_new_gateway_client_handle_and_authenticate( address: String, identity: identity::PublicKey, + owner: String, fresh_gateway_client_data: &FreshGatewayClientData, gateway_connection_timeout: Duration, ) -> Option<( @@ -286,7 +297,7 @@ impl PacketSender { (MixnetMessageReceiver, AcknowledgementReceiver), )> { let (new_client, (message_receiver, ack_receiver)) = - Self::new_gateway_client_handle(address, identity, fresh_gateway_client_data); + Self::new_gateway_client_handle(address, identity, owner, fresh_gateway_client_data); // Put this in timeout in case the gateway has incorrectly set their ulimit and our connection // gets stuck in their TCP queue and just hangs on our end but does not terminate @@ -329,11 +340,10 @@ impl PacketSender { client: &mut GatewayClient, ) -> Result<(), GatewayClientError> { if client.remaining_bandwidth() < REMAINING_BANDWIDTH_THRESHOLD { - info!( - "Client to gateway {} is running out of bandwidth... Claiming some more...", - client.gateway_identity().to_base58_string() - ); - client.claim_bandwidth().await + Err(GatewayClientError::NotEnoughBandwidth( + REMAINING_BANDWIDTH_THRESHOLD, + client.remaining_bandwidth(), + )) } else { Ok(()) } @@ -364,6 +374,7 @@ impl PacketSender { Self::create_new_gateway_client_handle_and_authenticate( packets.clients_address, packets.pub_key, + packets.gateway_owner, &fresh_gateway_client_data, gateway_connection_timeout, ) diff --git a/validator-api/src/network_monitor/test_route/mod.rs b/validator-api/src/network_monitor/test_route/mod.rs index 827087eb8d..03c64e5429 100644 --- a/validator-api/src/network_monitor/test_route/mod.rs +++ b/validator-api/src/network_monitor/test_route/mod.rs @@ -66,6 +66,10 @@ impl TestRoute { self.gateway().identity_key } + pub(crate) fn gateway_owner(&self) -> String { + self.gateway().owner.clone() + } + pub(crate) fn topology(&self) -> &NymTopology { &self.nodes } diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 27ce8c1980..80e5f9281e 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -44,8 +44,14 @@ impl Client { .parse() .expect("the mixnet contract address is invalid!"); - let client_config = - validator_client::Config::new(network, nymd_url, api_url, Some(mixnet_contract), None); + let client_config = validator_client::Config::new( + network, + nymd_url, + api_url, + Some(mixnet_contract), + None, + None, + ); let inner = validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!"); @@ -72,8 +78,14 @@ impl Client { .parse() .expect("the mnemonic is invalid!"); - let client_config = - validator_client::Config::new(network, nymd_url, api_url, Some(mixnet_contract), None); + let client_config = validator_client::Config::new( + network, + nymd_url, + api_url, + Some(mixnet_contract), + None, + None, + ); let inner = validator_client::Client::new_signing(client_config, mnemonic) .expect("Failed to connect to nymd!");