Skip to content
Open
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
1 change: 1 addition & 0 deletions configs/app/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export { default as rewards } from './rewards';
export { default as rollbar } from './rollbar';
export { default as rollup } from './rollup';
export { default as safe } from './safe';
export { default as signetActivity } from './signetActivity';
export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats';
export { default as suave } from './suave';
Expand Down
24 changes: 24 additions & 0 deletions configs/app/features/signetActivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Feature } from './types';

import { getEnvValue } from '../utils';

const title = 'Signet activity';

const config: Feature<{
readonly apiEndpoint: string | undefined;
}> = (() => {
if (getEnvValue('NEXT_PUBLIC_SIGNET_ACTIVITY_ENABLED') === 'true') {
return Object.freeze({
title,
isEnabled: true,
apiEndpoint: getEnvValue('NEXT_PUBLIC_SIGNET_ACTIVITY_API_ENDPOINT'),
});
}

return Object.freeze({
title,
isEnabled: false,
});
})();

export default config;
41 changes: 41 additions & 0 deletions types/api/signetActivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const ORDER_STATUSES = {
filled: 'filled',
pending: 'pending',
expired: 'expired',
} as const;

export type OrderStatus = typeof ORDER_STATUSES[keyof typeof ORDER_STATUSES];

export interface SignetTokenAmount {
readonly token_symbol: string;
readonly token_address: string;
readonly amount: string;
readonly decimals: number;
}

export interface SignetOutput extends SignetTokenAmount {
readonly chain_id: string | null;
readonly recipient: string;
}

export interface SignetOrder {
readonly order_hash: string;
readonly status: OrderStatus;
readonly inputs: ReadonlyArray<SignetTokenAmount>;
readonly outputs: ReadonlyArray<SignetOutput>;
readonly deadline: string;
readonly sender: string;
readonly fill_tx_hash: string | null;
}

export interface SignetFill {
readonly fill_tx_hash: string;
readonly filler: string;
readonly outputs: ReadonlyArray<SignetOutput>;
readonly order_hash: string;
}

export interface BlockSignetActivity {
readonly orders: ReadonlyArray<SignetOrder>;
readonly fills: ReadonlyArray<SignetFill>;
}
82 changes: 82 additions & 0 deletions ui/block/signetActivity/FillCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Box, Flex, Grid } from '@chakra-ui/react';
import React from 'react';

import type { SignetFill } from 'types/api/signetActivity';

import { route } from 'nextjs/routes';

import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';

import formatTokenAmount from './formatTokenAmount';

interface Props {
readonly fill: SignetFill;
readonly isLoading: boolean;
}

const FillCard = ({ fill, isLoading }: Props) => {
return (
<Box
borderWidth="1px"
borderColor="border.divider"
borderRadius="base"
p={ 4 }
>
<Flex alignItems="center" gap={ 2 } mb={ 3 } minW={ 0 }>
<Skeleton loading={ isLoading } fontWeight={ 600 } textStyle="sm">
Fill
</Skeleton>
<Skeleton loading={ isLoading } minW={ 0 }>
<Flex alignItems="center" gap={ 1 } minW={ 0 }>
<Link href={ route({ pathname: '/tx/[hash]', query: { hash: fill.fill_tx_hash } }) }>
<HashStringShortenDynamic hash={ fill.fill_tx_hash }/>
</Link>
</Flex>
</Skeleton>
</Flex>

<Grid templateColumns={{ base: '1fr', lg: '100px 1fr' }} gap={ 2 } textStyle="sm">
<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Filler
</Skeleton>
<AddressEntity
address={{ hash: fill.filler }}
isLoading={ isLoading }
truncation="constant"
/>

<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Outputs
</Skeleton>
<Flex flexDir="column" gap={ 1 }>
{ fill.outputs.map((output, idx) => (
<Flex key={ idx } alignItems="center" gap={ 1 }>
<Skeleton loading={ isLoading }>
{ formatTokenAmount(output) }
</Skeleton>
{ output.chain_id && (
<Tooltip content={ `Destination chain: ${ output.chain_id }` }>
<IconSvg name="navigation/cross_chain_txs" boxSize={ 4 } color="icon.secondary" cursor="pointer"/>
</Tooltip>
) }
</Flex>
)) }
</Flex>

<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Order
</Skeleton>
<Skeleton loading={ isLoading } minW={ 0 }>
<HashStringShortenDynamic hash={ fill.order_hash }/>
</Skeleton>
</Grid>
</Box>
);
};

export default React.memo(FillCard);
118 changes: 118 additions & 0 deletions ui/block/signetActivity/OrderCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Box, Flex, Grid, Text } from '@chakra-ui/react';
import React from 'react';

import type { SignetOrder } from 'types/api/signetActivity';

import { route } from 'nextjs/routes';

import dayjs from 'lib/date/dayjs';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';

import formatTokenAmount from './formatTokenAmount';
import OrderStatusBadge from './OrderStatusBadge';

interface Props {
readonly order: SignetOrder;
readonly isLoading: boolean;
}

const OrderCard = ({ order, isLoading }: Props) => {
const deadlineDate = dayjs(order.deadline);
const isExpired = deadlineDate.isBefore(dayjs());

return (
<Box
borderWidth="1px"
borderColor="border.divider"
borderRadius="base"
p={ 4 }
>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<Flex alignItems="center" gap={ 2 } minW={ 0 }>
<Skeleton loading={ isLoading } fontWeight={ 600 } textStyle="sm">
Order
</Skeleton>
<Skeleton loading={ isLoading } minW={ 0 }>
<Flex alignItems="center" gap={ 1 } minW={ 0 }>
<HashStringShortenDynamic hash={ order.order_hash }/>
<CopyToClipboard text={ order.order_hash }/>
</Flex>
</Skeleton>
</Flex>
<OrderStatusBadge status={ order.status } loading={ isLoading }/>
</Flex>

<Grid templateColumns={{ base: '1fr', lg: '100px 1fr' }} gap={ 2 } textStyle="sm">
<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Sender
</Skeleton>
<AddressEntity
address={{ hash: order.sender }}
isLoading={ isLoading }
truncation="constant"
/>

<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Inputs
</Skeleton>
<Flex flexDir="column" gap={ 1 }>
{ order.inputs.map((input, idx) => (
<Skeleton key={ idx } loading={ isLoading }>
{ formatTokenAmount(input) }
</Skeleton>
)) }
</Flex>

<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Outputs
</Skeleton>
<Flex flexDir="column" gap={ 1 }>
{ order.outputs.map((output, idx) => (
<Flex key={ idx } alignItems="center" gap={ 1 }>
<Skeleton loading={ isLoading }>
{ formatTokenAmount(output) }
</Skeleton>
{ output.chain_id && (
<Tooltip content={ `Destination chain: ${ output.chain_id }` }>
<IconSvg name="navigation/cross_chain_txs" boxSize={ 4 } color="icon.secondary" cursor="pointer"/>
</Tooltip>
) }
</Flex>
)) }
</Flex>

<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Deadline
</Skeleton>
<Skeleton loading={ isLoading }>
<Tooltip content={ deadlineDate.format('llll') }>
<Text as="span" color={ isExpired ? 'text.secondary' : undefined }>
{ deadlineDate.fromNow() }
</Text>
</Tooltip>
</Skeleton>

{ order.fill_tx_hash && (
<>
<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }>
Fill tx
</Skeleton>
<Skeleton loading={ isLoading }>
<Link href={ route({ pathname: '/tx/[hash]', query: { hash: order.fill_tx_hash } }) }>
<HashStringShortenDynamic hash={ order.fill_tx_hash }/>
</Link>
</Skeleton>
</>
) }
</Grid>
</Box>
);
};

export default React.memo(OrderCard);
30 changes: 30 additions & 0 deletions ui/block/signetActivity/OrderStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

import type { OrderStatus } from 'types/api/signetActivity';

import type { BadgeProps } from 'toolkit/chakra/badge';
import { Badge } from 'toolkit/chakra/badge';
import IconSvg from 'ui/shared/IconSvg';

interface Props extends BadgeProps {
readonly status: OrderStatus;
}

const STATUS_CONFIG = {
filled: { colorPalette: 'green', icon: 'status/success', label: 'Filled' },
pending: { colorPalette: 'yellow', icon: 'status/pending', label: 'Pending' },
expired: { colorPalette: 'red', icon: 'status/error', label: 'Expired' },
} as const;

const OrderStatusBadge = ({ status, ...rest }: Props) => {
const cfg = STATUS_CONFIG[status];
const iconElement = <IconSvg name={ cfg.icon } boxSize={ 2.5 }/>;

return (
<Badge colorPalette={ cfg.colorPalette } startElement={ iconElement } { ...rest }>
{ cfg.label }
</Badge>
);
};

export default React.memo(OrderStatusBadge);
Loading
Loading