Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Wallet Implementation #97

Merged
merged 3 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
3 changes: 3 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
root::RootPage,
terms::TermsOfService,
upload::UploadPostPage,
wallet::{transactions::Transactions, Wallet},
},
state::{
auth::{AuthClient, AuthState},
Expand Down Expand Up @@ -62,6 +63,8 @@ pub fn App() -> impl IntoView {
<Route path="/faq" view=Faq/>
<Route path="/terms-of-service" view=TermsOfService/>
<Route path="/privacy-policy" view=PrivacyPolicy/>
<Route path="/wallet" view=Wallet/>
<Route path="/transactions" view=Transactions/>
<Route path="" view=RootPage/>
</Route>
</Routes>
Expand Down
14 changes: 9 additions & 5 deletions src/component/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" | "/transactions" => 3,
"/menu" => 4,
s if s.starts_with("/hot-or-not") => {
home_path.set(path);
0
}
_ => 2,
_ => 4,
}
});

view! {
<div class="flex flex-row justify-between px-4 py-5 w-full bg-transparent fixed left-0 bottom-0 z-50">
<NavIcon idx=0 href=home_path icon=icondata::TbHome cur_selected=cur_selected/>
// TODO: achievements page
<NavIcon idx=1 href="/#" icon=icondata::AiTrophyOutlined cur_selected/>
<NavIcon
idx=1
idx=2
href="/upload"
icon=icondata::AiPlusCircleFilled
cur_selected=cur_selected
/>
<NavIcon idx=2 href="/menu" icon=icondata::AiMenuOutlined cur_selected=cur_selected/>
<NavIcon idx=3 href="/wallet" icon=icondata::BiWalletRegular cur_selected=cur_selected/>
<NavIcon idx=4 href="/menu" icon=icondata::AiMenuOutlined cur_selected=cur_selected/>
</div>
}
}
1 change: 1 addition & 0 deletions src/page/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pub mod refer_earn;
pub mod root;
pub mod terms;
pub mod upload;
pub mod wallet;
48 changes: 27 additions & 21 deletions src/page/refer_earn/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,18 @@ mod history_provider {
}

pub trait HistoryProvider {
async fn get_history(&self, from: u64, end: u64)
-> Result<Vec<HistoryDetails>, AgentError>;
async fn get_history(
&self,
from: u64,
end: u64,
) -> Result<(Vec<HistoryDetails>, bool), AgentError>;
}

pub async fn get_history(
prov: &impl HistoryProvider,
from: u64,
) -> Result<HistoryRes, AgentError> {
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,
Expand All @@ -158,7 +160,7 @@ mod history_provider {
&self,
from: u64,
end: u64,
) -> Result<Vec<HistoryDetails>, AgentError> {
) -> Result<(Vec<HistoryDetails>, bool), AgentError> {
use crate::canister::individual_user_template::{MintEvent, Result5, TokenEvent};
use crate::utils::route::failure_redirect;
let individual = self.authenticated_user();
Expand All @@ -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)| {
Expand All @@ -194,7 +197,7 @@ mod history_provider {
})
})
.collect();
Ok(details)
Ok((details, list_end))
}
}

Expand All @@ -218,21 +221,24 @@ mod history_provider {
&self,
from: u64,
end: u64,
) -> Result<Vec<HistoryDetails>, AgentError> {
) -> Result<(Vec<HistoryDetails>, 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,
))
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/page/refer_earn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?

<ReferLoading/>
}
})
Expand Down
135 changes: 135 additions & 0 deletions src/page/wallet/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
pub mod transactions;
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! {
<div class="flex flex-col">
<span class="text-white/50 text-md">Welcome!</span>
<div class="w-3/4 rounded-full py-2 bg-white/40 animate-pulse"></div>
</div>
<div class="w-16 aspect-square overflow-clip rounded-full justify-self-end bg-white/40 animate-pulse"></div>
}
}

#[component]
fn ProfileGreeter(details: ProfileDetails) -> impl IntoView {
view! {
<div class="flex flex-col">
<span class="text-white/50 text-md">Welcome!</span>
<span class="text-white text-lg md:text-xl truncate">
{details.display_name_or_fallback()}
</span>
</div>
<div class="w-16 aspect-square overflow-clip justify-self-end rounded-full">
<img class="h-full w-full object-cover" src=details.profile_pic_or_random()/>
</div>
}
}

const RECENT_TXN_CNT: u64 = 10;

#[component]
fn BalanceFallback() -> impl IntoView {
view! { <div class="w-1/4 rounded-full py-3 mt-1 bg-white/30 animate-pulse"></div> }
}

#[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! {
<div class="flex flex-col w-dvw min-h-dvh bg-black gap-12 px-4 py-4">
<div class="grid grid-cols-2 grid-rows-1 items-center w-full">
<Suspense fallback=FallbackGreeter>
{move || {
profile_details
.get()
.flatten()
.map(|details| view! { <ProfileGreeter details/> })
.unwrap_or_else(|| view! { <FallbackGreeter/> })
}}

</Suspense>
</div>
<div class="flex flex-col w-full items-center mt-12 text-white">
<span class="text-md lg:text-lg uppercase">Your Coin Balance</span>
<Suspense fallback=BalanceFallback>
{move || {
balance_resource
.get()
.flatten()
.map(|bal| view! { <span class="text-xl lg:text-2xl">{bal}</span> })
.unwrap_or_else(|| {
view! {
<span class="flex justify-center w-full">
<BalanceFallback/>
</span>
}
})
}}
</Suspense>
</div>
<div class="flex flex-col w-full gap-2">
<div class="flex flex-row w-full items-end justify-between">
<span class="text-white text-sm md:text-md">Recent Transactions</span>
<a href="/transactions" class="text-white/50 text-md md:text-lg">
See All
</a>
</div>
<div class="flex flex-col divide-y divide-white/10">
<Suspense fallback=BulletLoader>
{move || {
history_resource
.get()
.flatten()
.map(|history| {
history
.into_iter()
.map(|info| view! { <TxnView info/> })
.collect::<Vec<_>>()
})
.unwrap_or_else(|| vec![view! { <BulletLoader/> }])
}}
</Suspense>
</div>
</div>
</div>
}
}
Loading
Loading