Skip to content
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
27 changes: 27 additions & 0 deletions src/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { cn } from '@/lib/utils';
import { Loader2 } from 'lucide-react';

interface LoadingProps extends React.HTMLAttributes<HTMLDivElement> {
size?: number;
text?: string;
}

export function Loading({
size = 24,
text,
className,
...props
}: LoadingProps) {
return (
<div
className={cn(
'flex flex-col items-center justify-center gap-2',
className,
)}
{...props}
>
<Loader2 className='animate-spin' style={{ width: size, height: size }} />
{text && <p className='text-sm text-muted-foreground'>{text}</p>}
</div>
);
}
32 changes: 25 additions & 7 deletions src/pages/Send.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import ConfirmationDialog from '@/components/ConfirmationDialog';
import Container from '@/components/Container';
import Header from '@/components/Header';
import { TokenAmountInput } from '@/components/ui/masked-input';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
Form,
FormControl,
Expand All @@ -12,9 +15,10 @@ import {
import { Input } from '@/components/ui/input';
import { useErrors } from '@/hooks/useErrors';
import { amount, positiveAmount } from '@/lib/formTypes';
import { toMojos } from '@/lib/utils';
import { toDecimal, toMojos } from '@/lib/utils';
import { useWalletState } from '@/state';
import { zodResolver } from '@hookform/resolvers/zod';
import BigNumber from 'bignumber.js';
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
Expand All @@ -26,16 +30,12 @@ import {
SendXch,
TransactionResponse,
} from '../bindings';
import Container from '../components/Container';
import { TokenAmountInput } from '@/components/ui/masked-input';

export default function Send() {
const { asset_id: assetId } = useParams();
const isXch = assetId === 'xch';

const navigate = useNavigate();
const walletState = useWalletState();

const { addError } = useErrors();

const [asset, setAsset] = useState<(CatRecord & { decimals: number }) | null>(
Expand Down Expand Up @@ -69,7 +69,6 @@ export default function Send() {

const unlisten = events.syncEvent.listen((event) => {
const type = event.payload.type;

if (
type === 'coin_state' ||
type === 'puzzle_batch_synced' ||
Expand Down Expand Up @@ -98,7 +97,13 @@ export default function Send() {
(address) => commands.validateAddress(address).catch(addError),
'Invalid address',
),
amount: positiveAmount(asset?.decimals || 12),
amount: positiveAmount(asset?.decimals || 12).refine(
(amount) =>
asset
? BigNumber(amount).lte(toDecimal(asset.balance, asset.decimals))
: true,
'Amount exceeds balance',
),
fee: amount(walletState.sync.unit.decimals).optional(),
});

Expand Down Expand Up @@ -135,6 +140,19 @@ export default function Send() {
/>

<Container className='max-w-xl'>
{asset && (
<Card className='mb-6'>
<CardContent className='pt-6'>
<div className='text-sm text-muted-foreground'>
Available Balance
</div>
<div className='text-2xl font-medium mt-1'>
{toDecimal(asset.balance, asset.decimals)} {asset.ticker}
</div>
</CardContent>
</Card>
)}

<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<FormField
Expand Down
57 changes: 26 additions & 31 deletions src/pages/TokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useErrors } from '@/hooks/useErrors';
import { usePrices } from '@/hooks/usePrices';
import { useTokenParams } from '@/hooks/useTokenParams';
import { toDecimal } from '@/lib/utils';
import { ArrowDown10, ArrowDownAz, Coins, InfoIcon } from 'lucide-react';
import { ArrowDown10, ArrowDownAz, Coins, InfoIcon, Clock } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { CatRecord, commands, events } from '../bindings';
Expand All @@ -30,50 +30,48 @@ enum TokenView {
export function TokenList() {
const navigate = useNavigate();
const walletState = useWalletState();

const { getBalanceInUsd } = usePrices();
const { addError } = useErrors();

const [params, setParams] = useTokenParams();
const { view, showHidden } = params;

const [cats, setCats] = useState<CatRecord[]>([]);

const catsWithBalanceInUsd = useMemo(
() =>
cats.map((cat) => ({
...cat,
balanceInUsd: getBalanceInUsd(cat.asset_id, toDecimal(cat.balance, 3)),
})),
cats.map((cat) => {
const balance = Number(toDecimal(cat.balance, 3));
const usdValue = parseFloat(
getBalanceInUsd(cat.asset_id, balance.toString()),
);
return {
...cat,
balanceInUsd: usdValue,
sortValue: usdValue,
};
}),
[cats, getBalanceInUsd],
);

const sortedCats = catsWithBalanceInUsd.sort((a, b) => {
if (a.visible && !b.visible) {
return -1;
}

if (!a.visible && b.visible) {
return 1;
}

if (!a[view] && b[view]) {
return -1;
}
if (a.visible && !b.visible) return -1;
if (!a.visible && b.visible) return 1;

if (a[view] && !b[view]) {
return 1;
if (view === TokenView.Balance) {
if (a.balanceInUsd === 0 && b.balanceInUsd === 0) {
return (
Number(toDecimal(b.balance, 3)) - Number(toDecimal(a.balance, 3))
);
}
return b.sortValue - a.sortValue;
}

if (!a[view] && !b[view]) {
return 0;
}
const aName = a.name || 'Unknown CAT';
const bName = b.name || 'Unknown CAT';

if (view === TokenView.Balance) {
return Number(b.balanceInUsd) - Number(a.balanceInUsd);
}
if (aName === 'Unknown CAT' && bName !== 'Unknown CAT') return 1;
if (bName === 'Unknown CAT' && aName !== 'Unknown CAT') return -1;

return a.name!.localeCompare(b.name!);
return aName.localeCompare(bName);
});

const visibleCats = sortedCats.filter((cat) => showHidden || cat.visible);
Expand Down Expand Up @@ -151,7 +149,6 @@ export function TokenList() {
<Card className='transition-colors hover:bg-neutral-50 dark:hover:bg-neutral-900'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-md font-medium'>Chia</CardTitle>

<img
alt={`XCH logo`}
className='h-6 w-6'
Expand Down Expand Up @@ -187,7 +184,6 @@ export function TokenList() {
<CardTitle className='text-md font-medium truncate'>
{cat.name || 'Unknown CAT'}
</CardTitle>

{cat.icon_url && (
<img
alt={`${cat.asset_id} logo`}
Expand All @@ -200,7 +196,6 @@ export function TokenList() {
<div className='text-2xl font-medium truncate'>
{toDecimal(cat.balance, 3)} {cat.ticker ?? ''}
</div>

<div className='text-sm text-neutral-500'>
~${cat.balanceInUsd}
</div>
Expand Down
111 changes: 59 additions & 52 deletions src/pages/ViewOffer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,47 @@ import { commands, OfferSummary, TakeOfferResponse } from '@/bindings';
import ConfirmationDialog from '@/components/ConfirmationDialog';
import Container from '@/components/Container';
import Header from '@/components/Header';
import { Loading } from '@/components/Loading';
import { OfferCard } from '@/components/OfferCard';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useErrors } from '@/hooks/useErrors';
import { toDecimal, toMojos } from '@/lib/utils';
import { toMojos } from '@/lib/utils';
import { useWalletState } from '@/state';
import BigNumber from 'bignumber.js';
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';

export function ViewOffer() {
const { offer } = useParams();
const { addError } = useErrors();

const walletState = useWalletState();
const navigate = useNavigate();

const [isLoading, setIsLoading] = useState(true);
const [loadingStatus, setLoadingStatus] = useState('Initializing...');
const [summary, setSummary] = useState<OfferSummary | null>(null);
const [response, setResponse] = useState<TakeOfferResponse | null>(null);
const [fee, setFee] = useState('');

useEffect(() => {
if (!offer) return;

commands
.viewOffer({ offer })
.then((data) => setSummary(data.offer))
.catch(addError);
const loadOffer = async () => {
setIsLoading(true);
setLoadingStatus('Fetching offer details...');

commands
.viewOffer({ offer })
.then((data) => {
setSummary(data.offer);
setLoadingStatus('Processing offer data...');
})
.catch(addError)
.finally(() => setIsLoading(false));
};

loadOffer();
}, [offer, addError]);

const importOffer = () => {
Expand All @@ -40,18 +52,13 @@ export function ViewOffer() {
.catch(addError);
};

const take = () => {
commands
.importOffer({ offer: offer! })
.then(() =>
commands
.takeOffer({
offer: offer!,
fee: toMojos(fee || '0', walletState.sync.unit.decimals),
})
.then((result) => setResponse(result))
.catch(addError),
)
const take = async () => {
await commands
.takeOffer({
offer: offer!,
fee: toMojos(fee || '0', walletState.sync.unit.decimals),
})
.then((result) => setResponse(result))
.catch(addError);
};

Expand All @@ -60,41 +67,41 @@ export function ViewOffer() {
<Header title='View Offer' />

<Container>
{summary && (
<OfferCard summary={summary}>
<div className='flex flex-col space-y-1.5'>
<Label htmlFor='fee'>Network Fee</Label>
<Input
id='fee'
type='text'
placeholder='0.00'
className='pr-12'
value={fee}
onChange={(e) => setFee(e.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
take();
}
}}
/>
{isLoading ? (
<Loading className='my-8' text={loadingStatus} />
) : (
summary && (
<>
<OfferCard summary={summary}>
<div className='flex flex-col space-y-1.5'>
<Label htmlFor='fee'>Network Fee</Label>
<Input
id='fee'
type='text'
placeholder='0.00'
className='pr-12'
value={fee}
onChange={(e) => setFee(e.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
take();
}
}}
/>
</div>
</OfferCard>

<span className='text-xs text-muted-foreground'>
{BigNumber(summary?.fee ?? '0').isGreaterThan(0)
? `This does not include a fee of ${toDecimal(summary!.fee, walletState.sync.unit.decimals)} which was already added by the maker.`
: ''}
</span>
</div>
</OfferCard>
)}
<div className='mt-4 flex gap-2'>
<Button variant='outline' onClick={importOffer}>
Save Offer
</Button>

<div className='mt-4 flex gap-2'>
<Button variant='outline' onClick={importOffer}>
Save Offer
</Button>

<Button onClick={take}>Take Offer</Button>
</div>
<Button onClick={take}>Take Offer</Button>
</div>
</>
)
)}
</Container>

<ConfirmationDialog
Expand Down
Loading