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

feat(#328): my trades implementation #337

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
27 changes: 27 additions & 0 deletions src/pages/trade/api/latest-swaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { connectionStore } from '@/shared/model/connection';
import { penumbra } from '@/shared/const/penumbra';
import { ViewService } from '@penumbra-zone/protobuf';
import { LatestSwapsResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';

Check failure on line 5 in src/pages/trade/api/latest-swaps.ts

View workflow job for this annotation

GitHub Actions / Lint

Module '"@penumbra-zone/protobuf/penumbra/view/v1/view_pb"' has no exported member 'LatestSwapsResponse'.
import { AddressIndex } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';

const fetchQuery = async (subaccount?: number): Promise<LatestSwapsResponse[]> => {
return await Array.fromAsync(
penumbra.service(ViewService).latestSwaps({

Check failure on line 10 in src/pages/trade/api/latest-swaps.ts

View workflow job for this annotation

GitHub Actions / Lint

Property 'latestSwaps' does not exist on type 'PromiseClient<{ readonly typeName: "penumbra.view.v1.ViewService"; readonly methods: { readonly status: { readonly name: "Status"; readonly I: typeof StatusRequest; readonly O: typeof StatusResponse; readonly kind: MethodKind.Unary; }; ... 28 more ...; readonly auctions: { ...; }; }; }>'.
accountFilter:
typeof subaccount === 'undefined' ? undefined : new AddressIndex({ account: subaccount }),
}),
);
};

/**
* Must be used within the `observer` mobX HOC
*/
export const useLatestSwaps = (subaccount?: number) => {
return useQuery({
queryKey: ['my-trades', subaccount],
queryFn: () => fetchQuery(subaccount),
retry: 1,
enabled: connectionStore.connected,
});
};
2 changes: 1 addition & 1 deletion src/pages/trade/ui/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cn from 'clsx';
import { PairInfo } from './pair-info';
import { Chart } from './chart';
import { RouteTabs } from './route-tabs';
import { TradesTabs } from './trades-tabs';
import { TradesTabs } from './trades';
import { HistoryTabs } from './history-tabs';
import { FormTabs } from './form-tabs';
import { useEffect, useState } from 'react';
Expand Down
16 changes: 16 additions & 0 deletions src/pages/trade/ui/positions/cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ReactNode } from 'react';
import { Skeleton } from '@penumbra-zone/ui/Skeleton';

export const Cell = ({ children }: { children: ReactNode }) => {
return <div className='flex items-center py-1.5 px-3 min-h-12'>{children}</div>;
};

export const LoadingCell = () => {
return (
<Cell>
<div className='w-12 h-4'>
<Skeleton />
</div>
</Cell>
);
};
4 changes: 2 additions & 2 deletions src/pages/trade/ui/positions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { pnum } from '@penumbra-zone/types/pnum';
import { useAssets } from '@/shared/api/assets';
import { SquareArrowOutUpRight } from 'lucide-react';
import { usePathToMetadata } from '../../model/use-path';
import { PositionsCurrentValue } from '../positions-current-value';
import { LoadingCell } from '../market-trades';
import { PositionsCurrentValue } from './positions-current-value';
import { LoadingCell } from './cell';
import { NotConnectedNotice } from './not-connected-notice';
import { ErrorNotice } from './error-notice';
import { NoPositions } from './no-positions';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { useMarketPrice } from '../model/useMarketPrice';
import { useMarketPrice } from '../../model/useMarketPrice';
import { ValueViewComponent } from '@penumbra-zone/ui/ValueView';
import { LoadingCell } from './market-trades';
import { LoadingCell } from './cell';
import { pnum } from '@penumbra-zone/types/pnum';
import { DisplayPosition } from '../model/positions';
import { DisplayPosition } from '../../model/positions';

export const PositionsCurrentValue = ({ order }: { order: DisplayPosition['orders'][number] }) => {
const { baseAsset, quoteAsset } = order;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Tabs } from '@penumbra-zone/ui/Tabs';
import { Density } from '@penumbra-zone/ui/Density';
import { Button } from '@penumbra-zone/ui/Button';
import { Chart } from './chart';
import { Chart } from '../chart';
import { MarketTrades } from './market-trades';
import { MyTrades } from '@/pages/trade/ui/trades/my-trades';

enum TradesTabsType {
Chart = 'chart',
Expand Down Expand Up @@ -63,9 +64,7 @@ export const TradesTabs = ({ withChart = false }: { withChart?: boolean }) => {
</div>
)}
{tab === TradesTabsType.MarketTrades && <MarketTrades />}
{tab === TradesTabsType.MyTrades && (
<div className='text-text-secondary p-4'>Coming soon...</div>
)}
{tab === TradesTabsType.MyTrades && <MyTrades />}
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Fragment, ReactNode } from 'react';
import { Fragment } from 'react';
import { ChevronRight } from 'lucide-react';
import cn from 'clsx';
import { TableCell } from '@penumbra-zone/ui/TableCell';
import { Density } from '@penumbra-zone/ui/Density';
import { Skeleton } from '@penumbra-zone/ui/Skeleton';
import { Text } from '@penumbra-zone/ui/Text';
import { pluralize } from '@/shared/utils/pluralize';
import { useRecentExecutions } from '../api/recent-executions.ts';

export const Cell = ({ children }: { children: ReactNode }) => {
return <div className='flex items-center py-1.5 px-3 min-h-12'>{children}</div>;
};

export const LoadingCell = () => {
return (
<Cell>
<div className='w-12 h-4'>
<Skeleton />
</div>
</Cell>
);
};
import { useRecentExecutions } from '../../api/recent-executions.ts';

const ErrorState = ({ error }: { error: Error }) => {
return <div className='text-red-500'>{String(error)}</div>;
Expand Down Expand Up @@ -55,7 +40,7 @@ export const MarketTrades = () => {

{data?.map((trade, index) => (
<div
key={trade.timestamp + trade.amount}
key={trade.timestamp + trade.amount + trade.kind}
className={cn(
'relative grid grid-cols-subgrid col-span-4',
'group [&:hover>div:not(:last-child)]:invisible',
Expand Down
103 changes: 103 additions & 0 deletions src/pages/trade/ui/trades/my-trades.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import cn from 'clsx';
import { Fragment } from 'react';
import { observer } from 'mobx-react-lite';
import { ChevronRight } from 'lucide-react';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { TableCell } from '@penumbra-zone/ui/TableCell';
import { Density } from '@penumbra-zone/ui/Density';
import { Text } from '@penumbra-zone/ui/Text';
import { pluralize } from '@/shared/utils/pluralize';
import { useLatestSwaps } from '@/pages/trade/api/latest-swaps';
import { connectionStore } from '@/shared/model/connection';
import { pnum } from '@penumbra-zone/types/pnum';
import { ErrorState, formatLocalTime } from './shared';

export const MyTrades = observer(() => {
const { subaccount } = connectionStore;
const { data, isLoading, error } = useLatestSwaps(subaccount);
const [parent] = useAutoAnimate();

// TODO: replace with real data
const type = 'sell' as string;
const timestemp = '1738235546706' as string;
const hops = ['um', 'usdc'];

return (
<Density slim>
<div ref={parent} className='grid grid-cols-4 pt-4 px-4 pb-0 h-auto overflow-auto'>
<div className='grid grid-cols-subgrid col-span-4'>
<TableCell heading>Price</TableCell>
<TableCell heading>Amount</TableCell>
<TableCell heading>Time</TableCell>
<TableCell heading>Route</TableCell>
</div>

{error && <ErrorState error={error} />}

{data?.map((trade, index) => (
<div
key={index}
className={cn(
'relative grid grid-cols-subgrid col-span-4',
'group [&:hover>div:not(:last-child)]:invisible',
)}
>
<TableCell
numeric
variant={index !== data.length - 1 ? 'cell' : 'lastCell'}
loading={isLoading}
>
<span className={type === 'buy' ? 'text-success-light' : 'text-destructive-light'}>
{trade.input?.amount && pnum(trade.input.amount).toFormattedString()}
</span>
</TableCell>
<TableCell
variant={index !== data.length - 1 ? 'cell' : 'lastCell'}
numeric
loading={isLoading}
>
{trade.output?.amount && pnum(trade.output.amount).toFormattedString()}
</TableCell>
<TableCell
variant={index !== data.length - 1 ? 'cell' : 'lastCell'}
numeric
loading={isLoading}
>
{formatLocalTime(timestemp)}
</TableCell>
<TableCell
variant={index !== data.length - 1 ? 'cell' : 'lastCell'}
loading={isLoading}
>
<Text
as='span'
color={hops.length <= 2 ? 'text.primary' : 'text.special'}
whitespace='nowrap'
detailTechnical
>
{hops.length === 2 ? 'Direct' : pluralize(hops.length - 2, 'Hop', 'Hops')}
</Text>
</TableCell>

{/* Route display that shows on hover */}
<div
className={cn(
'hidden group-hover:flex justify-center items-center gap-1',
'absolute left-0 right-0 w-full h-full px-4 z-30 select-none border-b border-b-other-tonalStroke',
)}
>
{hops.map((token, index) => (
<Fragment key={index}>
{index > 0 && <ChevronRight className='w-3 h-3 text-neutral-light text-xs' />}
<Text tableItemSmall color='text.primary'>
{token}
</Text>
</Fragment>
))}
</div>
</div>
))}
</div>
</Density>
);
});
13 changes: 13 additions & 0 deletions src/pages/trade/ui/trades/shared.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const ErrorState = ({ error }: { error: Error }) => {
return <div className='text-red-500'>{String(error)}</div>;
};

export const formatLocalTime = (isoString: string): string => {
const date = new Date(isoString);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
Loading