diff --git a/core/payment/src/api/allocations.rs b/core/payment/src/api/allocations.rs index 4c730be0a6..f2653de28b 100644 --- a/core/payment/src/api/allocations.rs +++ b/core/payment/src/api/allocations.rs @@ -1,4 +1,5 @@ use std::convert::TryInto; +use std::fmt::Display; use std::str::FromStr; use std::time::Duration; // External crates @@ -32,187 +33,244 @@ const DEFAULT_TESTNET_NETWORK: NetworkName = NetworkName::Holesky; const DEFAULT_MAINNET_NETWORK: NetworkName = NetworkName::Polygon; const DEFAULT_PAYMENT_DRIVER: DriverName = DriverName::Erc20; -fn default_payment_platform_testnet() -> String { - format!( - "{}-{}-{}", - DEFAULT_PAYMENT_DRIVER, - DEFAULT_TESTNET_NETWORK, - get_default_token(&DEFAULT_PAYMENT_DRIVER, &DEFAULT_TESTNET_NETWORK) - ) -} - -fn default_payment_platform_mainnet() -> String { - format!( - "{}-{}-{}", - DEFAULT_PAYMENT_DRIVER, - DEFAULT_MAINNET_NETWORK, - get_default_token(&DEFAULT_PAYMENT_DRIVER, &DEFAULT_MAINNET_NETWORK) - ) -} +mod token_name { + use super::*; -pub fn register_endpoints(scope: Scope) -> Scope { - scope - .route("/allocations", post().to(create_allocation)) - .route("/allocations", get().to(get_allocations)) - .route("/allocations/{allocation_id}", get().to(get_allocation)) - .route("/allocations/{allocation_id}", put().to(amend_allocation)) - .route( - "/allocations/{allocation_id}", - delete().to(release_allocation), - ) - .route("/demandDecorations", get().to(get_demand_decorations)) -} + pub struct TokenName(String); -fn validate_network(network: &str) -> Result { - match NetworkName::from_str(network) { - Ok(NetworkName::Rinkeby) => Err("Rinkeby is no longer supported".to_string()), - Ok(network_name) => Ok(network_name), - Err(_) => Err(format!("Invalid network name: {network}")), + impl Display for TokenName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } } -} -fn validate_driver(network: &NetworkName, driver: &str) -> Result { - match DriverName::from_str(driver) { - Err(_) => Err(format!("Invalid driver name {}", driver)), - Ok(driver_name) => Ok(driver_name), + impl TokenName { + pub fn default(_driver: &DriverName, network: &NetworkName) -> TokenName { + Self(get_token_from_network_name(network).to_lowercase()) + } + + pub fn from_token_string( + driver: &DriverName, + network: &NetworkName, + token: &str, + ) -> Result { + if token == "GLM" || token == "tGLM" { + return Err(format!( + "Uppercase token names are not supported. Use lowercase glm or tglm instead of {}", + token + )); + } + let token_expected = Self::default(driver, network).to_string(); + if token != token_expected { + return Err(format!( + "Token {} does not match expected token {} for driver {} and network {}. \ + Note that for test networks expected token name is tglm and for production networks it is glm", + token, token_expected, driver, network + )); + } + Ok(Self(token.to_string())) + } } } -fn get_default_token(_driver: &DriverName, network: &NetworkName) -> String { - get_token_from_network_name(network).to_lowercase() -} +mod platform_triple { + use super::token_name::TokenName; + use super::*; + use anyhow::{anyhow, bail}; -fn validate_token(driver: &DriverName, network: &NetworkName, token: &str) -> Result<(), String> { - if token == "GLM" || token == "tGLM" { - return Err(format!( - "Uppercase token names are not supported. Use lowercase glm or tglm instead of {}", - token - )); - } - let token_expected = get_default_token(driver, network); - if token != token_expected { - return Err(format!( - "Token {} does not match expected token {} for driver {} and network {}. \ - Note that for test networks expected token name is tglm and for production networks it is glm", - token, token_expected, driver, network - )); + pub struct PaymentPlatformTriple { + driver: DriverName, + network: NetworkName, + token: TokenName, } - Ok(()) -} -fn bad_req_and_log(err_msg: String) -> HttpResponse { - log::error!("{}", err_msg); - response::bad_request(&err_msg) -} + impl PaymentPlatformTriple { + pub fn driver(&self) -> &DriverName { + &self.driver + } -fn payment_platform_to_string(p: &PaymentPlatform) -> Result { - let platform_str = if p.driver.is_none() && p.network.is_none() && p.token.is_none() { - let default_platform = default_payment_platform_testnet(); - log::debug!("Empty paymentPlatform object, using {default_platform}"); - default_platform - } else if p.token.is_some() && p.network.is_none() && p.driver.is_none() { - let token = p.token.as_ref().unwrap(); - if token == "GLM" || token == "tGLM" { - return Err(bad_req_and_log(format!( - "Uppercase token names are not supported. Use lowercase glm or tglm instead of {}", - token - ))); - } else if token == "glm" { - let default_platform = default_payment_platform_mainnet(); - log::debug!("Selected network {default_platform} (default for glm token)"); - default_platform - } else if token == "tglm" { - let default_platform = default_payment_platform_testnet(); - log::debug!("Selected network {default_platform} (default for tglm token)"); - default_platform - } else { - return Err(bad_req_and_log(format!( - "Only glm or tglm token values are accepted vs {token} provided" - ))); + pub fn network(&self) -> &NetworkName { + &self.network + } + + pub fn token(&self) -> &TokenName { + &self.token + } + + pub fn default_testnet() -> Self { + PaymentPlatformTriple { + driver: DEFAULT_PAYMENT_DRIVER, + network: DEFAULT_TESTNET_NETWORK, + token: TokenName::default(&DEFAULT_PAYMENT_DRIVER, &DEFAULT_TESTNET_NETWORK), + } } - } else { - let network_str = p.network.as_deref().unwrap_or_else(|| { - if let Some(token) = p.token.as_ref() { - if token == "glm" { + + pub fn default_mainnet() -> Self { + PaymentPlatformTriple { + driver: DEFAULT_PAYMENT_DRIVER, + network: DEFAULT_MAINNET_NETWORK, + token: TokenName::default(&DEFAULT_PAYMENT_DRIVER, &DEFAULT_MAINNET_NETWORK), + } + } + + pub fn from_payment_platform_input( + p: &PaymentPlatform, + ) -> anyhow::Result { + let platform = if p.driver.is_none() && p.network.is_none() && p.token.is_none() { + let default_platform = Self::default_testnet(); + log::debug!("Empty paymentPlatform object, using {default_platform}"); + default_platform + } else if p.token.is_some() && p.network.is_none() && p.driver.is_none() { + let token = p.token.as_ref().unwrap(); + if token == "GLM" || token == "tGLM" { + bail!( + "Uppercase token names are not supported. Use lowercase glm or tglm instead of {}", + token + ); + } else if token == "glm" { + let default_platform = Self::default_mainnet(); + log::debug!("Selected network {default_platform} (default for glm token)"); + default_platform + } else if token == "tglm" { + let default_platform = Self::default_testnet(); + log::debug!("Selected network {default_platform} (default for tglm token)"); + default_platform + } else { + bail!("Only glm or tglm token values are accepted vs {token} provided"); + } + } else { + let network_str = p.network.as_deref().unwrap_or_else(|| { + if let Some(token) = p.token.as_ref() { + if token == "glm" { + log::debug!( + "Network not specified, using default {}, because token set to glm", + DEFAULT_MAINNET_NETWORK + ); + DEFAULT_MAINNET_NETWORK.into() + } else { + log::debug!( + "Network not specified, using default {}", + DEFAULT_TESTNET_NETWORK + ); + DEFAULT_TESTNET_NETWORK.into() + } + } else { + log::debug!( + "Network not specified and token not specified, using default {}", + DEFAULT_TESTNET_NETWORK + ); + DEFAULT_TESTNET_NETWORK.into() + } + }); + let network = validate_network(network_str) + .map_err(|err| anyhow!("Validate network failed (1): {err}"))?; + + let driver_str = p.driver.as_deref().unwrap_or_else(|| { log::debug!( - "Network not specified, using default {}, because token set to glm", - DEFAULT_MAINNET_NETWORK + "Driver not specified, using default {}", + DEFAULT_PAYMENT_DRIVER ); - DEFAULT_MAINNET_NETWORK.into() + DEFAULT_PAYMENT_DRIVER.into() + }); + let driver = validate_driver(&network, driver_str) + .map_err(|err| anyhow!("Validate driver failed (1): {err}"))?; + + if let Some(token) = p.token.as_ref() { + let token = TokenName::from_token_string(&driver, &network, token) + .map_err(|err| anyhow!("Validate token failed (1): {err}"))?; + log::debug!("Selected network {}-{}-{}", driver, network, token); + Self { + driver, + network, + token, + } } else { + let default_token = TokenName::default(&driver, &network); + log::debug!( - "Network not specified, using default {}", - DEFAULT_TESTNET_NETWORK + "Selected network with default token {}-{}-{}", + driver, + network, + default_token ); - DEFAULT_TESTNET_NETWORK.into() + Self { + driver, + network, + token: default_token, + } } - } else { - log::debug!( - "Network not specified and token not specified, using default {}", - DEFAULT_TESTNET_NETWORK - ); - DEFAULT_TESTNET_NETWORK.into() - } - }); - let network = validate_network(network_str) - .map_err(|err| bad_req_and_log(format!("Validate network failed (1): {err}")))?; - - let driver_str = p.driver.as_deref().unwrap_or_else(|| { - log::debug!( - "Driver not specified, using default {}", - DEFAULT_PAYMENT_DRIVER - ); - DEFAULT_PAYMENT_DRIVER.into() - }); - let driver = validate_driver(&network, driver_str) - .map_err(|err| bad_req_and_log(format!("Validate driver failed (1): {err}")))?; - - if let Some(token) = p.token.as_ref() { - validate_token(&driver, &network, token) - .map_err(|err| bad_req_and_log(format!("Validate token failed (1): {err}")))?; - log::debug!("Selected network {}-{}-{}", driver, network, token); - format!("{}-{}-{}", driver, network, token) - } else { - let default_token = get_default_token(&driver, &network); + }; + Ok(platform) + } - log::debug!( - "Selected network with default token {}-{}-{}", + pub fn from_payment_platform_str( + payment_platform_str: &str, + ) -> anyhow::Result { + // payment_platform is of the form driver-network-token + // eg. erc20-rinkeby-tglm + let [driver_str, network_str, token_str]: [&str; 3] = payment_platform_str + .split('-') + .collect::>() + .try_into() + .map_err(|err| { + anyhow!( + "paymentPlatform must be of the form driver-network-token instead of {}", + payment_platform_str + ) + })?; + + let network = validate_network(network_str) + .map_err(|err| anyhow!("Validate network failed (2): {err}"))?; + + let driver = validate_driver(&network, driver_str) + .map_err(|err| anyhow!("Validate driver failed (2): {err}"))?; + + let token = TokenName::from_token_string(&driver, &network, token_str) + .map_err(|err| anyhow!("Validate token failed (2): {err}"))?; + + Ok(Self { driver, network, - default_token - ); - format!("{}-{}-{}", driver, network, default_token) + token, + }) } - }; - Ok(platform_str) -} + } -fn payment_platform_validate_and_check( - payment_platform_str: &str, -) -> Result<(DriverName, NetworkName, String), HttpResponse> { - // payment_platform is of the form driver-network-token - // eg. erc20-rinkeby-tglm - let [driver_str, network_str, token_str]: [&str; 3] = payment_platform_str - .split('-') - .collect::>() - .try_into() - .map_err(|err| { - bad_req_and_log(format!( - "paymentPlatform must be of the form driver-network-token instead of {}", - payment_platform_str - )) - })?; + impl Display for PaymentPlatformTriple { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}-{}", self.driver, self.network, self.token) + } + } - let network = validate_network(network_str) - .map_err(|err| bad_req_and_log(format!("Validate network failed (2): {err}")))?; + fn validate_network(network: &str) -> Result { + match NetworkName::from_str(network) { + Ok(NetworkName::Rinkeby) => Err("Rinkeby is no longer supported".to_string()), + Ok(network_name) => Ok(network_name), + Err(_) => Err(format!("Invalid network name: {network}")), + } + } - let driver = validate_driver(&network, driver_str) - .map_err(|err| bad_req_and_log(format!("Validate driver failed (2): {err}")))?; + fn validate_driver(network: &NetworkName, driver: &str) -> Result { + match DriverName::from_str(driver) { + Err(_) => Err(format!("Invalid driver name {}", driver)), + Ok(driver_name) => Ok(driver_name), + } + } +} - validate_token(&driver, &network, token_str) - .map_err(|err| bad_req_and_log(format!("Validate token failed (2): {err}")))?; +use platform_triple::PaymentPlatformTriple; - Ok((driver, network, token_str.to_string())) +pub fn register_endpoints(scope: Scope) -> Scope { + scope + .route("/allocations", post().to(create_allocation)) + .route("/allocations", get().to(get_allocations)) + .route("/allocations/{allocation_id}", get().to(get_allocation)) + .route("/allocations/{allocation_id}", put().to(amend_allocation)) + .route( + "/allocations/{allocation_id}", + delete().to(release_allocation), + ) + .route("/demandDecorations", get().to(get_demand_decorations)) } async fn create_allocation( @@ -220,21 +278,35 @@ async fn create_allocation( body: Json, id: Identity, ) -> HttpResponse { + let bad_req_and_log = |err_msg: String| -> HttpResponse { + log::error!("{}", err_msg); + response::bad_request(&err_msg) + }; + // TODO: Handle deposits & timeouts let allocation = body.into_inner(); let node_id = id.identity; - let payment_platform = match &allocation.payment_platform { + let payment_triple = match &allocation.payment_platform { Some(PaymentPlatformEnum::PaymentPlatformName(name)) => { - log::debug!("Using old API payment platform name as pure str: {}", name); - name.clone() + let payment_platform = match PaymentPlatformTriple::from_payment_platform_str(name) { + Ok(p) => p, + Err(err) => return bad_req_and_log(format!("{}", err)), + }; + log::debug!( + "Successfully parsed API payment platform name: {}", + payment_platform + ); + payment_platform + } + Some(PaymentPlatformEnum::PaymentPlatform(payment_platform)) => { + match PaymentPlatformTriple::from_payment_platform_input(payment_platform) { + Ok(platform_str) => platform_str, + Err(err) => return bad_req_and_log(format!("{}", err)), + } } - Some(PaymentPlatformEnum::PaymentPlatform(p)) => match payment_platform_to_string(p) { - Ok(platform_str) => platform_str, - Err(e) => return e, - }, None => { - let default_platform = default_payment_platform_testnet(); + let default_platform = PaymentPlatformTriple::default_testnet(); log::debug!("No paymentPlatform entry found, using {default_platform}"); default_platform } @@ -245,25 +317,15 @@ async fn create_allocation( .clone() .unwrap_or_else(|| node_id.to_string()); - // payment_platform is of the form driver-network-token - // This function rechecks depending on the flow, but the check is cheap and also counts as sanity check - let (driver, network, token_str) = match payment_platform_validate_and_check(&payment_platform) - { - Ok((driver, network, token_str)) => (driver, network, token_str), - Err(e) => return e, - }; - log::info!( - "Creating allocation for payment platform: {}-{}-{}", - driver, - network, - token_str + "Creating allocation for payment platform: {}", + payment_triple ); let acc = Account { - driver: driver.to_string(), + driver: payment_triple.driver().to_string(), address: address.clone(), - network: Some(network.to_string()), + network: Some(payment_triple.network().to_string()), token: None, send: true, receive: false, @@ -274,7 +336,7 @@ async fn create_allocation( } let validate_msg = ValidateAllocation { - platform: payment_platform.clone(), + platform: payment_triple.to_string(), address: address.clone(), amount: allocation.total_amount.clone(), }; @@ -282,14 +344,14 @@ async fn create_allocation( match async move { Ok(bus::service(LOCAL_SERVICE).send(validate_msg).await??) }.await { Ok(true) => {} Ok(false) => { - return bad_req_and_log(format!("Insufficient funds to make allocation for payment platform {payment_platform}. \ + return bad_req_and_log(format!("Insufficient funds to make allocation for payment platform {payment_triple}. \ Top up your account or release all existing allocations to unlock the funds via `yagna payment release-allocations`")); } Err(Error::Rpc(RpcMessageError::ValidateAllocation( ValidateAllocationError::AccountNotRegistered, ))) => { return bad_req_and_log(format!( - "Account not registered - payment platform {payment_platform}" + "Account not registered - payment platform {payment_triple}" )); } Err(e) => return response::server_error(&e), @@ -298,7 +360,7 @@ async fn create_allocation( let dao = db.as_dao::(); match dao - .create(allocation, node_id, payment_platform, address) + .create(allocation, node_id, payment_triple.to_string(), address) .await { Ok(allocation_id) => match dao.get(allocation_id, node_id).await {