Skip to content

Commit

Permalink
Merge pull request #88 from xch-dev/show-usd-balance
Browse files Browse the repository at this point in the history
Show USD balance for XCH and CATs
  • Loading branch information
Rigidity authored Nov 12, 2024
2 parents 5e162ed + fe3f2fe commit f3fe2ef
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 53 deletions.
5 changes: 4 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Transactions } from './pages/Transactions';
import { ViewOffer } from './pages/ViewOffer';
import Wallet from './pages/Wallet';
import { fetchState } from './state';
import { PriceProvider } from './contexts/PriceContext';

export interface DarkModeContext {
toggle: () => void;
Expand Down Expand Up @@ -128,7 +129,9 @@ export default function App() {
<DarkModeContext.Provider value={darkMode}>
{initialized && (
<PeerProvider>
<RouterProvider router={router} />
<PriceProvider>
<RouterProvider router={router} />
</PriceProvider>
</PeerProvider>
)}
{error && (
Expand Down
28 changes: 4 additions & 24 deletions src/components/CopyBox.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { CopyCheckIcon, CopyIcon } from 'lucide-react';
import { useState } from 'react';
import { Button } from './ui/button';
import { CopyButton } from './CopyButton';

export function CopyBox(props: {
title: string;
content: string;
className?: string;
}) {
const [copied, setCopied] = useState(false);

const copyAddress = () => {
writeText(props.content);

setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<div className={`flex rounded-md shadow-sm max-w-lg ${props.className}`}>
<input
Expand All @@ -26,18 +14,10 @@ export function CopyBox(props: {
readOnly
className='block w-full text-sm rounded-none rounded-l-md border-0 py-1.5 px-2 truncate text-muted-foreground bg-background font-mono tracking-tight ring-1 ring-inset ring-neutral-200 dark:ring-neutral-800 sm:leading-6'
/>
<Button
size='icon'
variant='ghost'
<CopyButton
value={props.content}
className='relative rounded-none -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-neutral-200 dark:ring-neutral-800 hover:bg-gray-50'
onClick={copyAddress}
>
{copied ? (
<CopyCheckIcon className='-ml-0.5 h-5 w-5 text-emerald-500' />
) : (
<CopyIcon className='-ml-0.5 h-5 w-5 text-muted-foreground' />
)}
</Button>
/>
</div>
);
}
30 changes: 30 additions & 0 deletions src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { CopyCheckIcon, CopyIcon } from 'lucide-react';
import { useState } from 'react';
import { Button } from './ui/button';

export function CopyButton(props: { value: string; className?: string }) {
const [copied, setCopied] = useState(false);

const copyAddress = () => {
writeText(props.value);

setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<Button
size='icon'
variant='ghost'
onClick={copyAddress}
className={props.className}
>
{copied ? (
<CopyCheckIcon className='h-5 w-5 text-emerald-500' />
) : (
<CopyIcon className='h-5 w-5 text-muted-foreground' />
)}
</Button>
);
}
2 changes: 1 addition & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Sheet, SheetContent, SheetTrigger } from './ui/sheet';

export default function Header(
props: PropsWithChildren<{
title: string;
title: string | ReactNode;
back?: () => void;
children?: ReactNode;
}>,
Expand Down
92 changes: 92 additions & 0 deletions src/contexts/PriceContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useWalletState } from '@/state';
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from 'react';

interface PriceContextType {
getBalanceInUsd: (assetId: string, balance: string) => string;
}

const PriceContext = createContext<PriceContextType | undefined>(undefined);

export function PriceProvider({ children }: { children: ReactNode }) {
const walletState = useWalletState();

const [xchUsdPrice, setChiaPrice] = useState<number>(0);
const [catPrices, setCatPrices] = useState<Record<string, number>>({});

useEffect(() => {
const fetchCatPrices = () =>
fetch('https://api.dexie.space/v2/prices/tickers')
.then((res) => res.json())
.then((data) => {
const tickers = data.tickers.reduce(
(acc: Record<string, string>, ticker: any) => {
acc[ticker.base_id] = ticker.last_price || 0;
return acc;
},
{},
);
setCatPrices(tickers);
})
.catch(() => {
setCatPrices({});
});

const fetchChiaPrice = () =>
fetch(
'https://api.coingecko.com/api/v3/simple/price?ids=chia&vs_currencies=usd',
)
.then((res) => res.json())
.then((data) => {
setChiaPrice(data.chia.usd || 0);
})
.catch(() => {
setChiaPrice(0);
});

const fetchPrices = () => Promise.all([fetchCatPrices(), fetchChiaPrice()]);

if (walletState.sync.unit.ticker === 'XCH') {
fetchPrices();
const interval = setInterval(fetchPrices, 60000);
return () => clearInterval(interval);
} else {
setChiaPrice(0);
setCatPrices({});
}
}, [walletState.sync.unit.ticker]);

const getBalanceInUsd = useCallback(
(assetId: string, balance: string) => {
if (assetId === 'xch') {
return (Number(balance) * xchUsdPrice).toFixed(2);
}
return (
Number(balance) *
(catPrices[assetId] || 0) *
xchUsdPrice
).toFixed(2);
},
[xchUsdPrice, catPrices],
);

return (
<PriceContext.Provider value={{ getBalanceInUsd }}>
{children}
</PriceContext.Provider>
);
}

export function usePrices() {
const context = useContext(PriceContext);
if (context === undefined) {
throw new Error('usePeers must be used within a PeerProvider');
}
return context;
}
52 changes: 52 additions & 0 deletions src/hooks/useTokenParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useSearchParams } from 'react-router-dom';

export interface TokenParams {
view: TokenView;
showHidden: boolean;
}

export enum TokenView {
Name = 'name',
Balance = 'balance',
}

export function parseView(view: string): TokenView {
switch (view) {
case 'name':
return TokenView.Name;
case 'balance':
return TokenView.Balance;
default:
return TokenView.Name;
}
}

export type SetTokenParams = (params: Partial<TokenParams>) => void;

export function useTokenParams(): [TokenParams, SetTokenParams] {
const [params, setParams] = useSearchParams();

const view = parseView(params.get('view') ?? 'name');
const showHidden = (params.get('showHidden') ?? 'false') === 'true';

const updateParams = ({ view, showHidden }: Partial<TokenParams>) => {
setParams(
(prev) => {
const next = new URLSearchParams(prev);

if (view !== undefined) {
next.set('view', view);
}

if (showHidden !== undefined) {
next.set('showHidden', showHidden.toString());
}

return next;
},
{ replace: true },
);
};

return [{ view, showHidden }, updateParams];
}
51 changes: 35 additions & 16 deletions src/pages/Token.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import CoinList from '@/components/CoinList';
import ConfirmationDialog from '@/components/ConfirmationDialog';
import Container from '@/components/Container';
import { CopyBox } from '@/components/CopyBox';
import Header from '@/components/Header';
import { ReceiveAddress } from '@/components/ReceiveAddress';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -53,10 +52,13 @@ import {
events,
TransactionSummary,
} from '../bindings';
import { usePrices } from '@/contexts/PriceContext';
import { CopyButton } from '@/components/CopyButton';

export default function Token() {
const navigate = useNavigate();
const walletState = useWalletState();
const { getBalanceInUsd } = usePrices();

const { asset_id: assetId } = useParams();

Expand All @@ -65,6 +67,11 @@ export default function Token() {
const [summary, setSummary] = useState<TransactionSummary | null>(null);
const [selectedCoins, setSelectedCoins] = useState<RowSelectionState>({});

const balanceInUsd = useMemo(() => {
if (!asset) return '0';
return getBalanceInUsd(asset.asset_id, asset.balance);
}, [asset, getBalanceInUsd]);

const updateCoins = () => {
const getCoins =
assetId === 'xch' ? commands.getCoins() : commands.getCatCoins(assetId!);
Expand Down Expand Up @@ -174,25 +181,37 @@ export default function Token() {

return (
<>
<Header title={asset ? (asset.name ?? 'Unknown asset') : ''} />
<Header
title={
<span>
{asset ? (asset.name ?? 'Unknown asset') : ''}{' '}
{asset?.asset_id !== 'xch' && (
<CopyButton value={asset?.asset_id ?? ''} />
)}
</span>
}
/>
<Container>
<div className='flex flex-col gap-8 max-w-screen-lg'>
{asset?.asset_id !== 'xch' && (
<CopyBox title='Asset Id' content={asset?.asset_id ?? ''} />
)}

<Card>
<CardHeader className='flex flex-row justify-between items-center space-y-0 space-x-2 pb-2'>
<div className='flex text-xl sm:text-4xl font-medium font-mono truncate'>
<span className='truncate'>{asset?.balance ?? ' '}&nbsp;</span>
{asset?.ticker}
<CardHeader className='flex flex-col pb-2'>
<div className='flex flex-row justify-between items-center space-y-0 space-x-2'>
<div className='flex text-xl sm:text-4xl font-medium font-mono truncate'>
<span className='truncate'>
{asset?.balance ?? ' '}&nbsp;
</span>
{asset?.ticker}
</div>
<div className='flex-shrink-0'>
<img
alt='asset icon'
src={asset?.icon_url ?? ''}
className='h-8 w-8'
/>
</div>
</div>
<div className='flex-shrink-0'>
<img
alt='asset icon'
src={asset?.icon_url ?? ''}
className='h-8 w-8'
/>
<div className='text-sm text-muted-foreground'>
~${balanceInUsd}
</div>
</CardHeader>
<CardContent className='flex flex-col gap-2'>
Expand Down
Loading

0 comments on commit f3fe2ef

Please sign in to comment.