diff --git a/mobile_verifier/migrations/36_promotion_rewards.sql b/mobile_verifier/migrations/36_promotion_rewards.sql index a989132fa..db7a34b19 100644 --- a/mobile_verifier/migrations/36_promotion_rewards.sql +++ b/mobile_verifier/migrations/36_promotion_rewards.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS promotion_rewards ( time_of_reward TIMESTAMPTZ NOT NULL, subscriber_id BYTEA NOT NULL, gateway_key TEXT NOT NULL, - service_provider BIGINT NOT NULL, + service_provider INTEGER NOT NULL, shares BIGINT NOT NULL, PRIMARY KEY (time_of_reward, subscriber_id, gateway_key, service_provider) ); diff --git a/mobile_verifier/src/promotion_reward.rs b/mobile_verifier/src/promotion_reward.rs index 25fafe189..c54ab9fe5 100644 --- a/mobile_verifier/src/promotion_reward.rs +++ b/mobile_verifier/src/promotion_reward.rs @@ -238,20 +238,19 @@ pub async fn clear_promotion_rewards( } pub struct AggregatePromotionRewards { - rewards: Vec, + pub rewards: Vec, } pub struct PromotionRewardShares { - service_provider: ServiceProvider, - rewardable_entity: Entity, - shares: u64, + pub service_provider: i32, + pub rewardable_entity: Entity, + pub shares: u64, } impl sqlx::FromRow<'_, PgRow> for PromotionRewardShares { fn from_row(row: &PgRow) -> sqlx::Result { let subscriber_id: Vec = row.try_get("subscriber_id")?; let shares: i64 = row.try_get("shares")?; - let service_provider: i64 = row.try_get("service_provider")?; Ok(Self { rewardable_entity: if subscriber_id.is_empty() { Entity::GatewayKey(row.try_get("gateway_key")?) @@ -259,7 +258,7 @@ impl sqlx::FromRow<'_, PgRow> for PromotionRewardShares { Entity::SubscriberId(subscriber_id) }, shares: shares as u64, - service_provider: ServiceProvider::try_from(service_provider as i32).unwrap(), + service_provider: row.try_get("service_provider")?, }) } } @@ -297,7 +296,7 @@ impl AggregatePromotionRewards { let total_promotion_rewards_allocated = sp_rewards.get_total_rewards_allocated_for_promotion(); let total_shares_per_service_provider = self.rewards.iter().fold( - HashMap::::default(), + HashMap::::default(), |mut shares, promotion_reward_share| { *shares .entry(promotion_reward_share.service_provider) @@ -308,16 +307,21 @@ impl AggregatePromotionRewards { let sp_promotion_reward_shares: HashMap<_, _> = total_shares_per_service_provider .iter() .map(|(sp, total_shares)| { - let rewards_allocated_for_promotion = - sp_rewards.take_rewards_allocated_for_promotion(sp); - let share_of_unallocated_pool = - rewards_allocated_for_promotion / total_promotion_rewards_allocated; - let sp_share = SpPromotionRewardShares { - shares_per_reward: rewards_allocated_for_promotion / total_shares, - shares_per_matched_reward: unallocated_sp_rewards * share_of_unallocated_pool - / total_shares, - }; - (*sp, sp_share) + if total_promotion_rewards_allocated.is_zero() || total_shares.is_zero() { + (*sp, SpPromotionRewardShares::default()) + } else { + let rewards_allocated_for_promotion = + sp_rewards.take_rewards_allocated_for_promotion(sp); + let share_of_unallocated_pool = + rewards_allocated_for_promotion / total_promotion_rewards_allocated; + let sp_share = SpPromotionRewardShares { + shares_per_reward: rewards_allocated_for_promotion / total_shares, + shares_per_matched_reward: unallocated_sp_rewards + * share_of_unallocated_pool + / total_shares, + }; + (*sp, sp_share) + } }) .collect(); @@ -357,3 +361,183 @@ impl AggregatePromotionRewards { }) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::reward_shares::ServiceProviderReward; + use chrono::Duration; + use helium_proto::services::poc_mobile::{ + mobile_reward_share::Reward, promotion_reward::Entity as ProtoEntity, PromotionReward, + }; + use rust_decimal_macros::dec; + + fn aggregate_subcriber_rewards( + rewards: impl Iterator, + ) -> HashMap, (u64, u64)> { + let mut aggregated = HashMap::, (u64, u64)>::new(); + for (_, reward) in rewards { + match reward.reward { + Some(Reward::PromotionReward(PromotionReward { + entity: Some(ProtoEntity::SubscriberId(subscriber_id)), + service_provider_amount, + matched_amount, + })) => { + let entry = aggregated.entry(subscriber_id).or_default(); + entry.0 += service_provider_amount; + entry.1 += matched_amount; + } + _ => (), + } + } + aggregated + } + + #[test] + fn ensure_no_rewards_if_none_allocated() { + let now = Utc::now(); + let epoch = now - Duration::hours(24)..now; + let mut rewards = HashMap::new(); + rewards.insert( + 0_i32, + ServiceProviderReward { + for_service_provider: dec!(100), + for_promotions: dec!(0), + }, + ); + let mut sp_rewards = ServiceProviderRewards { rewards }; + let promotion_rewards = AggregatePromotionRewards { + rewards: vec![PromotionRewardShares { + service_provider: 0, + rewardable_entity: Entity::SubscriberId(vec![0]), + shares: 1, + }], + }; + let result = aggregate_subcriber_rewards(promotion_rewards.into_rewards( + &mut sp_rewards, + dec!(0), + &epoch, + )); + assert!(result.is_empty()); + } + + #[test] + fn ensure_no_matched_rewards_if_no_unallocated() { + let now = Utc::now(); + let epoch = now - Duration::hours(24)..now; + let mut rewards = HashMap::new(); + rewards.insert( + 0_i32, + ServiceProviderReward { + for_service_provider: dec!(100), + for_promotions: dec!(100), + }, + ); + let mut sp_rewards = ServiceProviderRewards { rewards }; + let promotion_rewards = AggregatePromotionRewards { + rewards: vec![PromotionRewardShares { + service_provider: 0, + rewardable_entity: Entity::SubscriberId(vec![0]), + shares: 1, + }], + }; + let result = aggregate_subcriber_rewards(promotion_rewards.into_rewards( + &mut sp_rewards, + dec!(0), + &epoch, + )); + assert_eq!(sp_rewards.rewards.get(&0).unwrap().for_promotions, dec!(0)); + let result = result.get(&vec![0]).unwrap(); + assert_eq!(result.0, 100); + assert_eq!(result.1, 0); + } + + #[test] + fn ensure_fully_matched_rewards_and_correctly_divided() { + let now = Utc::now(); + let epoch = now - Duration::hours(24)..now; + let mut rewards = HashMap::new(); + rewards.insert( + 0_i32, + ServiceProviderReward { + for_service_provider: dec!(100), + for_promotions: dec!(100), + }, + ); + let mut sp_rewards = ServiceProviderRewards { rewards }; + let promotion_rewards = AggregatePromotionRewards { + rewards: vec![ + PromotionRewardShares { + service_provider: 0, + rewardable_entity: Entity::SubscriberId(vec![0]), + shares: 1, + }, + PromotionRewardShares { + service_provider: 0, + rewardable_entity: Entity::SubscriberId(vec![1]), + shares: 2, + }, + ], + }; + let result = aggregate_subcriber_rewards(promotion_rewards.into_rewards( + &mut sp_rewards, + dec!(100), + &epoch, + )); + assert_eq!(sp_rewards.rewards.get(&0).unwrap().for_promotions, dec!(0)); + let result1 = result.get(&vec![0]).unwrap(); + assert_eq!(result1.0, 33); + assert_eq!(result1.1, 33); + let result2 = result.get(&vec![1]).unwrap(); + assert_eq!(result2.0, 66); + assert_eq!(result2.1, 66); + } + + #[test] + fn ensure_properly_scaled_unallocated_rewards() { + let now = Utc::now(); + let epoch = now - Duration::hours(24)..now; + let mut rewards = HashMap::new(); + rewards.insert( + 0_i32, + ServiceProviderReward { + for_service_provider: dec!(100), + for_promotions: dec!(100), + }, + ); + rewards.insert( + 1_i32, + ServiceProviderReward { + for_service_provider: dec!(100), + for_promotions: dec!(200), + }, + ); + let mut sp_rewards = ServiceProviderRewards { rewards }; + let promotion_rewards = AggregatePromotionRewards { + rewards: vec![ + PromotionRewardShares { + service_provider: 0, + rewardable_entity: Entity::SubscriberId(vec![0]), + shares: 1, + }, + PromotionRewardShares { + service_provider: 1, + rewardable_entity: Entity::SubscriberId(vec![1]), + shares: 1, + }, + ], + }; + let result = aggregate_subcriber_rewards(promotion_rewards.into_rewards( + &mut sp_rewards, + dec!(100), + &epoch, + )); + assert_eq!(sp_rewards.rewards.get(&0).unwrap().for_promotions, dec!(0)); + let result1 = result.get(&vec![0]).unwrap(); + assert_eq!(result1.0, 100); + assert_eq!(result1.1, 33); + let result2 = result.get(&vec![1]).unwrap(); + assert_eq!(result2.0, 200); + assert_eq!(result2.1, 66); + } +} diff --git a/mobile_verifier/src/reward_shares/mod.rs b/mobile_verifier/src/reward_shares/mod.rs index dea03e121..5b4cd5bf0 100644 --- a/mobile_verifier/src/reward_shares/mod.rs +++ b/mobile_verifier/src/reward_shares/mod.rs @@ -299,16 +299,6 @@ pub struct ServiceProviderShares { pub shares: Vec, } -pub struct ServiceProviderRewards { - pub rewards: HashMap, - pub unallocated_rewards: Decimal, -} - -pub struct ServiceProviderReward { - for_service_provider: Decimal, - for_promotions: Decimal, -} - impl ServiceProviderShares { pub fn new(shares: Vec) -> Self { Self { shares } @@ -353,11 +343,9 @@ impl ServiceProviderShares { pub async fn into_service_provider_rewards( self, - total_sp_rewards: Decimal, reward_per_share: Decimal, solana: &impl SolanaNetwork, ) -> anyhow::Result { - let mut allocated_sp_rewards = Decimal::ZERO; let mut rewards = HashMap::new(); for share in self.shares.into_iter() { @@ -365,14 +353,13 @@ impl ServiceProviderShares { if total.is_zero() { continue; } - allocated_sp_rewards += total; let percent_for_promotion_rewards = solana .fetch_incentive_escrow_fund_percent(service_provider_id_to_carrier_name( share.service_provider, )) .await?; rewards.insert( - share.service_provider, + share.service_provider as i32, ServiceProviderReward { for_promotions: total * percent_for_promotion_rewards, for_service_provider: total - total * percent_for_promotion_rewards, @@ -380,10 +367,7 @@ impl ServiceProviderShares { ); } - Ok(ServiceProviderRewards { - rewards, - unallocated_rewards: total_sp_rewards - allocated_sp_rewards, - }) + Ok(ServiceProviderRewards { rewards }) } fn maybe_cap_service_provider_rewards( @@ -415,6 +399,15 @@ impl ServiceProviderShares { } } +pub struct ServiceProviderRewards { + pub rewards: HashMap, +} + +pub struct ServiceProviderReward { + pub for_service_provider: Decimal, + pub for_promotions: Decimal, +} + impl ServiceProviderRewards { pub fn get_total_rewards(&self) -> Decimal { self.rewards @@ -430,7 +423,7 @@ impl ServiceProviderRewards { /// Take the rewards allocated for promotion from a service provider, leaving none /// left. If any rewards allocated for promotion are left by the time we call /// into_mobile_reward_share, they will be converted to service provider rewards. - pub fn take_rewards_allocated_for_promotion(&mut self, sp: &ServiceProvider) -> Decimal { + pub fn take_rewards_allocated_for_promotion(&mut self, sp: &i32) -> Decimal { if let Some(ref mut rewards) = self.rewards.get_mut(sp) { std::mem::take(&mut rewards.for_promotions) } else { @@ -449,7 +442,7 @@ impl ServiceProviderRewards { end_period: reward_period.end.encode_timestamp(), reward: Some(ProtoReward::ServiceProviderReward( proto::ServiceProviderReward { - service_provider_id: service_provider_id as i32, + service_provider_id, amount: (reward.for_promotions + reward.for_service_provider) .round_dp_with_strategy(0, RoundingStrategy::ToZero) .to_u64() @@ -2399,7 +2392,7 @@ mod test { let mut sp_rewards = HashMap::::new(); let mut allocated_sp_rewards = 0_u64; for sp_reward in sp_shares - .into_service_provider_rewards(total_sp_rewards, rewards_per_share, &None) + .into_service_provider_rewards(rewards_per_share, &None) .await .unwrap() .into_mobile_reward_shares(&epoch) @@ -2449,7 +2442,7 @@ mod test { let mut sp_rewards = HashMap::new(); let mut allocated_sp_rewards = 0_u64; for sp_reward in sp_shares - .into_service_provider_rewards(total_sp_rewards_in_bones, rewards_per_share, &None) + .into_service_provider_rewards(rewards_per_share, &None) .await .unwrap() .into_mobile_reward_shares(&epoch) @@ -2498,7 +2491,7 @@ mod test { let mut sp_rewards = HashMap::new(); let mut allocated_sp_rewards = 0_u64; for sp_reward in sp_shares - .into_service_provider_rewards(total_sp_rewards_in_bones, rewards_per_share, &None) + .into_service_provider_rewards(rewards_per_share, &None) .await .unwrap() .into_mobile_reward_shares(&epoch) @@ -2548,7 +2541,7 @@ mod test { let mut sp_rewards = HashMap::new(); let mut allocated_sp_rewards = 0_u64; for sp_reward in sp_shares - .into_service_provider_rewards(total_sp_rewards_in_bones, rewards_per_share, &None) + .into_service_provider_rewards(rewards_per_share, &None) .await .unwrap() .into_mobile_reward_shares(&epoch) diff --git a/mobile_verifier/src/rewarder/mod.rs b/mobile_verifier/src/rewarder/mod.rs index c20ef2c40..58a6e15b1 100644 --- a/mobile_verifier/src/rewarder/mod.rs +++ b/mobile_verifier/src/rewarder/mod.rs @@ -597,7 +597,7 @@ pub async fn reward_service_providers( ); let rewards_per_share = sp_shares.rewards_per_share(total_sp_rewards, mobile_bone_price)?; let mut sp_rewards = sp_shares - .into_service_provider_rewards(total_sp_rewards, rewards_per_share, solana) + .into_service_provider_rewards(rewards_per_share, solana) .await?; let unallocated_sp_rewards = total_sp_rewards - sp_rewards.get_total_rewards(); let agg_promotion_rewards = AggregatePromotionRewards::aggregate(pool, reward_period).await?;