Skip to content

Commit

Permalink
feat: add transparent assets table (#1234)
Browse files Browse the repository at this point in the history
  • Loading branch information
euharrison authored Nov 8, 2024
1 parent ce300c8 commit ed94316
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 9 deletions.
8 changes: 8 additions & 0 deletions apps/namadillo/src/App/AccountOverview/AccountOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { useAtomValue } from "jotai";
import { twMerge } from "tailwind-merge";
import { AccountBalanceContainer } from "./AccountBalanceContainer";
import { BalanceContainer } from "./BalanceContainer";
import { MaspBanner } from "./MaspBanner";
import { NamBalanceContainer } from "./NamBalanceContainer";
import { NavigationFooter } from "./NavigationFooter";
import { TransparentOverviewPanel } from "./TransparentOverviewPanel";

export const AccountOverview = (): JSX.Element => {
const userHasAccount = useUserHasAccount();
Expand Down Expand Up @@ -42,6 +44,12 @@ export const AccountOverview = (): JSX.Element => {
<StakingRewardsPanel />
</Panel>
</div>
{maspEnabled && (
<>
<MaspBanner />
<TransparentOverviewPanel />
</>
)}
<Panel className="flex items-center flex-1 justify-center">
<NavigationFooter />
</Panel>
Expand Down
43 changes: 43 additions & 0 deletions apps/namadillo/src/App/AccountOverview/MaspBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ActionButton, Panel } from "@namada/components";
import { FiatCurrency } from "App/Common/FiatCurrency";
import { routes } from "App/routes";
import { shieldedTokensAtom } from "atoms/balance/atoms";
import { getTotalDollar } from "atoms/balance/functions";
import { useAtomValue } from "jotai";
import { twMerge } from "tailwind-merge";
import maspBg from "./assets/masp-bg.png";

export const MaspBanner = (): JSX.Element => {
const shieldedTokensQuery = useAtomValue(shieldedTokensAtom);
const total = getTotalDollar(shieldedTokensQuery.data);

return (
<Panel
className={twMerge(
"relative p-10 border border-yellow",
"flex items-center flex-wrap gap-10",
"text-yellow"
)}
>
<div className="relative h-[170px] w-[170px] flex items-center justify-center">
<img src={maspBg} className="absolute" />
<div className="col-start-1 row-start-1 text-4xl">MASP</div>
</div>
<div className="flex-1">
{total && total.gt(0) && (
<>
<div className="text-sm">Total shielded balance</div>
<FiatCurrency className="text-4xl" amount={total} />
</>
)}
</div>
<ActionButton
size="md"
href={routes.ibcShieldAll}
className="self-end justify-end"
>
Manage your shielded assets
</ActionButton>
</Panel>
);
};
197 changes: 197 additions & 0 deletions apps/namadillo/src/App/AccountOverview/TransparentOverviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import {
ActionButton,
Panel,
SkeletonLoading,
TableRow,
} from "@namada/components";
import { AtomErrorBoundary } from "App/Common/AtomErrorBoundary";
import { FiatCurrency } from "App/Common/FiatCurrency";
import { TableWithPaginator } from "App/Common/TableWithPaginator";
import { TokenCurrency } from "App/Common/TokenCurrency";
import { routes } from "App/routes";
import { TokenBalance, transparentTokensAtom } from "atoms/balance/atoms";
import { getTotalDollar } from "atoms/balance/functions";
import { getAssetImageUrl } from "integrations/utils";
import { useAtomValue } from "jotai";
import { useEffect, useState } from "react";
import { namadaAsset } from "registry/namadaAsset";
import { twMerge } from "tailwind-merge";

const resultsPerPage = 100;
const initialPage = 0;

const TransparentTokensTable = ({
data,
}: {
data: TokenBalance[];
}): JSX.Element => {
const [page, setPage] = useState(initialPage);

const headers = ["Token", { children: "Balance", className: "text-right" }];

const renderRow = ({ asset, balance, dollar }: TokenBalance): TableRow => {
const display = asset.display;
const icon = getAssetImageUrl(asset);

return {
cells: [
<div key={`token-${display}`} className="flex items-center gap-4">
<div className="aspect-square w-8 h-8">
{icon ?
<img src={icon} />
: <div className="rounded-full h-full border border-white" />}
</div>
{asset.symbol}
</div>,
<div
key={`balance-${display}`}
className="flex flex-col text-right leading-tight"
>
<TokenCurrency asset={asset} amount={balance} />
{dollar && (
<FiatCurrency
className="text-neutral-600 text-sm"
amount={dollar}
/>
)}
</div>,
<div
key={`buttons-${display}`}
className="flex items-center justify-end gap-1"
>
<ActionButton size="xs" href={routes.maspShield}>
Shield
</ActionButton>
{display === namadaAsset.display && (
<ActionButton
size="xs"
className="w-fit mx-auto"
backgroundColor="cyan"
href={routes.stakingBondingIncrement}
>
Stake
</ActionButton>
)}
</div>,
],
};
};

useEffect(() => {
setPage(0);
}, [data]);

const paginatedItems = data.slice(
page * resultsPerPage,
page * resultsPerPage + resultsPerPage
);

const pageCount = Math.ceil(data.length / resultsPerPage);

return (
<>
<div className="text-sm font-medium mt-6">
<span className="text-yellow">{data.length} </span>
Tokens
</div>
<TableWithPaginator
id="transparent-tokens"
headers={headers.concat("")}
renderRow={renderRow}
itemList={paginatedItems}
page={page}
pageCount={pageCount}
onPageChange={setPage}
tableProps={{
className: twMerge(
"w-full flex-1 [&_td]:px-1 [&_th]:px-1 [&_td:first-child]:pl-4 [&_td]:h-[64px]",
"[&_td]:font-normal [&_td:last-child]:pr-4 [&_th:first-child]:pl-4 [&_th:last-child]:pr-4",
"[&_td:first-child]:rounded-s-md [&_td:last-child]:rounded-e-md",
"mt-2"
),
}}
headProps={{ className: "text-neutral-500" }}
/>
</>
);
};

const PanelContent = ({ data }: { data: TokenBalance[] }): JSX.Element => {
const namBalance = data.find((i) => i.asset.base === namadaAsset.base);

return (
<div className="flex flex-col gap-2">
<div className="grid md:grid-cols-2 gap-2">
{[
{
title: "Total Transparent Asset Balance",
amount: getTotalDollar(data),
button: (
<ActionButton size="xs" href={routes.ibcShieldAll}>
Shield All
</ActionButton>
),
},
{
title: "Transparent NAM Balance",
amount: namBalance?.dollar,
namAmount: namBalance?.balance,
button: (
<ActionButton
size="xs"
backgroundColor="cyan"
href={routes.stakingBondingIncrement}
>
Stake
</ActionButton>
),
},
].map(({ title, amount, namAmount, button }) => (
<div key={title} className="bg-gray px-6 py-3 rounded-sm flex gap-6">
<div className="flex-1 overflow-auto">
<div className="text-sm">{title}</div>
<div className="text-2xl sm:text-3xl whitespace-nowrap overflow-auto">
{amount ?
<FiatCurrency amount={amount} />
: "N/A"}
</div>
{namAmount && namBalance && (
<TokenCurrency
amount={namAmount}
asset={namBalance.asset}
className="text-neutral-400 text-sm"
/>
)}
</div>
<div className="self-center">{button}</div>
</div>
))}
</div>
<TransparentTokensTable data={data} />
</div>
);
};

export const TransparentOverviewPanel = (): JSX.Element => {
const transparentTokensQuery = useAtomValue(transparentTokensAtom);

return (
<Panel className="min-h-[300px] flex flex-col" title="Transparent Overview">
{transparentTokensQuery.isPending ?
<SkeletonLoading height="100%" width="100%" />
: <AtomErrorBoundary
result={transparentTokensQuery}
niceError="Unable to load your transparent balance"
containerProps={{ className: "pb-16" }}
>
{transparentTokensQuery.data?.length ?
<PanelContent data={transparentTokensQuery.data} />
: <div className="bg-neutral-900 p-6 rounded-sm text-center font-medium my-14">
You currently hold no assets in your unshielded account
</div>
}
</AtomErrorBoundary>
}
</Panel>
);
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 16 additions & 1 deletion apps/namadillo/src/App/Common/FiatCurrency.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { Currency, CurrencyProps } from "@namada/components";
import BigNumber from "bignumber.js";

type FiatCurrencyProps = Omit<CurrencyProps, "currency">;

export const FiatCurrency = (props: FiatCurrencyProps): JSX.Element => {
return <Currency currency={{ symbol: "$", fraction: "cents" }} {...props} />;
let amount = new BigNumber(props.amount);
if (amount.lt(0.01)) {
amount = new BigNumber(amount.toPrecision(2));
} else {
amount = amount.decimalPlaces(2);
}

return (
<Currency
{...props}
currency={{ symbol: "$", fraction: "cents" }}
amount={amount}
decimalPlaces={amount.decimalPlaces() === 1 ? 2 : undefined}
/>
);
};
6 changes: 3 additions & 3 deletions apps/namadillo/src/App/Masp/ShieldedFungibleTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const ShieldedFungibleTable = ({
<img src={icon} />
: <div className="rounded-full h-full border border-white" />}
</div>
{display.toUpperCase()}
{asset.symbol}
</div>,
<div
key={`balance-${display}`}
Expand All @@ -62,7 +62,7 @@ export const ShieldedFungibleTable = ({
size="xs"
outlineColor="white"
className="w-fit mx-auto"
href={display === "NAM" ? routes.maspUnshield : routes.ibcWithdraw}
href={routes.maspUnshield}
>
Unshield
</ActionButton>,
Expand All @@ -88,7 +88,7 @@ export const ShieldedFungibleTable = ({
Tokens
</div>
<TableWithPaginator
id={"my-validators"}
id="shielded-tokens"
headers={headers.concat("")}
renderRow={renderRow}
itemList={paginatedItems}
Expand Down
1 change: 0 additions & 1 deletion apps/namadillo/src/App/Sidebars/MainnetRoadmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const MainnetRoadmap = (): JSX.Element => {
claimRewardsEnabled ? "opacity-100" : "opacity-25",
claimRewardsEnabled
)}
{renderPhase("2", <></>, "opacity-25")}
{renderPhase(
"3",
<>
Expand Down
6 changes: 3 additions & 3 deletions apps/namadillo/src/atoms/balance/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const sqsOsmosisApi = "https://sqs.osmosis.zone/";
export const fetchCoinPrices = async (
assetBaseList: string[]
): Promise<Record<string, { [usdcAddress: string]: string }>> =>
fetch(`${sqsOsmosisApi}/tokens/prices?base=${assetBaseList.join(",")}`).then(
(res) => res.json()
);
fetch(
`${sqsOsmosisApi}/tokens/prices?base=${assetBaseList.sort((a, b) => a.localeCompare(b)).join(",")}`
).then((res) => res.json());

export const fetchShieldedBalance = async (
viewingKey: string,
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/Currency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type CurrencyObject = {

export type CurrencyProps = {
amount: number | BigNumber;
decimalPlaces?: number;
hideBalances?: boolean;
currency: CurrencyObject;
separator?: "." | "," | "";
Expand All @@ -21,6 +22,7 @@ export type CurrencyProps = {

export const Currency = ({
amount,
decimalPlaces,
currency,
hideBalances = false,
currencyPosition = "left",
Expand All @@ -32,7 +34,7 @@ export const Currency = ({
fractionClassName = "",
...containerRest
}: CurrencyProps): JSX.Element => {
const amountParts = BigNumber(amount).toFormat().split(".");
const amountParts = BigNumber(amount).toFormat(decimalPlaces).split(".");
const baseAmount = hideBalances ? "✳✳✳✳" : amountParts[0] || "0";
const fraction =
amountParts.length > 1 && !hideBalances ? amountParts[1] : "";
Expand Down

1 comment on commit ed94316

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.