From 29ebf3af1792bc16c84667599045f48fcadd24dd Mon Sep 17 00:00:00 2001 From: rupansh Date: Wed, 6 Mar 2024 00:59:22 +0530 Subject: [PATCH 1/3] feat: initial wallet page implementation --- Cargo.toml | 3 + src/app.rs | 2 + src/component/nav.rs | 14 ++- src/page/mod.rs | 1 + src/page/refer_earn/mod.rs | 2 + src/page/wallet/mod.rs | 135 +++++++++++++++++++++ src/page/wallet/txn.rs | 236 +++++++++++++++++++++++++++++++++++++ 7 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 src/page/wallet/mod.rs create mode 100644 src/page/wallet/txn.rs diff --git a/Cargo.toml b/Cargo.toml index 74cfa43a..5ef6846b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,9 @@ ssr = [ ] # Fetch mock referral history instead of history via canister mock-referral-history = ["rand_chacha", "k256/arithmetic"] +# Fetch mock wallet transactions instead of history via canister +mock-wallet-history = ["rand_chacha"] +mock-history = ["mock-referral-history", "mock-wallet-history"] cloudflare = [] release-bin = ["ssr", "cloudflare"] release-lib = ["hydrate", "cloudflare"] diff --git a/src/app.rs b/src/app.rs index 70cb8d17..97427883 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use crate::{ root::RootPage, terms::TermsOfService, upload::UploadPostPage, + wallet::*, }, state::{ auth::{AuthClient, AuthState}, @@ -62,6 +63,7 @@ pub fn App() -> impl IntoView { + diff --git a/src/component/nav.rs b/src/component/nav.rs index 43094612..d7f17b31 100644 --- a/src/component/nav.rs +++ b/src/component/nav.rs @@ -30,26 +30,30 @@ pub fn NavBar() -> impl IntoView { let path = cur_location.pathname.get(); match path.as_str() { "/" => 0, - "/upload" => 1, - "/menu" => 2, + "/upload" => 2, + "/wallet" => 3, + "/menu" => 4, s if s.starts_with("/hot-or-not") => { home_path.set(path); 0 } - _ => 2, + _ => 4, } }); view! {
+ // TODO: achievements page + - + +
} } diff --git a/src/page/mod.rs b/src/page/mod.rs index ffbac377..a1110293 100644 --- a/src/page/mod.rs +++ b/src/page/mod.rs @@ -10,3 +10,4 @@ pub mod refer_earn; pub mod root; pub mod terms; pub mod upload; +pub mod wallet; diff --git a/src/page/refer_earn/mod.rs b/src/page/refer_earn/mod.rs index 3c270467..a5c98d00 100644 --- a/src/page/refer_earn/mod.rs +++ b/src/page/refer_earn/mod.rs @@ -85,6 +85,8 @@ fn ReferCode() -> impl IntoView { // Is refer id supposed to be individual canister id or user id? + // Is refer id supposed to be individual canister id or user id? + } }) diff --git a/src/page/wallet/mod.rs b/src/page/wallet/mod.rs new file mode 100644 index 00000000..0f4ad919 --- /dev/null +++ b/src/page/wallet/mod.rs @@ -0,0 +1,135 @@ +mod txn; +use leptos::*; + +use crate::{ + component::bullet_loader::BulletLoader, + state::canisters::authenticated_canisters, + try_or_redirect_opt, + utils::{profile::ProfileDetails, MockPartialEq}, +}; +use txn::{ + provider::{get_history_provider, HistoryProvider}, + TxnView, +}; + +#[component] +fn FallbackGreeter() -> impl IntoView { + view! { +
+ Welcome! +
+
+
+ } +} + +#[component] +fn ProfileGreeter(details: ProfileDetails) -> impl IntoView { + view! { +
+ Welcome! + + {details.display_name_or_fallback()} + +
+
+ +
+ } +} + +const RECENT_TXN_CNT: u64 = 10; + +#[component] +fn BalanceFallback() -> impl IntoView { + view! {
} +} + +#[component] +pub fn Wallet() -> impl IntoView { + let canisters = authenticated_canisters(); + let canisters_reader = move || MockPartialEq(canisters.get().and_then(|c| c.transpose())); + let profile_details = create_resource(canisters_reader, move |canisters| async move { + let canisters = try_or_redirect_opt!(canisters.0?); + let user = canisters.authenticated_user(); + let user_details = user.get_profile_details().await.ok()?; + Some(ProfileDetails::from(user_details)) + }); + let balance_resource = create_resource(canisters_reader, move |canisters| async move { + let canisters = try_or_redirect_opt!(canisters.0?); + let user = canisters.authenticated_user(); + let balance = user + .get_utility_token_balance() + .await + .map(|b| b.to_string()) + .unwrap_or("Error".to_string()); + Some(balance) + }); + let history_resource = create_resource(canisters_reader, move |canisters| async move { + let canisters = try_or_redirect_opt!(canisters.0?); + let history_prov = get_history_provider(canisters); + let history = history_prov.get_history(0, RECENT_TXN_CNT).await.ok()?; + + Some(history) + }); + + view! { +
+
+ + {move || { + profile_details + .get() + .flatten() + .map(|details| view! { }) + .unwrap_or_else(|| view! { }) + }} + + +
+
+ Your Coin Balance + + {move || { + balance_resource + .get() + .flatten() + .map(|bal| view! { {bal} }) + .unwrap_or_else(|| { + view! { + + + + } + }) + }} + +
+
+
+ Recent Transactions + // TODO: href + + See All + +
+
+ + {move || { + history_resource + .get() + .flatten() + .map(|history| { + history + .into_iter() + .map(|info| view! { }) + .collect::>() + }) + .unwrap_or_else(|| vec![view! { }]) + }} + +
+
+
+ } +} diff --git a/src/page/wallet/txn.rs b/src/page/wallet/txn.rs new file mode 100644 index 00000000..83202538 --- /dev/null +++ b/src/page/wallet/txn.rs @@ -0,0 +1,236 @@ +use std::fmt::{self, Display, Formatter}; + +use leptos::*; +use leptos_icons::Icon; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy)] +pub enum TxnDirection { + Bonus, + Added, + Deducted, +} + +impl TxnDirection { + fn positive(self) -> bool { + use TxnDirection::*; + match self { + Bonus => true, + Added => true, + Deducted => false, + } + } +} + +impl From for &'static icondata_core::IconData { + fn from(val: TxnDirection) -> Self { + use TxnDirection::*; + match val { + Bonus => icondata::AiPlusCircleOutlined, + Added => icondata::AiUpCircleOutlined, + Deducted => icondata::AiDownCircleOutlined, + } + } +} + +#[derive(Clone, Copy, Serialize, Deserialize)] +pub enum TxnTag { + BetPlaced, + SignupBonus, + Referral, + Winnings, + Commission, +} + +impl From for TxnDirection { + fn from(value: TxnTag) -> TxnDirection { + use TxnTag::*; + match value { + BetPlaced => TxnDirection::Deducted, + Winnings | Commission => TxnDirection::Added, + SignupBonus | Referral => TxnDirection::Bonus, + } + } +} + +impl TxnTag { + fn to_text(self) -> &'static str { + use TxnTag::*; + match self { + BetPlaced => "Bet Placement", + SignupBonus => "Sign Up Bonus", + Referral => "Referral Reward", + Winnings => "Bet Winnings", + Commission => "Bet Commission", + } + } + + fn icondata(self) -> &'static icondata_core::IconData { + TxnDirection::from(self).into() + } +} + +impl Display for TxnTag { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.to_text()) + } +} + +#[derive(Clone, Copy, Serialize, Deserialize)] +pub struct TxnInfo { + pub tag: TxnTag, + pub amount: u64, + pub id: u64, +} + +#[component] +pub fn TxnView(info: TxnInfo) -> impl IntoView { + let direction = TxnDirection::from(info.tag); + let bal_res = format!( + "{} {}", + if direction.positive() { "+" } else { "-" }, + info.amount + ); + + view! { +
+
+
+ +
+
+ + {info.tag.to_text()} + + {info.amount} Coins +
+
+ {bal_res} Coins +
+ } +} + +pub mod provider { + use ic_agent::AgentError; + + use crate::state::canisters::Canisters; + + use super::*; + + pub trait HistoryProvider { + async fn get_history(&self, start: u64, end: u64) -> Result, AgentError>; + } + + pub fn get_history_provider(canisters: Canisters) -> impl HistoryProvider { + #[cfg(feature = "mock-wallet-history")] + { + _ = canisters; + mock::MockHistoryProvider + } + #[cfg(not(feature = "mock-wallet-history"))] + { + canisters + } + } + + #[cfg(not(feature = "mock-wallet-history"))] + mod canister { + use super::*; + use crate::canister::individual_user_template::{ + HotOrNotOutcomePayoutEvent, MintEvent, TokenEvent, + }; + use crate::{canister::individual_user_template::Result5, state::canisters::Canisters}; + + fn event_to_txn(event: (u64, TokenEvent)) -> Option { + let (amount, tag) = match event.1 { + TokenEvent::Stake { amount, .. } => (amount, TxnTag::BetPlaced), + TokenEvent::Burn => return None, + TokenEvent::Mint { + amount, + details: MintEvent::NewUserSignup { .. }, + .. + } => (amount, TxnTag::SignupBonus), + TokenEvent::Mint { + amount, + details: MintEvent::Referral { .. }, + .. + } => (amount, TxnTag::Referral), + TokenEvent::Transfer => return None, + TokenEvent::HotOrNotOutcomePayout { + amount, + details: HotOrNotOutcomePayoutEvent::CommissionFromHotOrNotBet { .. }, + .. + } => (amount, TxnTag::Commission), + TokenEvent::HotOrNotOutcomePayout { + amount, + details: HotOrNotOutcomePayoutEvent::WinningsEarnedFromBet { .. }, + .. + } => (amount, TxnTag::Winnings), + }; + + Some(TxnInfo { + tag, + amount, + id: event.0, + }) + } + + impl HistoryProvider for Canisters { + async fn get_history(&self, start: u64, end: u64) -> Result, AgentError> { + let user = self.authenticated_user(); + let history = user + .get_user_utility_token_transaction_history_with_pagination(start, end) + .await?; + let history = match history { + Result5::Ok(v) => v, + Result5::Err(_) => vec![], + }; + Ok(history.into_iter().filter_map(event_to_txn).collect()) + } + } + } + + #[cfg(feature = "mock-wallet-history")] + mod mock { + use rand_chacha::{ + rand_core::{RngCore, SeedableRng}, + ChaCha8Rng, + }; + + use crate::utils::current_epoch; + + use super::*; + + pub struct MockHistoryProvider; + + fn tag_from_u32(v: u32) -> TxnTag { + match v % 5 { + 0 => TxnTag::BetPlaced, + 1 => TxnTag::SignupBonus, + 2 => TxnTag::Referral, + 3 => TxnTag::Winnings, + 4 => TxnTag::Commission, + _ => unreachable!(), + } + } + + impl HistoryProvider for MockHistoryProvider { + async fn get_history(&self, from: u64, end: u64) -> Result, AgentError> { + let mut rand_gen = ChaCha8Rng::seed_from_u64(current_epoch().as_nanos() as u64); + Ok((from..end) + .map(|_| TxnInfo { + amount: rand_gen.next_u64() % 3001, + tag: tag_from_u32(rand_gen.next_u32()), + id: rand_gen.next_u64(), + }) + .collect()) + } + } + } +} From 8b5be11c88f1dcdba0ca1452d9ff27d8080f040d Mon Sep 17 00:00:00 2001 From: rupansh Date: Wed, 6 Mar 2024 19:41:45 +0530 Subject: [PATCH 2/3] feat: add transactions page --- src/app.rs | 3 +- src/component/nav.rs | 2 +- src/page/refer_earn/history.rs | 48 +++++++++-------- src/page/wallet/mod.rs | 6 +-- src/page/wallet/transactions.rs | 95 +++++++++++++++++++++++++++++++++ src/page/wallet/txn.rs | 41 ++++++++++---- 6 files changed, 158 insertions(+), 37 deletions(-) create mode 100644 src/page/wallet/transactions.rs diff --git a/src/app.rs b/src/app.rs index 97427883..f0ee85a7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,7 +14,7 @@ use crate::{ root::RootPage, terms::TermsOfService, upload::UploadPostPage, - wallet::*, + wallet::{transactions::Transactions, Wallet}, }, state::{ auth::{AuthClient, AuthState}, @@ -64,6 +64,7 @@ pub fn App() -> impl IntoView { + diff --git a/src/component/nav.rs b/src/component/nav.rs index d7f17b31..45ef9c0d 100644 --- a/src/component/nav.rs +++ b/src/component/nav.rs @@ -31,7 +31,7 @@ pub fn NavBar() -> impl IntoView { match path.as_str() { "/" => 0, "/upload" => 2, - "/wallet" => 3, + "/wallet" | "/transactions" => 3, "/menu" => 4, s if s.starts_with("/hot-or-not") => { home_path.set(path); diff --git a/src/page/refer_earn/history.rs b/src/page/refer_earn/history.rs index ac25771d..e8eddc6b 100644 --- a/src/page/refer_earn/history.rs +++ b/src/page/refer_earn/history.rs @@ -123,16 +123,18 @@ mod history_provider { } pub trait HistoryProvider { - async fn get_history(&self, from: u64, end: u64) - -> Result, AgentError>; + async fn get_history( + &self, + from: u64, + end: u64, + ) -> Result<(Vec, bool), AgentError>; } pub async fn get_history( prov: &impl HistoryProvider, from: u64, ) -> Result { - let details = prov.get_history(from, from + 10).await?; - let list_end = details.len() < 10; + let (details, list_end) = prov.get_history(from, from + 10).await?; Ok(HistoryRes { details, cursor: from + 10, @@ -158,7 +160,7 @@ mod history_provider { &self, from: u64, end: u64, - ) -> Result, AgentError> { + ) -> Result<(Vec, bool), AgentError> { use crate::canister::individual_user_template::{MintEvent, Result5, TokenEvent}; use crate::utils::route::failure_redirect; let individual = self.authenticated_user(); @@ -169,9 +171,10 @@ mod history_provider { Result5::Ok(history) => history, Result5::Err(_) => { failure_redirect("failed to get posts"); - return Ok(vec![]); + return Ok((vec![], true)); } }; + let list_end = history.len() < (end - from) as usize; let details = history .into_iter() .filter_map(|(_, ev)| { @@ -194,7 +197,7 @@ mod history_provider { }) }) .collect(); - Ok(details) + Ok((details, list_end)) } } @@ -218,21 +221,24 @@ mod history_provider { &self, from: u64, end: u64, - ) -> Result, AgentError> { + ) -> Result<(Vec, bool), AgentError> { let mut rand_gen = ChaCha8Rng::seed_from_u64(current_epoch().as_nanos() as u64); - Ok((from..end) - .map(|_| { - let sk = SecretKey::random(&mut rand_gen); - let epoch_secs = rand_gen.next_u32() as u64; - let identity = Secp256k1Identity::from_private_key(sk); - let amount = rand_gen.next_u64() % 500; - HistoryDetails { - epoch_secs, - referee: identity.sender().unwrap(), - amount, - } - }) - .collect()) + Ok(( + (from..end) + .map(|_| { + let sk = SecretKey::random(&mut rand_gen); + let epoch_secs = rand_gen.next_u32() as u64; + let identity = Secp256k1Identity::from_private_key(sk); + let amount = rand_gen.next_u64() % 500; + HistoryDetails { + epoch_secs, + referee: identity.sender().unwrap(), + amount, + } + }) + .collect(), + false, + )) } } } diff --git a/src/page/wallet/mod.rs b/src/page/wallet/mod.rs index 0f4ad919..c12f0a6e 100644 --- a/src/page/wallet/mod.rs +++ b/src/page/wallet/mod.rs @@ -1,3 +1,4 @@ +pub mod transactions; mod txn; use leptos::*; @@ -68,7 +69,7 @@ pub fn Wallet() -> impl IntoView { let history_resource = create_resource(canisters_reader, move |canisters| async move { let canisters = try_or_redirect_opt!(canisters.0?); let history_prov = get_history_provider(canisters); - let history = history_prov.get_history(0, RECENT_TXN_CNT).await.ok()?; + let (history, _) = history_prov.get_history(0, RECENT_TXN_CNT).await.ok()?; Some(history) }); @@ -108,8 +109,7 @@ pub fn Wallet() -> impl IntoView {
Recent Transactions - // TODO: href - + See All
diff --git a/src/page/wallet/transactions.rs b/src/page/wallet/transactions.rs new file mode 100644 index 00000000..dbb4e4f1 --- /dev/null +++ b/src/page/wallet/transactions.rs @@ -0,0 +1,95 @@ +use leptos::{html::Div, *}; +use leptos_use::{use_intersection_observer_with_options, UseIntersectionObserverOptions}; + +use crate::{ + component::bullet_loader::BulletLoader, + state::canisters::{authenticated_canisters, Canisters}, + try_or_redirect_opt, +}; + +use super::txn::{ + provider::{get_history_provider, HistoryProvider}, + TxnInfo, TxnView, +}; + +const FETCH_CNT: u64 = 15; + +#[component] +pub fn TransactionList(canisters: Canisters) -> impl IntoView { + let transactions = create_rw_signal(Vec::::new()); + let end = create_rw_signal(false); + let cursor = create_rw_signal(0); + let txn_fetch_resource = create_resource(cursor, move |cursor| { + let canisters = canisters.clone(); + let provider = get_history_provider(canisters); + + async move { + let (txns, list_end) = match provider.get_history(cursor, cursor + FETCH_CNT).await { + Ok(t) => t, + Err(e) => { + log::warn!("failed to fetch tnxs err {e}"); + (vec![], true) + } + }; + transactions.update(|t| t.extend(txns)); + end.set(list_end); + } + }); + let upper_txns = move || { + with!(|transactions| transactions + .iter() + .take(transactions.len().saturating_sub(1)) + .cloned() + .collect::>()) + }; + let last_txn = move || with!(|transactions| transactions.last().cloned()); + let last_elem = create_node_ref::
(); + use_intersection_observer_with_options( + last_elem, + move |entry, _| { + let Some(_visible) = entry.first().filter(|entry| entry.is_intersecting()) else { + return; + }; + if end.get_untracked() { + return; + } + cursor.update(|c| *c += FETCH_CNT); + }, + UseIntersectionObserverOptions::default().thresholds(vec![0.1]), + ); + + view! { +
+ + + + {move || last_txn().map(|info| { + view! { +
+ +
+ } + })} + + + +
+ } +} + +#[component] +pub fn Transactions() -> impl IntoView { + let canisters = authenticated_canisters(); + + view! { +
+ Transactions + + {move || canisters.get().and_then(|c| { + let canisters = try_or_redirect_opt!(c)?; + Some(view! { }) + }).unwrap_or_else(|| view! { })} + +
+ } +} diff --git a/src/page/wallet/txn.rs b/src/page/wallet/txn.rs index 83202538..98665171 100644 --- a/src/page/wallet/txn.rs +++ b/src/page/wallet/txn.rs @@ -124,7 +124,11 @@ pub mod provider { use super::*; pub trait HistoryProvider { - async fn get_history(&self, start: u64, end: u64) -> Result, AgentError>; + async fn get_history( + &self, + start: u64, + end: u64, + ) -> Result<(Vec, bool), AgentError>; } pub fn get_history_provider(canisters: Canisters) -> impl HistoryProvider { @@ -182,7 +186,11 @@ pub mod provider { } impl HistoryProvider for Canisters { - async fn get_history(&self, start: u64, end: u64) -> Result, AgentError> { + async fn get_history( + &self, + start: u64, + end: u64, + ) -> Result<(Vec, bool), AgentError> { let user = self.authenticated_user(); let history = user .get_user_utility_token_transaction_history_with_pagination(start, end) @@ -191,7 +199,11 @@ pub mod provider { Result5::Ok(v) => v, Result5::Err(_) => vec![], }; - Ok(history.into_iter().filter_map(event_to_txn).collect()) + let list_end = history.len() < (end - start) as usize; + Ok(( + history.into_iter().filter_map(event_to_txn).collect(), + list_end, + )) } } } @@ -221,15 +233,22 @@ pub mod provider { } impl HistoryProvider for MockHistoryProvider { - async fn get_history(&self, from: u64, end: u64) -> Result, AgentError> { + async fn get_history( + &self, + from: u64, + end: u64, + ) -> Result<(Vec, bool), AgentError> { let mut rand_gen = ChaCha8Rng::seed_from_u64(current_epoch().as_nanos() as u64); - Ok((from..end) - .map(|_| TxnInfo { - amount: rand_gen.next_u64() % 3001, - tag: tag_from_u32(rand_gen.next_u32()), - id: rand_gen.next_u64(), - }) - .collect()) + Ok(( + (from..end) + .map(|_| TxnInfo { + amount: rand_gen.next_u64() % 3001, + tag: tag_from_u32(rand_gen.next_u32()), + id: rand_gen.next_u64(), + }) + .collect(), + false, + )) } } } From d027acc31cb35f03d7b15168069f4b99649e2148 Mon Sep 17 00:00:00 2001 From: rupansh Date: Wed, 6 Mar 2024 19:46:18 +0530 Subject: [PATCH 3/3] fix(CI/CD): use release-bin & release-lib features for building --- .github/workflows/build-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index ae2e8154..596ed221 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -41,7 +41,7 @@ jobs: nix-shell --run "cargo fmt --check" nix-shell --run "cargo clippy --no-deps --all-features --release -- -Dwarnings" - name: Build the Leptos project to `musl` output - run: nix-shell --run 'cargo leptos build --release' + run: nix-shell --run 'cargo leptos build --release --lib-features release-lib --bin-features release-bin' env: LEPTOS_BIN_TARGET_TRIPLE: x86_64-unknown-linux-musl - run: touch .empty