use serde::Deserialize; use std::str::FromStr; use std::time::Duration; use tracing::{debug, info, trace}; use web3_proxy::frontend::users::authentication::PostLogin; use web3_proxy::prelude::ethers::prelude::{Http, Provider}; use web3_proxy::prelude::ethers::{signers::Signer, types::Signature}; use web3_proxy::prelude::migration::sea_orm::prelude::Decimal; use web3_proxy::prelude::reqwest; use web3_proxy::prelude::tokio; use web3_proxy::prelude::ulid::Ulid; use web3_proxy::rpcs::blockchain::ArcBlock; use web3_proxy_cli::test_utils::admin_deposits::get_admin_deposits; use web3_proxy_cli::test_utils::admin_increases_balance::admin_increase_balance; use web3_proxy_cli::test_utils::create_admin::create_user_as_admin; use web3_proxy_cli::test_utils::create_user::create_user; use web3_proxy_cli::test_utils::referral::{ get_referral_code, get_shared_referral_codes, get_used_referral_codes, UserSharedReferralInfo, UserUsedReferralInfo, }; use web3_proxy_cli::test_utils::rpc_key::{user_get_first_rpc_key, RpcKey}; use web3_proxy_cli::test_utils::user_balance::user_get_balance; use web3_proxy_cli::test_utils::TestAnvil; use web3_proxy_cli::test_utils::TestApp; use web3_proxy_cli::test_utils::TestMysql; /// TODO: use this type in the frontend #[derive(Debug, Deserialize)] struct LoginPostResponse { pub bearer_token: Ulid, // pub rpc_keys: Value, // /// unknown data gets put here // #[serde(flatten, default = "HashMap::default")] // pub extra: HashMap, } /// TODO: 191 and the other message formats in another test #[cfg_attr(not(feature = "tests-needing-docker"), ignore)] #[test_log::test(tokio::test)] async fn test_log_in_and_out() { let a = TestAnvil::spawn(31337).await; let db = TestMysql::spawn().await; let x = TestApp::spawn(&a, Some(&db), None, None).await; let r = reqwest::Client::new(); let w = a.wallet(0); let login_get_url = format!("{}user/login/{:?}", x.proxy_provider.url(), w.address()); let login_message = r.get(login_get_url).send().await.unwrap(); let login_message = login_message.text().await.unwrap(); // sign the message and POST it let signed: Signature = w.sign_message(&login_message).await.unwrap(); trace!(?signed); let post_login_data = PostLogin { msg: login_message, sig: signed.to_string(), referral_code: None, }; debug!(?post_login_data); let login_post_url = format!("{}user/login", x.proxy_provider.url()); let login_response = r .post(login_post_url) .json(&post_login_data) .send() .await .unwrap() .json::() .await .unwrap(); info!(?login_response); // use the bearer token to log out let logout_post_url = format!("{}user/logout", x.proxy_provider.url()); let logout_response = r .post(logout_post_url) .bearer_auth(login_response.bearer_token) .send() .await .unwrap() .text() .await .unwrap(); info!(?logout_response); assert_eq!(logout_response, "goodbye"); } #[cfg_attr(not(feature = "tests-needing-docker"), ignore)] #[test_log::test(tokio::test)] async fn test_admin_balance_increase() { info!("Starting admin can increase balance"); let a: TestAnvil = TestAnvil::spawn(31337).await; let db = TestMysql::spawn().await; let x = TestApp::spawn(&a, Some(&db), None, None).await; let r = reqwest::Client::builder() .timeout(Duration::from_secs(20)) .build() .unwrap(); let user_wallet = a.wallet(0); let admin_wallet = a.wallet(1); // Create three users, one referrer, one admin who bumps both their balances let admin_login_response = create_user_as_admin(&x, &db, &r, &admin_wallet).await; let user_login_response = create_user(&x, &r, &user_wallet, None).await; // Bump both user's wallet to $20 admin_increase_balance( &x, &r, &admin_login_response, &user_wallet, Decimal::from(20), ) .await; info!("Getting admin deposits"); let response = get_admin_deposits(&x, &r, &user_login_response).await; info!(?response); assert_eq!( Decimal::from_str( response["deposits"].get(0).unwrap()["amount"] .as_str() .unwrap() ) .unwrap(), Decimal::from(20) ); assert_eq!( response["deposits"].get(0).unwrap()["note"] .as_str() .unwrap(), "Test increasing balance" ); } #[cfg_attr(not(feature = "tests-needing-docker"), ignore)] #[test_log::test(tokio::test)] async fn test_user_balance_decreases() { info!("Starting balance decreases with usage test"); let a: TestAnvil = TestAnvil::spawn(31337).await; let db = TestMysql::spawn().await; let x = TestApp::spawn(&a, Some(&db), None, None).await; let r = reqwest::Client::builder() .timeout(Duration::from_secs(20)) .build() .unwrap(); let user_wallet = a.wallet(0); let admin_wallet = a.wallet(1); // Create three users, one referrer, one admin who bumps both their balances let admin_login_response = create_user_as_admin(&x, &db, &r, &admin_wallet).await; let user_login_response = create_user(&x, &r, &user_wallet, None).await; // 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); let proxy_provider = Provider::::try_from(proxy_endpoint).unwrap(); // Make some requests while in the free tier, so we can test bookkeeping here for _ in 1..=10_000 { let _ = proxy_provider .request::<_, Option>("eth_getBlockByNumber", ("latest", false)) .await .unwrap() .unwrap(); } // Flush all stats here let flushed = x.flush_stats_and_wait().await.unwrap(); info!(?flushed); // assert_eq!(flush_count.timeseries, 0); // assert!(flush_count.relational > 0); // Check the balance, it should not have decreased; there should have been accounted free credits, however let user_balance = user_get_balance(&x, &r, &user_login_response).await; // Check that the balance is 0 assert_eq!(user_balance.remaining(), Decimal::from(0)); // Check that paid credits is 0 (because balance is 0) assert_eq!(user_balance.total_spent_paid_credits, Decimal::from(0)); // Check that paid credits is 0 (because balance is 0) assert_eq!(user_balance.total_deposits(), Decimal::from(0)); // Check that total credits incl free used is larger than 0 let previously_free_spent = user_balance.total_spent; assert!(previously_free_spent > Decimal::from(0)); // Bump both user's wallet to $20 admin_increase_balance( &x, &r, &admin_login_response, &user_wallet, Decimal::from(20), ) .await; let user_balance_response = user_get_balance(&x, &r, &user_login_response).await; let user_balance_pre = user_balance_response.remaining(); assert_eq!(user_balance_pre, Decimal::from(20)); for _ in 1..=10_000 { let _ = proxy_provider .request::<_, Option>("eth_getBlockByNumber", ("latest", false)) .await .unwrap() .unwrap(); } // Flush all stats here let flushed = x.flush_stats_and_wait().await.unwrap(); info!(?flushed); // assert_eq!(flush_count.timeseries, 0); // assert!(flush_count.relational == 1); // Deposits should not be affected, and should be equal to what was initially provided let user_balance = user_get_balance(&x, &r, &user_login_response).await; let total_deposits = user_balance.total_deposits(); assert_eq!(total_deposits, Decimal::from(20)); // Check that total_spent_paid credits is equal to total_spent, because we are all still inside premium assert_eq!( user_balance.total_spent_paid_credits + previously_free_spent, user_balance.total_spent, ); // Get the full balance endpoint let user_balance_post = user_balance.remaining(); assert!(user_balance_post < user_balance_pre); // 10k while free, 10k while premium assert_eq!(user_balance.total_frontend_requests, 20_000); // Balance should be total deposits - usage while in the paid tier let total_spent_in_paid_credits = user_balance.total_spent_paid_credits; assert_eq!( total_deposits - total_spent_in_paid_credits, user_balance_post ); // This should never be negative assert!(user_balance.total_spent > Decimal::from(0)); } #[cfg_attr(not(feature = "tests-needing-docker"), ignore)] #[test_log::test(tokio::test)] async fn test_referral_bonus_non_concurrent() { info!("Starting referral bonus test"); let a: TestAnvil = TestAnvil::spawn(31337).await; let db = TestMysql::spawn().await; let x = TestApp::spawn(&a, Some(&db), None, None).await; let r = reqwest::Client::builder() .timeout(Duration::from_secs(20)) .build() .unwrap(); let user_wallet = a.wallet(0); let referrer_wallet = a.wallet(1); let admin_wallet = a.wallet(2); // Create three users, one referrer, one admin who bumps both their balances let referrer_login_response = create_user(&x, &r, &referrer_wallet, None).await; let admin_login_response = create_user_as_admin(&x, &db, &r, &admin_wallet).await; // Get the first user's referral link let referral_link = get_referral_code(&x, &r, &referrer_login_response).await; let user_login_response = create_user(&x, &r, &user_wallet, Some(referral_link.clone())).await; // Bump both user's wallet to $20 (which will give them the Premium user tier) admin_increase_balance( &x, &r, &admin_login_response, &user_wallet, Decimal::from(20), ) .await; admin_increase_balance( &x, &r, &admin_login_response, &referrer_wallet, Decimal::from(20), ) .await; // Get balance before for both users let user_balance_response = user_get_balance(&x, &r, &user_login_response).await; let user_balance_pre = user_balance_response.remaining(); let referrer_balance_response = user_get_balance(&x, &r, &referrer_login_response).await; let referrer_balance_pre = referrer_balance_response.remaining(); // Make sure they both have balance now assert_eq!(user_balance_pre, Decimal::from(20)); assert_eq!(referrer_balance_pre, Decimal::from(20)); // Setup variables that will be used let shared_referral_code: UserSharedReferralInfo = get_shared_referral_codes(&x, &r, &referrer_login_response).await; let used_referral_code: UserUsedReferralInfo = get_used_referral_codes(&x, &r, &user_login_response).await; // assert that the used referral code is used assert_eq!( format!("{:?}", user_wallet.address()), shared_referral_code .clone() .referrals .first() .unwrap() .referred_address .clone() .unwrap() ); assert_eq!( referral_link.clone(), used_referral_code .clone() .referrals .first() .unwrap() .used_referral_code .clone() .unwrap() ); // Make a for-loop just spam it a bit // Make a JSON request let rpc_keys: RpcKey = user_get_first_rpc_key(&x, &r, &user_login_response).await; info!("Rpc key is: {:?}", rpc_keys); let proxy_endpoint = format!("{}rpc/{}", x.proxy_provider.url(), rpc_keys.secret_key); let proxy_provider = Provider::::try_from(proxy_endpoint).unwrap(); for _ in 1..=20_000 { let _proxy_result = proxy_provider .request::<_, Option>("eth_getBlockByNumber", ("latest", false)) .await .unwrap() .unwrap(); } // Flush all stats here let flushed = x.flush_stats_and_wait().await.unwrap(); info!(?flushed); // we can't assert because the intervals might flush for us // assert_eq!(flush_count.timeseries, 0); // assert!(flush_count.relational > 0); // Check that at least something was earned: let shared_referral_code: UserSharedReferralInfo = get_shared_referral_codes(&x, &r, &referrer_login_response).await; info!(referrals=?shared_referral_code.referrals.first().unwrap(), "Referral code"); let user_balance = user_get_balance(&x, &r, &user_login_response).await; // first, make sure that 20k requests were saved to the db assert_eq!(user_balance.total_frontend_requests, 20_000); // We make sure that the referrer has $10 + 10% of the used balance // The admin provides credits for both let user_balance_post = user_balance.remaining(); let referrer_balance = user_get_balance(&x, &r, &referrer_login_response).await; let referrer_balance_post = referrer_balance.remaining(); info!( "Balances before and after are (user): {:?} {:?}", user_balance_pre, user_balance_post ); info!( "Balances before and after are (referrer): {:?} {:?}", referrer_balance_pre, referrer_balance_post ); let difference = user_balance_pre - user_balance_post; // Make sure that the pre and post balance is not the same (i.e. some change has occurred) assert_ne!( user_balance_pre, user_balance_post, "Pre and post balance is equivalent" ); assert!(user_balance_pre > user_balance_post); assert!(referrer_balance_pre < referrer_balance_post); // Finally, make sure that referrer has received 10$ of balances assert_eq!( referrer_balance_pre + difference / Decimal::from(10), referrer_balance_post ); } #[cfg_attr(not(feature = "tests-needing-docker"), ignore)] #[test_log::test(tokio::test)] async fn test_referral_bonus_concurrent_referrer_only() { info!("Starting referral bonus test"); let a = TestAnvil::spawn(31337).await; let db = TestMysql::spawn().await; let x = TestApp::spawn(&a, Some(&db), None, None).await; let r = reqwest::Client::builder() .timeout(Duration::from_secs(20)) .build() .unwrap(); let user_wallet = a.wallet(0); let referrer_wallet = a.wallet(1); let admin_wallet = a.wallet(2); // Create three users, one referrer, one admin who bumps both their balances let referrer_login_response = create_user(&x, &r, &referrer_wallet, None).await; let admin_login_response = create_user_as_admin(&x, &db, &r, &admin_wallet).await; // Get the first user's referral link let referral_link = get_referral_code(&x, &r, &referrer_login_response).await; let user_login_response = create_user(&x, &r, &user_wallet, Some(referral_link.clone())).await; // Bump both user's wallet to $20 admin_increase_balance( &x, &r, &admin_login_response, &user_wallet, Decimal::from(20), ) .await; admin_increase_balance( &x, &r, &admin_login_response, &referrer_wallet, Decimal::from(20), ) .await; // Get balance before for both users let user_balance_response = user_get_balance(&x, &r, &user_login_response).await; let user_balance_pre = user_balance_response.remaining(); let referrer_balance_response = user_get_balance(&x, &r, &referrer_login_response).await; let referrer_balance_pre = referrer_balance_response.remaining(); // Make sure they both have balance now assert_eq!(user_balance_pre, Decimal::from(20)); assert_eq!(referrer_balance_pre, Decimal::from(20)); // Setup variables that will be used let shared_referral_code: UserSharedReferralInfo = get_shared_referral_codes(&x, &r, &referrer_login_response).await; let used_referral_code: UserUsedReferralInfo = get_used_referral_codes(&x, &r, &user_login_response).await; // assert that the used referral code is used assert_eq!( format!("{:?}", user_wallet.address()), shared_referral_code .clone() .referrals .first() .unwrap() .referred_address .clone() .unwrap() ); assert_eq!( referral_link.clone(), used_referral_code .clone() .referrals .first() .unwrap() .used_referral_code .clone() .unwrap() ); // Make a for-loop just spam it a bit // Make a JSON request let rpc_keys: RpcKey = user_get_first_rpc_key(&x, &r, &user_login_response).await; info!("Rpc key is: {:?}", rpc_keys); // Spawn many requests proxy_providers let number_requests = 100; // Spin up concurrent requests ... let mut handles = Vec::with_capacity(number_requests); for _ in 0..number_requests { let url = x.proxy_provider.url().clone(); let secret_key = rpc_keys.secret_key; handles.push(tokio::spawn(async move { let proxy_endpoint = format!("{}rpc/{}", url, secret_key); let proxy_provider = Provider::::try_from(proxy_endpoint).unwrap(); proxy_provider .request::<_, Option>("eth_getBlockByNumber", ("latest", false)) .await .unwrap() .unwrap() })); } let mut results = Vec::with_capacity(handles.len()); for handle in handles { results.push(handle.await.unwrap()); } // Flush all stats here let flushed = x.flush_stats_and_wait().await.unwrap(); info!(?flushed); // assert_eq!(flush_count.timeseries, 0); // assert!(flush_count.relational > 0); // Check that at least something was earned: let shared_referral_code: UserSharedReferralInfo = get_shared_referral_codes(&x, &r, &referrer_login_response).await; info!("Referral code"); info!("{:?}", shared_referral_code.referrals.first().unwrap()); // We make sure that the referrer has $10 + 10% of the used balance // The admin provides credits for both let user_balance_response = user_get_balance(&x, &r, &user_login_response).await; let user_balance_post = user_balance_response.remaining(); let referrer_balance_response = user_get_balance(&x, &r, &referrer_login_response).await; let referrer_balance_post = referrer_balance_response.remaining(); info!( "Balances before and after are (user): {:?} {:?}", user_balance_pre, user_balance_post ); info!( "Balances before and after are (referrer): {:?} {:?}", referrer_balance_pre, referrer_balance_post ); let difference = user_balance_pre - user_balance_post; // Make sure that the pre and post balance is not the same (i.e. some change has occurred) assert_ne!( user_balance_pre, user_balance_post, "Pre and post balance is equivalent" ); assert!(user_balance_pre > user_balance_post); assert!(referrer_balance_pre < referrer_balance_post); // Finally, make sure that referrer has received 10$ of balances assert_eq!( referrer_balance_pre + difference / Decimal::from(10), referrer_balance_post ); // drop x first to avoid spurious warnings about anvil/influx/mysql shutting down before the app drop(x); } #[cfg_attr(not(feature = "tests-needing-docker"), ignore)] #[test_log::test(tokio::test)] async fn test_referral_bonus_concurrent_referrer_and_user() { info!("Starting referral bonus test"); let a = TestAnvil::spawn(31337).await; let db = TestMysql::spawn().await; let x = TestApp::spawn(&a, Some(&db), None, None).await; let r = reqwest::Client::builder() .timeout(Duration::from_secs(20)) .build() .unwrap(); let user_wallet = a.wallet(0); let referrer_wallet = a.wallet(1); let admin_wallet = a.wallet(2); // Create three users, one referrer, one admin who bumps both their balances let referrer_login_response = create_user(&x, &r, &referrer_wallet, None).await; let admin_login_response = create_user_as_admin(&x, &db, &r, &admin_wallet).await; // Get the first user's referral link let referral_link = get_referral_code(&x, &r, &referrer_login_response).await; let user_login_response = create_user(&x, &r, &user_wallet, Some(referral_link.clone())).await; // Bump both user's wallet to $20 admin_increase_balance( &x, &r, &admin_login_response, &user_wallet, Decimal::from(20), ) .await; admin_increase_balance( &x, &r, &admin_login_response, &referrer_wallet, Decimal::from(20), ) .await; // Get balance before for both users let user_balance_response = user_get_balance(&x, &r, &user_login_response).await; let user_balance_pre = user_balance_response.remaining(); let referrer_balance_response = user_get_balance(&x, &r, &referrer_login_response).await; let referrer_balance_pre = referrer_balance_response.remaining(); // Make sure they both have balance now assert_eq!(user_balance_pre, Decimal::from(20)); assert_eq!(referrer_balance_pre, Decimal::from(20)); // Setup variables that will be used let shared_referral_code: UserSharedReferralInfo = get_shared_referral_codes(&x, &r, &referrer_login_response).await; let used_referral_code: UserUsedReferralInfo = get_used_referral_codes(&x, &r, &user_login_response).await; // assert that the used referral code is used assert_eq!( format!("{:?}", user_wallet.address()), shared_referral_code .clone() .referrals .first() .unwrap() .referred_address .clone() .unwrap() ); assert_eq!( referral_link.clone(), used_referral_code .clone() .referrals .first() .unwrap() .used_referral_code .clone() .unwrap() ); // Make a for-loop just spam it a bit // Make a JSON request let referre_rpc_keys: RpcKey = user_get_first_rpc_key(&x, &r, &referrer_login_response).await; let user_rpc_keys: RpcKey = user_get_first_rpc_key(&x, &r, &user_login_response).await; info!("Rpc key is: {:?}", user_rpc_keys); // Spawn many requests proxy_providers let number_requests = 50; // Spin up concurrent requests ... let mut handles = Vec::with_capacity(number_requests); // Make one request to create the cache; this originates from no user let url = x.proxy_provider.url().clone(); let proxy_endpoint = format!("{}", url); let proxy_provider = Provider::::try_from(proxy_endpoint).unwrap(); let _proxy_result = proxy_provider .request::<_, Option>("eth_getBlockByNumber", ("latest", false)) .await .unwrap() .unwrap(); for _ in 1..number_requests { let url = x.proxy_provider.url().clone(); let user_secret_key = user_rpc_keys.secret_key; handles.push(tokio::spawn(async move { let proxy_endpoint = format!("{}rpc/{}", url, user_secret_key); let proxy_provider = Provider::::try_from(proxy_endpoint).unwrap(); proxy_provider .request::<_, Option>("eth_getBlockByNumber", ("latest", false)) .await .unwrap() .unwrap() })); let url = x.proxy_provider.url().clone(); let referrer_secret_key = referre_rpc_keys.secret_key; handles.push(tokio::spawn(async move { let proxy_endpoint = format!("{}rpc/{}", url, referrer_secret_key); let proxy_provider = Provider::::try_from(proxy_endpoint).unwrap(); proxy_provider .request::<_, Option>("eth_getBlockByNumber", ("latest", false)) .await .unwrap() .unwrap() })); } let mut results = Vec::with_capacity(handles.len()); for handle in handles { results.push(handle.await.unwrap()); } // Flush all stats here let flushed = x.flush_stats_and_wait().await.unwrap(); info!(?flushed); // assert_eq!(flush_count.timeseries, 0); // assert!(flush_count.relational > 0); // Check that at least something was earned: let shared_referral_code: UserSharedReferralInfo = get_shared_referral_codes(&x, &r, &referrer_login_response).await; info!("Referral code"); info!("{:?}", shared_referral_code.referrals.first().unwrap()); // We make sure that the referrer has $10 + 10% of the used balance // The admin provides credits for both let user_balance_response = user_get_balance(&x, &r, &user_login_response).await; let user_balance_post = user_balance_response.remaining(); let referrer_balance_response = user_get_balance(&x, &r, &referrer_login_response).await; let referrer_balance_post = referrer_balance_response.remaining(); info!( "Balances before and after are (user): {:?} {:?}", user_balance_pre, user_balance_post ); info!( "Balances before and after are (referrer): {:?} {:?}", referrer_balance_pre, referrer_balance_post ); let user_difference = user_balance_pre - user_balance_post; let referrer_difference = referrer_balance_pre - referrer_balance_post; // Make sure that the pre and post balance is not the same (i.e. some change has occurred) assert_ne!( user_balance_pre, user_balance_post, "Pre and post balance is equivalent" ); assert!(user_balance_pre > user_balance_post); assert!(referrer_balance_pre > referrer_balance_post); assert!(user_difference > referrer_difference); // In this case, the referrer loses as much as the user spends (because exact same operations), but gains 10% of what the user has spent // I'm also adding 19.9992160000 - 19.9992944000 = 0.0000784 as this is the non-cached requests cost on top (for the user) // Because of 1 cached request, we basically give one unit tolerance assert_eq!( (referrer_balance_pre - user_difference + user_difference / Decimal::from(10)), referrer_balance_post ); // drop x first to avoid spurious warnings about anvil/influx/mysql shutting down before the app drop(x); }