From 5d207fb2c64b6351087e6e9e29539d0bfdf821d7 Mon Sep 17 00:00:00 2001 From: Bryan Stitt Date: Wed, 12 Jul 2023 10:24:16 -0700 Subject: [PATCH] Auto tier change (#184) * lint * change user to premium on admin credits todo: change on other deposits * set tier more places * BadRequest instead of 500 * insert existing users too * add the premium file --- entities/src/user.rs | 2 +- web3_proxy/src/balance.rs | 2 + web3_proxy/src/caches.rs | 2 +- web3_proxy/src/frontend/admin.rs | 10 ++- web3_proxy/src/frontend/users/payment.rs | 19 +++++- .../src/frontend/users/payment_stripe.rs | 11 +++- web3_proxy/src/lib.rs | 1 + web3_proxy/src/premium.rs | 64 +++++++++++++++++++ web3_proxy/tests/test_admins.rs | 4 +- web3_proxy/tests/test_sum_credits_used.rs | 14 +--- web3_proxy/tests/test_users.rs | 33 +--------- 11 files changed, 108 insertions(+), 54 deletions(-) create mode 100644 web3_proxy/src/premium.rs diff --git a/entities/src/user.rs b/entities/src/user.rs index 1cbe7815..ee957c0b 100644 --- a/entities/src/user.rs +++ b/entities/src/user.rs @@ -4,7 +4,7 @@ use crate::serialization; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, Hash, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "user")] pub struct Model { #[sea_orm(primary_key)] diff --git a/web3_proxy/src/balance.rs b/web3_proxy/src/balance.rs index b9f03a9b..4db43ea5 100644 --- a/web3_proxy/src/balance.rs +++ b/web3_proxy/src/balance.rs @@ -62,6 +62,8 @@ impl Balance { } pub fn was_ever_premium(&self) -> bool { + // TODO: technically we should also check that user_tier.downgrade_tier_id.is_some() + // but now we set premium automatically on deposit, so its fine for now self.user_id != 0 && self.total_deposits() >= Decimal::from(10) } diff --git a/web3_proxy/src/caches.rs b/web3_proxy/src/caches.rs index ebc1e291..ab5c7695 100644 --- a/web3_proxy/src/caches.rs +++ b/web3_proxy/src/caches.rs @@ -4,7 +4,7 @@ use crate::frontend::authorization::{AuthorizationChecks, RpcSecretKey}; use derive_more::From; use entities::rpc_key; use migration::sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; -use moka::future::{Cache, ConcurrentCacheExt}; +use moka::future::Cache; use std::fmt; use std::net::IpAddr; use std::sync::Arc; diff --git a/web3_proxy/src/frontend/admin.rs b/web3_proxy/src/frontend/admin.rs index 286fb63f..a8064858 100644 --- a/web3_proxy/src/frontend/admin.rs +++ b/web3_proxy/src/frontend/admin.rs @@ -6,6 +6,7 @@ use crate::app::Web3ProxyApp; use crate::errors::Web3ProxyResponse; use crate::errors::{Web3ProxyError, Web3ProxyErrorContext}; use crate::frontend::users::authentication::PostLogin; +use crate::premium::{get_user_and_tier_from_address, grant_premium_tier}; use crate::user_token::UserBearerToken; use axum::{ extract::{Path, Query}, @@ -65,14 +66,16 @@ pub async fn admin_increase_balance( .await? .ok_or_else(|| Web3ProxyError::AccessDenied("not an admin".into()))?; - let user_entry: user::Model = user::Entity::find() - .filter(user::Column::Address.eq(payload.user_address.as_bytes())) - .one(&txn) + let (user_entry, user_tier_entry) = get_user_and_tier_from_address(&payload.user_address, &txn) .await? .ok_or(Web3ProxyError::BadRequest( format!("No user found with {:?}", payload.user_address).into(), ))?; + grant_premium_tier(&user_entry, user_tier_entry.as_ref(), &txn) + .await + .web3_context("granting premium tier")?; + let increase_balance_receipt = admin_increase_balance_receipt::ActiveModel { amount: sea_orm::Set(payload.amount), admin_id: sea_orm::Set(admin_entry.id), @@ -81,6 +84,7 @@ pub async fn admin_increase_balance( ..Default::default() }; increase_balance_receipt.save(&txn).await?; + txn.commit().await?; // Invalidate the user_balance_cache for this user: diff --git a/web3_proxy/src/frontend/users/payment.rs b/web3_proxy/src/frontend/users/payment.rs index 97173489..eb22314f 100644 --- a/web3_proxy/src/frontend/users/payment.rs +++ b/web3_proxy/src/frontend/users/payment.rs @@ -1,10 +1,11 @@ use crate::app::Web3ProxyApp; use crate::balance::Balance; -use crate::errors::{Web3ProxyError, Web3ProxyResponse, Web3ProxyResult}; +use crate::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResponse, Web3ProxyResult}; use crate::frontend::authorization::{ login_is_authorized, Authorization as Web3ProxyAuthorization, }; use crate::frontend::users::authentication::register_new_user; +use crate::premium::grant_premium_tier; use anyhow::Context; use axum::{ extract::Path, @@ -16,7 +17,7 @@ use axum_client_ip::InsecureClientIp; use axum_macros::debug_handler; use entities::{ admin_increase_balance_receipt, increase_on_chain_balance_receipt, - stripe_increase_balance_receipt, user, + stripe_increase_balance_receipt, user, user_tier, }; use ethers::abi::AbiEncode; use ethers::types::{Address, Block, TransactionReceipt, TxHash, H256}; @@ -302,6 +303,8 @@ pub async fn user_balance_post( // TODO: check bloom filters + let mut user_ids_need_premium = HashSet::new(); + // the transaction might contain multiple relevant logs. collect them all let mut response_data = vec![]; let mut user_ids_to_invalidate = HashSet::new(); @@ -405,9 +408,21 @@ pub async fn user_balance_post( debug!("deposit data: {:#?}", x); response_data.push(x); + + user_ids_need_premium.insert(recipient); } } + for user in user_ids_need_premium.into_iter() { + let user_tier = user_tier::Entity::find_by_id(user.user_tier_id) + .one(&txn) + .await?; + + grant_premium_tier(&user, user_tier.as_ref(), &txn) + .await + .web3_context("granting premium tier")?; + } + txn.commit().await?; for user_id in user_ids_to_invalidate.into_iter() { diff --git a/web3_proxy/src/frontend/users/payment_stripe.rs b/web3_proxy/src/frontend/users/payment_stripe.rs index 9a4dcd28..819e58a1 100644 --- a/web3_proxy/src/frontend/users/payment_stripe.rs +++ b/web3_proxy/src/frontend/users/payment_stripe.rs @@ -1,5 +1,6 @@ use crate::app::Web3ProxyApp; use crate::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResponse}; +use crate::premium::grant_premium_tier; use anyhow::Context; use axum::{ headers::{authorization::Bearer, Authorization}, @@ -7,7 +8,7 @@ use axum::{ Extension, Json, TypedHeader, }; use axum_macros::debug_handler; -use entities::{stripe_increase_balance_receipt, user}; +use entities::{stripe_increase_balance_receipt, user, user_tier}; use ethers::types::Address; use http::HeaderMap; use migration::sea_orm::prelude::Decimal; @@ -163,6 +164,14 @@ pub async fn user_balance_stripe_post( Some(recipient) => { let _ = insert_receipt_model.save(&txn).await; + let user_tier = user_tier::Entity::find_by_id(recipient.user_tier_id) + .one(&txn) + .await?; + + grant_premium_tier(&recipient, user_tier.as_ref(), &txn) + .await + .web3_context("granting premium tier")?; + txn.commit().await?; // Finally invalidate the cache as well diff --git a/web3_proxy/src/lib.rs b/web3_proxy/src/lib.rs index d4744dea..6919a8b8 100644 --- a/web3_proxy/src/lib.rs +++ b/web3_proxy/src/lib.rs @@ -14,6 +14,7 @@ pub mod frontend; pub mod http_params; pub mod jsonrpc; pub mod pagerduty; +pub mod premium; pub mod prometheus; pub mod referral_code; pub mod relational_db; diff --git a/web3_proxy/src/premium.rs b/web3_proxy/src/premium.rs new file mode 100644 index 00000000..a0828755 --- /dev/null +++ b/web3_proxy/src/premium.rs @@ -0,0 +1,64 @@ +use crate::errors::Web3ProxyResult; +use anyhow::Context; +use entities::{user, user_tier}; +use ethers::prelude::Address; +use migration::sea_orm::{ + self, ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, + QueryFilter, +}; +use tracing::info; + +pub async fn get_user_and_tier_from_address( + user_address: &Address, + txn: &DatabaseTransaction, +) -> Web3ProxyResult)>> { + let x = user::Entity::find() + .filter(user::Column::Address.eq(user_address.as_bytes())) + .find_also_related(user_tier::Entity) + .one(txn) + .await?; + + Ok(x) +} + +pub async fn get_user_and_tier_from_id( + user_id: u64, + txn: &DatabaseTransaction, +) -> Web3ProxyResult)>> { + let x = user::Entity::find_by_id(user_id) + .find_also_related(user_tier::Entity) + .one(txn) + .await?; + + Ok(x) +} + +/// TODO: improve this so that funding an account that has an "unlimited" key is left alone +pub async fn grant_premium_tier( + user: &user::Model, + user_tier: Option<&user_tier::Model>, + txn: &DatabaseTransaction, +) -> Web3ProxyResult<()> { + if user_tier.is_none() || user_tier.and_then(|x| x.downgrade_tier_id).is_none() { + if user_tier.map(|x| x.title.as_str()) == Some("Premium") { + // user is already premium + } else { + info!("upgrading {} to Premium", user.id); + + // switch the user to the premium tier + let new_user_tier = user_tier::Entity::find() + .filter(user_tier::Column::Title.like("Premium")) + .one(txn) + .await? + .context("premium tier not found")?; + + let mut user = user.clone().into_active_model(); + + user.user_tier_id = sea_orm::Set(new_user_tier.id); + + user.save(txn).await?; + } + } + + Ok(()) +} diff --git a/web3_proxy/tests/test_admins.rs b/web3_proxy/tests/test_admins.rs index a215bad5..99da9ed1 100644 --- a/web3_proxy/tests/test_admins.rs +++ b/web3_proxy/tests/test_admins.rs @@ -5,7 +5,7 @@ use std::time::Duration; use crate::common::admin_increases_balance::admin_increase_balance; use crate::common::create_admin::create_user_as_admin; -use crate::common::create_user::{create_user, set_user_tier}; +use crate::common::create_user::create_user; use crate::common::user_balance::user_get_balance; use crate::common::TestApp; use migration::sea_orm::prelude::Decimal; @@ -39,8 +39,6 @@ async fn test_admin_grant_credits() { let admin_login_response = create_user_as_admin(&x, &r, &admin_wallet).await; info!(?admin_login_response); - set_user_tier(&x, user_login_response.user.clone(), "Premium").await.unwrap(); - let increase_balance_response = admin_increase_balance( &x, &r, diff --git a/web3_proxy/tests/test_sum_credits_used.rs b/web3_proxy/tests/test_sum_credits_used.rs index 67b4ab76..9df25314 100644 --- a/web3_proxy/tests/test_sum_credits_used.rs +++ b/web3_proxy/tests/test_sum_credits_used.rs @@ -1,12 +1,8 @@ mod common; use crate::common::{ - admin_increases_balance::admin_increase_balance, - create_admin::create_user_as_admin, - create_user::{create_user, set_user_tier}, - rpc_key::user_get_provider, - user_balance::user_get_balance, - TestApp, + admin_increases_balance::admin_increase_balance, create_admin::create_user_as_admin, + create_user::create_user, rpc_key::user_get_provider, user_balance::user_get_balance, TestApp, }; use ethers::prelude::U64; use migration::sea_orm::prelude::Decimal; @@ -33,12 +29,6 @@ async fn test_sum_credits_used() { let admin_login_response = create_user_as_admin(&x, &r, &admin_wallet).await; let user_login_response = create_user(&x, &r, &user_wallet, None).await; - set_user_tier(&x, user_login_response.user.clone(), "Premium") - .await - .unwrap(); - - // TODO: set the user's user_id to the "Premium" tier - info!("starting balance"); let balance: Balance = user_get_balance(&x, &r, &user_login_response).await; assert_eq!( diff --git a/web3_proxy/tests/test_users.rs b/web3_proxy/tests/test_users.rs index d25f47f4..a332fff1 100644 --- a/web3_proxy/tests/test_users.rs +++ b/web3_proxy/tests/test_users.rs @@ -3,7 +3,7 @@ mod common; use crate::common::admin_deposits::get_admin_deposits; use crate::common::admin_increases_balance::admin_increase_balance; use crate::common::create_admin::create_user_as_admin; -use crate::common::create_user::{create_user, set_user_tier}; +use crate::common::create_user::create_user; use crate::common::referral::{ get_referral_code, get_shared_referral_codes, get_used_referral_codes, UserSharedReferralInfo, UserUsedReferralInfo, @@ -105,10 +105,6 @@ async fn test_admin_balance_increase() { let admin_login_response = create_user_as_admin(&x, &r, &admin_wallet).await; let user_login_response = create_user(&x, &r, &user_wallet, None).await; - set_user_tier(&x, user_login_response.user.clone(), "Premium") - .await - .unwrap(); - // Bump both user's wallet to $20 admin_increase_balance( &x, @@ -156,10 +152,6 @@ async fn test_user_balance_decreases() { let admin_login_response = create_user_as_admin(&x, &r, &admin_wallet).await; let user_login_response = create_user(&x, &r, &user_wallet, None).await; - set_user_tier(&x, user_login_response.user.clone(), "Premium") - .await - .unwrap(); - // Get the rpc keys for this user let rpc_keys: RpcKey = user_get_first_rpc_key(&x, &r, &user_login_response).await; let proxy_endpoint = format!("{}rpc/{}", x.proxy_provider.url(), rpc_keys.secret_key); @@ -267,14 +259,7 @@ async fn test_referral_bonus_non_concurrent() { let user_login_response = create_user(&x, &r, &user_wallet, Some(referral_link.clone())).await; - set_user_tier(&x, referrer_login_response.user.clone(), "Premium") - .await - .unwrap(); - set_user_tier(&x, user_login_response.user.clone(), "Premium") - .await - .unwrap(); - - // Bump both user's wallet to $20 + // Bump both user's wallet to $20 (which will give them the Premium user tier) admin_increase_balance( &x, &r, @@ -417,13 +402,6 @@ async fn test_referral_bonus_concurrent_referrer_only() { let user_login_response = create_user(&x, &r, &user_wallet, Some(referral_link.clone())).await; - set_user_tier(&x, referrer_login_response.user.clone(), "Premium") - .await - .unwrap(); - set_user_tier(&x, user_login_response.user.clone(), "Premium") - .await - .unwrap(); - // Bump both user's wallet to $20 admin_increase_balance( &x, @@ -578,13 +556,6 @@ async fn test_referral_bonus_concurrent_referrer_and_user() { let user_login_response = create_user(&x, &r, &user_wallet, Some(referral_link.clone())).await; - set_user_tier(&x, referrer_login_response.user.clone(), "Premium") - .await - .unwrap(); - set_user_tier(&x, user_login_response.user.clone(), "Premium") - .await - .unwrap(); - // Bump both user's wallet to $20 admin_increase_balance( &x,