use std::collections::HashMap;
use bigdecimal::{BigDecimal, Zero};
use prost::Message;
use zera_proto::zera_txn::{
BaseTxn, CoinTxn, ContractFeeType, ContractUpdateTxn, GovernanceVote, InstrumentContract,
PublicKey, SmartContractExecuteTxn, TransactionType,
};
use crate::api::{
get_balance_with_client, get_base_fee_with_client, get_token_fee_info_with_client,
GetTokenFeeInfoParams, TokenInfo,
};
use crate::crypto::address::{
get_key_type_from_public_key, get_public_key_identifier_from_bytes, sanitize_and_decode_address,
};
use crate::crypto::constants::KeyType;
use crate::error::{Result, ZeraError};
use crate::fees::{
get_decimal_places_from_denomination, get_denomination_fallback, get_signature_size, HASH_SIZE,
PROTOBUF_AUTH_SIGNATURE_OVERHEAD, PROTOBUF_BASE_SIGNATURE_OVERHEAD, PROTOBUF_HASH_OVERHEAD,
};
use crate::grpc::{UnaryTransport, ValidatorApiClient};
use crate::tx::{BaseTxnAccess, TypedTransaction};
use crate::types::RpcConfig;
use crate::utils::amount::{
decimal_from_f64, floor_decimal, floor_decimal_to_string, floor_to_scale, pow10, to_decimal,
to_smallest_units,
};
use crate::utils::token::normalize_contract_id;
const DEFAULT_BASE_FEE_ID: &str = "$ZRA+0000";
const NON_NATIVE_FEE_MULTIPLIER: u32 = 10;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FeeConfig {
pub base_fee_id: Option<String>,
pub base_fee: Option<String>,
pub contract_fee_id: Option<String>,
pub contract_fee: Option<String>,
pub interface_fee_id: Option<String>,
pub interface_fee: Option<String>,
pub interface_address: Option<String>,
pub overestimate_percent: Option<f64>,
pub gas_fee_in_usd: Option<f64>,
pub grpc_config: Option<RpcConfig>,
pub needs_initialization: Option<bool>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FeeConfigHelper<T> {
pub base_fee_id: Option<String>,
pub base_fee: Option<String>,
pub contract_fee_id: Option<String>,
pub contract_fee: Option<String>,
pub interface_fee_id: Option<String>,
pub interface_fee: Option<String>,
pub interface_address: Option<String>,
pub overestimate_percent: Option<f64>,
pub gas_fee_in_usd: Option<f64>,
pub grpc_config: Option<RpcConfig>,
pub needs_initialization: Option<bool>,
pub contract_id: Option<String>,
pub proto_object: T,
pub token_info_map: HashMap<String, TokenInfo>,
}
impl<T> From<(T, FeeConfig, HashMap<String, TokenInfo>)> for FeeConfigHelper<T> {
fn from(
(proto_object, fee_config, token_info_map): (T, FeeConfig, HashMap<String, TokenInfo>),
) -> Self {
Self {
base_fee_id: fee_config.base_fee_id,
base_fee: fee_config.base_fee,
contract_fee_id: fee_config.contract_fee_id,
contract_fee: fee_config.contract_fee,
interface_fee_id: fee_config.interface_fee_id,
interface_fee: fee_config.interface_fee,
interface_address: fee_config.interface_address,
overestimate_percent: fee_config.overestimate_percent,
gas_fee_in_usd: fee_config.gas_fee_in_usd,
grpc_config: fee_config.grpc_config,
needs_initialization: fee_config.needs_initialization,
contract_id: None,
proto_object,
token_info_map,
}
}
}
pub trait FeeTransaction: Message + BaseTxnAccess + TypedTransaction + Sized {
fn first_public_key(&self) -> Option<PublicKey>;
fn key_types(&self) -> Result<Vec<KeyType>>;
fn as_coin_txn(&self) -> Option<&CoinTxn> {
None
}
fn as_coin_txn_mut(&mut self) -> Option<&mut CoinTxn> {
None
}
}
impl FeeTransaction for CoinTxn {
fn first_public_key(&self) -> Option<PublicKey> {
self.auth
.as_ref()
.and_then(|auth| auth.public_key.first().cloned())
}
fn key_types(&self) -> Result<Vec<KeyType>> {
extract_key_types(
self.auth
.as_ref()
.map(|auth| auth.public_key.as_slice())
.unwrap_or(&[]),
)
}
fn as_coin_txn(&self) -> Option<&CoinTxn> {
Some(self)
}
fn as_coin_txn_mut(&mut self) -> Option<&mut CoinTxn> {
Some(self)
}
}
impl FeeTransaction for GovernanceVote {
fn first_public_key(&self) -> Option<PublicKey> {
self.base.as_ref().and_then(|base| base.public_key.clone())
}
fn key_types(&self) -> Result<Vec<KeyType>> {
extract_base_key_types(self.base.as_ref())
}
}
impl FeeTransaction for InstrumentContract {
fn first_public_key(&self) -> Option<PublicKey> {
self.base.as_ref().and_then(|base| base.public_key.clone())
}
fn key_types(&self) -> Result<Vec<KeyType>> {
extract_base_key_types(self.base.as_ref())
}
}
impl FeeTransaction for ContractUpdateTxn {
fn first_public_key(&self) -> Option<PublicKey> {
self.base.as_ref().and_then(|base| base.public_key.clone())
}
fn key_types(&self) -> Result<Vec<KeyType>> {
extract_base_key_types(self.base.as_ref())
}
}
impl FeeTransaction for SmartContractExecuteTxn {
fn first_public_key(&self) -> Option<PublicKey> {
self.base.as_ref().and_then(|base| base.public_key.clone())
}
fn key_types(&self) -> Result<Vec<KeyType>> {
extract_base_key_types(self.base.as_ref())
}
}
pub struct UniversalFeeCalculator;
impl UniversalFeeCalculator {
pub async fn calculate_fee<T>(options: FeeConfigHelper<T>) -> Result<T>
where
T: FeeTransaction,
{
let client = ValidatorApiClient::new(options.grpc_config.clone().unwrap_or_default())?;
Self::calculate_fee_with_client(options, &client).await
}
pub async fn calculate_fee_with_client<T, U>(
mut options: FeeConfigHelper<T>,
client: &ValidatorApiClient<U>,
) -> Result<T>
where
T: FeeTransaction,
U: UnaryTransport,
{
let effective_base_fee_id = normalize_contract_id(
options
.base_fee_id
.as_deref()
.unwrap_or(DEFAULT_BASE_FEE_ID),
);
ensure_token_info_present(&mut options.token_info_map, &effective_base_fee_id, client)
.await?;
let transaction_type = extract_transaction_type::<T>()?;
if let Some(coin_txn) = options.proto_object.as_coin_txn_mut() {
if let Some(contract_id) =
(!coin_txn.contract_id.is_empty()).then(|| coin_txn.contract_id.clone())
{
if has_contract_fees(&options.token_info_map, &contract_id) {
calculate_contract_fee(
coin_txn,
options.contract_fee_id.as_deref(),
options.contract_fee.as_deref(),
&options.token_info_map,
options.overestimate_percent.unwrap_or(5.0),
)?;
}
}
}
if options.interface_fee_id.is_some() && options.interface_fee.is_some() {
calculate_interface_fee(
&mut options.proto_object,
options.interface_fee.as_deref(),
options.interface_fee_id.as_deref(),
options.interface_address.as_deref(),
&options.token_info_map,
)?;
}
let new_wallet_fee_scaled = calculate_network_fee(
&mut options.proto_object,
transaction_type,
&effective_base_fee_id,
options.base_fee.as_deref(),
&options.token_info_map,
options.overestimate_percent.unwrap_or(5.0),
options.gas_fee_in_usd,
client,
)
.await?;
if transaction_type == TransactionType::CoinType && options.base_fee.is_none() {
if let Some(coin_txn) = options.proto_object.as_coin_txn_mut() {
if !coin_txn.contract_id.is_empty() {
let _ = calculate_new_token_balance_fee(
coin_txn,
&effective_base_fee_id,
&coin_txn.contract_id.clone(),
&options.token_info_map,
&new_wallet_fee_scaled,
options.needs_initialization,
client,
)
.await;
}
}
}
Ok(options.proto_object)
}
}
fn extract_transaction_type<T>() -> Result<TransactionType>
where
T: TypedTransaction,
{
match T::TYPE_NAME {
"zera_txn.CoinTXN" => Ok(TransactionType::CoinType),
"zera_txn.InstrumentContract" => Ok(TransactionType::ContractTxnType),
"zera_txn.GovernanceVote" => Ok(TransactionType::VoteType),
"zera_txn.SmartContractExecuteTXN" => Ok(TransactionType::SmartContractExecuteType),
"zera_txn.ContractUpdateTXN" => Ok(TransactionType::UpdateContractType),
_ => Err(ZeraError::Unsupported(format!(
"Unable to determine transaction type from {}",
T::TYPE_NAME
))),
}
}
fn extract_base_key_types(base: Option<&BaseTxn>) -> Result<Vec<KeyType>> {
let Some(base) = base else {
return Err(ZeraError::Validation(
"Failed to detect key type from BaseTXN transaction. Check that publicKey contains valid key structure.".to_string(),
));
};
let public_key = base.public_key.as_ref().ok_or_else(|| {
ZeraError::Validation(
"Failed to detect key type from BaseTXN transaction. Check that publicKey contains valid key structure.".to_string(),
)
})?;
extract_key_types(std::slice::from_ref(public_key))
}
fn extract_key_types(public_keys: &[PublicKey]) -> Result<Vec<KeyType>> {
let mut key_types = Vec::new();
for public_key in public_keys {
if !public_key.single.is_empty() {
let identifier = get_public_key_identifier_from_bytes(&public_key.single)?;
key_types.push(get_key_type_from_public_key(&identifier)?);
} else {
return Err(ZeraError::Unsupported(
"Multi-signature wallet not currently supported in SDK".to_string(),
));
}
}
if key_types.is_empty() {
return Err(ZeraError::Validation(
"Failed to detect key types from transaction. Transaction must have either auth.publicKey (CoinTXN) or base.publicKey (other types).".to_string(),
));
}
Ok(key_types)
}
fn calculate_total_transaction_size<T>(proto_object: &T) -> Result<usize>
where
T: FeeTransaction,
{
let proto_size = proto_object.encode_to_vec().len();
let key_types = proto_object.key_types()?;
let is_coin = proto_object.as_coin_txn().is_some();
let mut signature_size = 0usize;
for key_type in &key_types {
let raw_signature_size = get_signature_size(*key_type);
signature_size += raw_signature_size
+ if is_coin {
PROTOBUF_AUTH_SIGNATURE_OVERHEAD
} else {
PROTOBUF_BASE_SIGNATURE_OVERHEAD
};
}
if key_types.len() == 1 && key_types[0] == KeyType::Ed448 {
signature_size += 1;
}
Ok(proto_size + signature_size + HASH_SIZE + PROTOBUF_HASH_OVERHEAD)
}
fn has_contract_fees(token_info_map: &HashMap<String, TokenInfo>, contract_id: &str) -> bool {
token_info_map
.get(contract_id)
.and_then(|token| token.contract_fees.as_ref())
.is_some()
}
async fn ensure_token_info_present<T>(
token_info_map: &mut HashMap<String, TokenInfo>,
contract_id: &str,
client: &ValidatorApiClient<T>,
) -> Result<()>
where
T: UnaryTransport,
{
if token_info_map.contains_key(contract_id) {
return Ok(());
}
let response = get_token_fee_info_with_client(
GetTokenFeeInfoParams {
contract_ids: vec![contract_id.to_string()],
},
client,
)
.await?;
for token in response.tokens {
let normalized = normalize_contract_id(&token.contract_id);
let token_info = TokenInfo {
contract_id: token.contract_id.clone(),
denomination: token.denomination.clone(),
rate: token.rate.clone(),
authorized: token.authorized,
allowed_fees: token.allowed_fees.clone(),
used_fees: token.used_fees.clone(),
contract_fees: token.contract_fees.clone(),
};
token_info_map.insert(token.contract_id.clone(), token_info.clone());
token_info_map.insert(normalized, token_info);
}
Ok(())
}
fn fee_multiplier(base_fee_id: &str) -> BigDecimal {
if base_fee_id == DEFAULT_BASE_FEE_ID {
BigDecimal::from(1u32)
} else {
BigDecimal::from(NON_NATIVE_FEE_MULTIPLIER)
}
}
fn token_decimals_for(
token_info_map: &HashMap<String, TokenInfo>,
contract_id: &str,
) -> Result<u32> {
let token = token_info_map
.get(contract_id)
.ok_or_else(|| ZeraError::Validation(format!("Token info not found for {contract_id}")))?;
let denomination = if token.denomination.is_empty() {
get_denomination_fallback(contract_id)?
} else {
token.denomination.clone()
};
get_decimal_places_from_denomination(&denomination)
}
fn calculate_contract_fee(
coin_txn: &mut CoinTxn,
contract_fee_id: Option<&str>,
contract_fee: Option<&str>,
token_info_map: &HashMap<String, TokenInfo>,
overestimate_percent: f64,
) -> Result<()> {
let contract_id = coin_txn.contract_id.clone();
if let (Some(contract_fee), Some(contract_fee_id)) = (contract_fee, contract_fee_id) {
let contract_fee_amount =
to_smallest_units(contract_fee, contract_fee_id, None, Some(token_info_map))?;
coin_txn.contract_fee_id = Some(contract_fee_id.to_string());
coin_txn.contract_fee_amount = Some(contract_fee_amount);
return Ok(());
}
let contract_fees = token_info_map
.get(&contract_id)
.and_then(|token| token.contract_fees.as_ref())
.ok_or_else(|| ZeraError::Validation("contractId not found".to_string()))?;
if contract_fees.allowed_fee_instrument.is_empty() {
return Ok(());
}
let contract_fee_id = normalize_contract_id(contract_fee_id.unwrap_or(&contract_id));
let current_token = token_info_map.get(&contract_id).ok_or_else(|| {
ZeraError::Validation(format!(
"Exchange rate not available for contract {contract_id}"
))
})?;
if current_token.rate.is_empty() {
return Err(ZeraError::Validation(format!(
"Exchange rate not available for contract {contract_id}"
)));
}
let output_total = coin_txn
.output_transfers
.iter()
.fold(BigDecimal::zero(), |sum, output| {
sum + to_decimal(&output.amount).unwrap_or_else(|_| BigDecimal::zero())
});
let denomination_decimals = if current_token.denomination.is_empty() {
get_decimal_places_from_denomination(&get_denomination_fallback(&contract_id)?)?
} else {
get_decimal_places_from_denomination(¤t_token.denomination)?
};
let transaction_amount_in_token_units = output_total.clone() / pow10(denomination_decimals);
let rate_decimal = to_decimal(¤t_token.rate)?;
let usd_value_scaled = transaction_amount_in_token_units * rate_decimal.clone();
let convert_usd_to_fee_token_smallest_units =
|usd_amount: BigDecimal, fee_contract_id: &str| -> Result<String> {
let fee_token = token_info_map.get(fee_contract_id).ok_or_else(|| {
ZeraError::Validation(format!(
"Exchange rate not available for fee contract {fee_contract_id}"
))
})?;
if fee_token.rate.is_empty() {
return Err(ZeraError::Validation(format!(
"Exchange rate not available for fee contract {fee_contract_id}"
)));
}
let fee_token_decimals = if fee_token.denomination.is_empty() {
get_decimal_places_from_denomination(&get_denomination_fallback(fee_contract_id)?)?
} else {
get_decimal_places_from_denomination(&fee_token.denomination)?
};
let fee_token_rate = to_decimal(&fee_token.rate)?;
let fee_token_units = usd_amount / fee_token_rate;
Ok(floor_decimal_to_string(
&(fee_token_units * pow10(fee_token_decimals)),
))
};
let mut transaction_amount = match ContractFeeType::try_from(contract_fees.contract_fee_type)
.unwrap_or(ContractFeeType::None)
{
ContractFeeType::Fixed => {
if contract_fee_id == contract_id {
contract_fees.fee.clone()
} else {
let fee_amount_decimal = to_decimal(&contract_fees.fee)?;
let fee_amount_in_usd = fee_amount_decimal
* (BigDecimal::from(1u32) / pow10(denomination_decimals))
* rate_decimal;
convert_usd_to_fee_token_smallest_units(fee_amount_in_usd, &contract_fee_id)?
}
}
ContractFeeType::CurEquivalent => convert_usd_to_fee_token_smallest_units(
to_decimal(&contract_fees.fee)?,
&contract_fee_id,
)?,
ContractFeeType::Percentage => {
let percentage_value =
(to_decimal(&contract_fees.fee)? / pow10(18)) * BigDecimal::from(100u32);
if contract_fee_id == contract_id {
floor_decimal_to_string(
&((output_total * percentage_value) / BigDecimal::from(100u32)),
)
} else {
let percentage_of_transaction_usd =
(usd_value_scaled * percentage_value) / BigDecimal::from(100u32);
convert_usd_to_fee_token_smallest_units(
percentage_of_transaction_usd,
&contract_fee_id,
)?
}
}
ContractFeeType::None => return Ok(()),
};
if overestimate_percent > 0.0 {
let multiplier = (BigDecimal::from(100u32) + decimal_from_f64(overestimate_percent)?)
/ BigDecimal::from(100u32);
transaction_amount =
floor_decimal_to_string(&(to_decimal(&transaction_amount)? * multiplier));
}
coin_txn.contract_fee_id = Some(contract_fee_id);
coin_txn.contract_fee_amount = Some(transaction_amount);
Ok(())
}
fn calculate_interface_fee<T>(
proto_object: &mut T,
interface_fee_amount: Option<&str>,
interface_fee_id: Option<&str>,
interface_address: Option<&str>,
token_info_map: &HashMap<String, TokenInfo>,
) -> Result<()>
where
T: BaseTxnAccess,
{
let (Some(interface_fee_amount), Some(interface_fee_id)) =
(interface_fee_amount, interface_fee_id)
else {
return Ok(());
};
let Some(base) = proto_object.base_mut() else {
return Ok(());
};
let Some(interface_address) = interface_address else {
return Ok(());
};
let fee_in_smallest_units = to_smallest_units(
interface_fee_amount,
interface_fee_id,
None,
Some(token_info_map),
)?;
base.interface_fee = Some(fee_in_smallest_units);
base.interface_fee_id = Some(interface_fee_id.to_string());
base.interface_address = Some(sanitize_and_decode_address(interface_address)?);
Ok(())
}
fn calculate_gas_fee(
gas_fee_in_usd: f64,
base_fee_id: &str,
token_info_map: &HashMap<String, TokenInfo>,
) -> Result<String> {
let token = token_info_map.get(base_fee_id).ok_or_else(|| {
ZeraError::Validation(format!(
"Token info not found for fee ID {base_fee_id} when calculating gas fee"
))
})?;
let exchange_rate = if token.rate.is_empty() {
BigDecimal::from(1u32)
} else {
to_decimal(&token.rate)?
};
let decimals = token_decimals_for(token_info_map, base_fee_id)?;
let gas_fee_scaled =
decimal_from_f64(gas_fee_in_usd)? * pow10(18) * fee_multiplier(base_fee_id);
let gas_fee_in_token_units = gas_fee_scaled / exchange_rate;
let rounded = floor_to_scale(&gas_fee_in_token_units, decimals);
let final_value = if rounded.is_zero() && gas_fee_in_token_units > BigDecimal::zero() {
BigDecimal::from(1u32) / pow10(decimals)
} else {
rounded
};
to_smallest_units(
final_value.to_string(),
base_fee_id,
None,
Some(token_info_map),
)
}
#[allow(clippy::too_many_arguments)]
async fn calculate_network_fee<T, U>(
proto_object: &mut T,
transaction_type: TransactionType,
base_fee_id: &str,
base_fee: Option<&str>,
token_info_map: &HashMap<String, TokenInfo>,
overestimate_percent: f64,
gas_fee_in_usd: Option<f64>,
client: &ValidatorApiClient<U>,
) -> Result<String>
where
T: FeeTransaction,
U: UnaryTransport,
{
if let Some(base_fee) = base_fee {
let mut final_fee = to_smallest_units(base_fee, base_fee_id, None, Some(token_info_map))?;
if let Some(gas_fee_in_usd) = gas_fee_in_usd.filter(|value| *value > 0.0) {
let gas_fee = calculate_gas_fee(gas_fee_in_usd, base_fee_id, token_info_map)?;
final_fee = floor_decimal_to_string(&(to_decimal(&final_fee)? + to_decimal(&gas_fee)?));
}
if let Some(base) = proto_object.base_mut() {
base.fee_amount = final_fee;
base.fee_id = base_fee_id.to_string();
}
return Ok("0".to_string());
}
let mut transaction_size = calculate_total_transaction_size(proto_object)?;
let base_fee_response =
get_base_fee_with_client(transaction_type, proto_object.first_public_key(), client).await?;
let new_wallet_fee_scaled = if base_fee_response.new_wallet_fee.is_empty() {
"0".to_string()
} else {
base_fee_response.new_wallet_fee.clone()
};
let per_byte_fee = to_decimal(&base_fee_response.byte_fee)? / pow10(18);
let key_and_hash_fees =
(to_decimal(&base_fee_response.key_fee)? / pow10(18)) * fee_multiplier(base_fee_id);
let exchange_rate = token_info_map
.get(base_fee_id)
.map(|token| {
if token.rate.is_empty() {
Ok(BigDecimal::from(1u32))
} else {
to_decimal(&token.rate)
}
})
.transpose()?
.unwrap_or_else(|| BigDecimal::from(1u32));
let decimals = token_decimals_for(token_info_map, base_fee_id)?;
let mut total_network_fee = ((BigDecimal::from(transaction_size as u64)
* per_byte_fee.clone())
* fee_multiplier(base_fee_id)
+ key_and_hash_fees)
* pow10(18);
total_network_fee = total_network_fee / exchange_rate.clone();
let mut rounded_fee = floor_to_scale(&total_network_fee, decimals);
if rounded_fee.is_zero() && total_network_fee > BigDecimal::zero() {
rounded_fee = BigDecimal::from(1u32) / pow10(decimals);
}
let mut transaction_amount = to_smallest_units(
rounded_fee.to_string(),
base_fee_id,
None,
Some(token_info_map),
)?;
let fee_size_difference = transaction_amount.len().saturating_sub(1);
if fee_size_difference > 0 {
transaction_size += fee_size_difference;
let mut corrected = ((BigDecimal::from(transaction_size as u64) * per_byte_fee)
* fee_multiplier(base_fee_id)
+ (to_decimal(&base_fee_response.key_fee)? / pow10(18)) * fee_multiplier(base_fee_id))
* pow10(18);
corrected = corrected / exchange_rate;
let corrected_rounded = floor_to_scale(&corrected, decimals);
transaction_amount = to_smallest_units(
corrected_rounded.to_string(),
base_fee_id,
None,
Some(token_info_map),
)?;
}
if overestimate_percent > 0.0 {
let multiplier = (BigDecimal::from(100u32) + decimal_from_f64(overestimate_percent)?)
/ BigDecimal::from(100u32);
transaction_amount =
floor_decimal_to_string(&(to_decimal(&transaction_amount)? * multiplier));
}
if let Some(gas_fee_in_usd) = gas_fee_in_usd.filter(|value| *value > 0.0) {
let gas_fee = calculate_gas_fee(gas_fee_in_usd, base_fee_id, token_info_map)?;
transaction_amount =
floor_decimal_to_string(&(to_decimal(&transaction_amount)? + to_decimal(&gas_fee)?));
}
if let Some(base) = proto_object.base_mut() {
base.fee_amount = transaction_amount;
base.fee_id = base_fee_id.to_string();
}
Ok(new_wallet_fee_scaled)
}
async fn calculate_new_token_balance_fee<T>(
coin_txn: &mut CoinTxn,
base_fee_id: &str,
contract_id: &str,
token_info_map: &HashMap<String, TokenInfo>,
new_wallet_fee_scaled: &str,
needs_initialization: Option<bool>,
client: &ValidatorApiClient<T>,
) -> Result<()>
where
T: UnaryTransport,
{
let mut addresses_to_check = Vec::new();
for output in &coin_txn.output_transfers {
if !output.wallet_address.is_empty() {
let address = bs58::encode(&output.wallet_address).into_string();
if !addresses_to_check.contains(&address) {
addresses_to_check.push(address);
}
}
}
if addresses_to_check.is_empty() {
return Ok(());
}
let addresses_without_balance = match needs_initialization {
Some(true) => addresses_to_check.len(),
Some(false) => 0,
None => {
let mut missing = 0usize;
for address in &addresses_to_check {
match get_balance_with_client(address, contract_id, client).await {
Ok(response) if response.balance == "0" => missing += 1,
Ok(_) => {}
Err(_) => missing += 1,
}
}
missing
}
};
if addresses_without_balance == 0 {
return Ok(());
}
let token = token_info_map.get(base_fee_id).ok_or_else(|| {
ZeraError::Validation(format!(
"Cannot calculate new token balance fee: missing exchange rate for base fee token {base_fee_id}"
))
})?;
if token.rate.is_empty() {
return Ok(());
}
let total_fee_scaled = to_decimal(new_wallet_fee_scaled)?
* BigDecimal::from(addresses_without_balance as u64)
* fee_multiplier(base_fee_id);
let fee_in_token_units = total_fee_scaled / to_decimal(&token.rate)?;
let decimals = token_decimals_for(token_info_map, base_fee_id)?;
let fee_in_smallest_units = floor_decimal(&(fee_in_token_units * pow10(decimals)));
if let Some(base) = coin_txn.base.as_mut() {
let current_fee = to_decimal(&base.fee_amount)?;
base.fee_amount = floor_decimal_to_string(&(current_fee + fee_in_smallest_units));
}
Ok(())
}