diff --git a/packages/app/.env.production b/packages/app/.env.production index e4dbc4235..6453c6456 100644 --- a/packages/app/.env.production +++ b/packages/app/.env.production @@ -1,2 +1,2 @@ -FUEL_PROVIDER_URL=http://beta-4.fuel.network/graphql +FUEL_PROVIDER_URL=http://beta-3.fuel.network/graphql diff --git a/packages/app/next.config.mjs b/packages/app/next.config.mjs index 27f06f715..b75ce30f0 100644 --- a/packages/app/next.config.mjs +++ b/packages/app/next.config.mjs @@ -7,15 +7,17 @@ const config = { externalDir: true, serverComponentsExternalPackages: [ 'bcryptjs', + 'ws', + 'isomorphic-ws', '@graphql-tools/delegate', '@graphql-tools/load', + '@graphql-tools/load-files', '@graphql-tools/schema', '@graphql-tools/stitch', '@graphql-tools/url-loader', '@graphql-tools/utils', ], serverActions: true, - esmExternals: true, }, /** We run eslint as a separate task in CI */ eslint: { diff --git a/packages/app/src/app/globals.css b/packages/app/src/app/globals.css index f13043b57..2571f86be 100644 --- a/packages/app/src/app/globals.css +++ b/packages/app/src/app/globals.css @@ -14,6 +14,7 @@ h5, h6 { font-weight: 600; + letter-spacing: -0.025em; } kbd { font-size: 0.875rem; diff --git a/packages/app/src/app/page.tsx b/packages/app/src/app/page.tsx index cfbf560d9..7945a7f76 100644 --- a/packages/app/src/app/page.tsx +++ b/packages/app/src/app/page.tsx @@ -4,13 +4,17 @@ import { getLastTxs } from '~/systems/Transaction/actions/get-last-txs'; import { TxList } from '~/systems/Transaction/component/TxList/TxList'; export default async function Home() { - const transactions = await getLastTxs({}); + const transactions = await getLastTxs({ last: 30 }); return ( - + Recent Transactions - + ); } diff --git a/packages/app/src/app/tx/[id]/page.tsx b/packages/app/src/app/tx/[id]/page.tsx new file mode 100644 index 000000000..aa444851d --- /dev/null +++ b/packages/app/src/app/tx/[id]/page.tsx @@ -0,0 +1,25 @@ +import { Layout } from '~/systems/Core/components/Layout/Layout'; +import { getTx } from '~/systems/Transaction/actions/get-tx'; +import { TxScreen } from '~/systems/Transaction/screens/TxScreen/TxScreen'; + +type TransactionProps = { + params: { + id: string; + }; +}; + +export default async function Transaction({ params }: TransactionProps) { + const id = params.id; + const tx = await getTx({ id }); + if (!tx) { + throw new Error('Transaction not found'); + } + return ( + + + + ); +} + +// Revalidate cache every 10 seconds +export const revalidate = 10; diff --git a/packages/app/src/systems/Core/utils/address.ts b/packages/app/src/systems/Core/utils/address.ts deleted file mode 100644 index 4e4d7a0a3..000000000 --- a/packages/app/src/systems/Core/utils/address.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function shortAddress(address: string = '') { - return address.length > 10 - ? `${address.slice(0, 6)}...${address.slice(-4)}` - : address; -} diff --git a/packages/app/src/systems/Core/utils/sdk.ts b/packages/app/src/systems/Core/utils/sdk.ts index 3194445e8..65f0b26e8 100644 --- a/packages/app/src/systems/Core/utils/sdk.ts +++ b/packages/app/src/systems/Core/utils/sdk.ts @@ -12,5 +12,5 @@ const getBaseUrl = () => { }; const API_URL = resolve(getBaseUrl(), '/api/graphql'); -const client = new GraphQLClient(API_URL); +const client = new GraphQLClient(API_URL, { fetch }); export const sdk = getSdk(client); diff --git a/packages/app/src/systems/Transaction/actions/get-last-txs.ts b/packages/app/src/systems/Transaction/actions/get-last-txs.ts index cdb531d9b..060ff96fe 100644 --- a/packages/app/src/systems/Transaction/actions/get-last-txs.ts +++ b/packages/app/src/systems/Transaction/actions/get-last-txs.ts @@ -5,12 +5,11 @@ import { act } from '~/systems/Core/utils/act-server'; import { sdk } from '~/systems/Core/utils/sdk'; const schema = z.object({ - last: z.number().default(12).optional(), + first: z.number().optional().nullable(), + last: z.number().optional().nullable(), }); -export const getLastTxs = act(schema, async ({ last = 12 }) => { - const { data } = await sdk.getLastTransactions({ last }).catch(() => ({ - data: { transactions: { nodes: [] } }, - })); - return data.transactions.nodes; +export const getLastTxs = act(schema, async (input) => { + const { data } = await sdk.getLastTransactions(input); + return data.transactions; }); diff --git a/packages/app/src/systems/Transaction/actions/get-tx.ts b/packages/app/src/systems/Transaction/actions/get-tx.ts new file mode 100644 index 000000000..1e4a6178f --- /dev/null +++ b/packages/app/src/systems/Transaction/actions/get-tx.ts @@ -0,0 +1,14 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '~/systems/Core/utils/act-server'; +import { sdk } from '~/systems/Core/utils/sdk'; + +const schema = z.object({ + id: z.string(), +}); + +export const getTx = act(schema, async (input) => { + const { data } = await sdk.getTransaction(input); + return data.transaction; +}); diff --git a/packages/app/src/systems/Transaction/component/TxAccountItem/TxAccountItem.tsx b/packages/app/src/systems/Transaction/component/TxAccountItem/TxAccountItem.tsx index 4dd98aec7..f1fca6044 100644 --- a/packages/app/src/systems/Transaction/component/TxAccountItem/TxAccountItem.tsx +++ b/packages/app/src/systems/Transaction/component/TxAccountItem/TxAccountItem.tsx @@ -10,7 +10,7 @@ import { TxIcon } from '../TxIcon/TxIcon'; export type TxAccountItemProps = CardProps & { type: TxAccountType; id: string; - spent: BN; + spent?: BN; }; const COLOR_MAP = { @@ -33,9 +33,11 @@ export function TxAccountItem({ - - Spent: {bn(spent).format({ units: 3 })} - + {spent && ( + + Spent: {bn(spent).format()} + + )} diff --git a/packages/app/src/systems/Transaction/component/TxAssetItem/TxAssetItem.tsx b/packages/app/src/systems/Transaction/component/TxAssetItem/TxAssetItem.tsx index 2c457cb17..88ec6c048 100644 --- a/packages/app/src/systems/Transaction/component/TxAssetItem/TxAssetItem.tsx +++ b/packages/app/src/systems/Transaction/component/TxAssetItem/TxAssetItem.tsx @@ -7,6 +7,8 @@ import type { BN } from 'fuels'; import Image from 'next/image'; import { useMemo } from 'react'; +import { TxIcon } from '../TxIcon/TxIcon'; + export type TxAssetItemProps = CardProps & { assetId: string; amountIn: BN; @@ -27,21 +29,24 @@ export function TxAssetItem({ () => ASSET_LIST.find((i) => i.assetId === assetId), [assetId], ); - if (!asset) { - throw new Error(`Asset not found: ${assetId}`); - } + const assetName = asset?.name ?? 'Unknown'; + const assetSymbol = asset?.symbol ?? null; return ( - {asset.name} + {asset?.icon ? ( + {asset.name} + ) : ( + + )} - + - {bn(amountIn).format({ units: 4 })} {asset.symbol} + {bn(amountIn).format()} {assetSymbol} - {bn(amountOut).format({ units: 4 })} {asset.symbol} + {bn(amountOut).format()} {assetSymbol} diff --git a/packages/app/src/systems/Transaction/component/TxBreadcrumb/TxBreadcrumb.tsx b/packages/app/src/systems/Transaction/component/TxBreadcrumb/TxBreadcrumb.tsx new file mode 100644 index 000000000..4d7ee05ef --- /dev/null +++ b/packages/app/src/systems/Transaction/component/TxBreadcrumb/TxBreadcrumb.tsx @@ -0,0 +1,34 @@ +'use client'; + +import type { BreadcrumbProps } from '@fuels/ui'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + Copyable, + Icon, + shortAddress, +} from '@fuels/ui'; +import { IconHome } from '@tabler/icons-react'; +import Link from 'next/link'; + +import type { TransactionNode } from '../../types'; + +type TxBreadcrumbProps = BreadcrumbProps & { + transaction: TransactionNode; +}; + +export function TxBreadcrumb({ transaction: tx, ...props }: TxBreadcrumbProps) { + return ( + + + + + + + + {shortAddress(tx.id)} + + + ); +} diff --git a/packages/app/src/systems/Transaction/component/TxCard/TxCard.tsx b/packages/app/src/systems/Transaction/component/TxCard/TxCard.tsx index 5a936ab3e..03ebae8dd 100644 --- a/packages/app/src/systems/Transaction/component/TxCard/TxCard.tsx +++ b/packages/app/src/systems/Transaction/component/TxCard/TxCard.tsx @@ -9,6 +9,7 @@ import { IconTransfer, IconUsers, } from '@tabler/icons-react'; +import Link from 'next/link'; import { tv } from 'tailwind-variants'; import type { TransactionNode, TxStatus } from '../../types'; @@ -22,35 +23,37 @@ export function TxCard({ transaction: tx, className, ...props }: TxCardProps) { const classes = styles(); const title = tx.title as string; return ( - - - - - - - - - - - - {tx.totalAccounts} accounts - - - {tx.totalOperations} operations - - - {tx.statusType} - - - - - {tx.totalAssets} assets - - {bn(tx.gasUsed).format({ units: 3 })} ETH - - - - + + + + + + + + + + + + + {tx.totalAccounts} accounts + + + {tx.totalOperations} operations + + + {tx.statusType} + + + + + {tx.totalAssets} assets + + {bn(tx.gasUsed).format({ precision: 5 })} ETH + + + + + ); } diff --git a/packages/app/src/systems/Transaction/component/TxInput/TxInput.tsx b/packages/app/src/systems/Transaction/component/TxInput/TxInput.tsx index e2d37a5a2..b8a6e7205 100644 --- a/packages/app/src/systems/Transaction/component/TxInput/TxInput.tsx +++ b/packages/app/src/systems/Transaction/component/TxInput/TxInput.tsx @@ -85,7 +85,7 @@ const TxInputCoin = createComponent({ {amount && ( - {bn(amount).format({ units: 3 })} {asset.symbol} + {bn(amount).format({ precision: 3 })} {asset.symbol} )} ({ {inputs?.map((input: InputCoin) => ( - {input.utxoId} + + {shortAddress(input.utxoId, 14, 14)} + - {bn(input.amount).format({ units: 3 })} + {bn(input.amount).format({ precision: 3 })} {asset.symbol} ))} @@ -221,7 +226,7 @@ export function TxInput({ input, ...props }: TxInputProps) { const styles = tv({ slots: { header: 'group flex flex-row gap-4 justify-between items-center', - icon: 'transition-transform group-hover:rotate-180 group-data-[state=open]:rotate-180', + icon: 'transition-transform group-data-[state=closed]:hover:rotate-180 group-data-[state=open]:rotate-180', utxos: 'bg-gray-2 mx-4 py-3 px-4 rounded', }, }); diff --git a/packages/app/src/systems/Transaction/component/TxList/TxList.tsx b/packages/app/src/systems/Transaction/component/TxList/TxList.tsx index 6004fd15c..870fc380a 100644 --- a/packages/app/src/systems/Transaction/component/TxList/TxList.tsx +++ b/packages/app/src/systems/Transaction/component/TxList/TxList.tsx @@ -1,17 +1,17 @@ +import type { GetLastTransactionsQuery } from '@fuel-explorer/graphql'; import { Grid } from '@fuels/ui'; -import type { TransactionNode } from '../../types'; import { TxCard } from '../TxCard/TxCard'; type TxListProps = { - transactions: TransactionNode[]; + transactions: GetLastTransactionsQuery['transactions']['edges']; }; export function TxList({ transactions = [] }: TxListProps) { return ( {transactions.map((transaction) => ( - + ))} ); diff --git a/packages/app/src/systems/Transaction/component/TxSummary/TxSummary.tsx b/packages/app/src/systems/Transaction/component/TxSummary/TxSummary.tsx index 56c94636e..9f36cd05f 100644 --- a/packages/app/src/systems/Transaction/component/TxSummary/TxSummary.tsx +++ b/packages/app/src/systems/Transaction/component/TxSummary/TxSummary.tsx @@ -1,3 +1,5 @@ +'use client'; + import { bn } from '@fuel-ts/math'; import type { BaseProps, CardProps, BoxProps } from '@fuels/ui'; import { @@ -101,7 +103,7 @@ export const TxSummaryDetails = createComponent< iconSize={24} leftIcon={IconGasStation} > - {bn(tx.gasUsed).format({ units: 3 })} ETH + {bn(tx.gasUsed).format()} ETH @@ -144,7 +146,7 @@ export const TxSummary = withNamespace(TxSummaryRoot, { const styles = tv({ slots: { - root: 'grid grid-cols-[2fr,1fr]', + root: 'grid grid-cols-[2fr,1fr] gap-6', details: 'p-6', params: 'p-6 fuel-[Text]:text-lg', row: 'grid grid-cols-[100px,1fr] gap-8 items-center', diff --git a/packages/app/src/systems/Transaction/screens/TxScreen/TxScreen.tsx b/packages/app/src/systems/Transaction/screens/TxScreen/TxScreen.tsx new file mode 100644 index 000000000..8921a9275 --- /dev/null +++ b/packages/app/src/systems/Transaction/screens/TxScreen/TxScreen.tsx @@ -0,0 +1,80 @@ +'use client'; + +import type { GroupedInput, Maybe } from '@fuel-explorer/graphql'; +import { bn } from '@fuel-ts/math'; +import { Grid, Heading, VStack } from '@fuels/ui'; + +import { TxAccountItem } from '../../component/TxAccountItem/TxAccountItem'; +import { TxAssetItem } from '../../component/TxAssetItem/TxAssetItem'; +import { TxBreadcrumb } from '../../component/TxBreadcrumb/TxBreadcrumb'; +import { TxInput } from '../../component/TxInput/TxInput'; +import { TxSummary } from '../../component/TxSummary/TxSummary'; +import type { TransactionNode, TxAccountType } from '../../types'; + +type TxScreenProps = { + transaction: TransactionNode; +}; + +export function TxScreen({ transaction: tx }: TxScreenProps) { + console.log(tx.inputContracts); + return ( + + + Transaction Details + + + + + + + Assets + + + {tx.inputAssetIds?.map((assetId) => ( + + ))} + + + + + Accounts + + + {tx.accountsInvolved?.map((acc) => ( + + ))} + + + + + + Inputs + + {tx.groupedInputs?.map((input) => ( + + ))} + + + + ); +} + +function getInputId(input?: Maybe) { + if (!input) return 0; + if (input.type === 'InputCoin') return input.assetId; + if (input.type === 'InputContract') return input.contractId; + return input.sender; +} diff --git a/packages/ui/src/components/Breadcrumb/Breadcrumb.tsx b/packages/ui/src/components/Breadcrumb/Breadcrumb.tsx index e5d3424c0..9920621dc 100644 --- a/packages/ui/src/components/Breadcrumb/Breadcrumb.tsx +++ b/packages/ui/src/components/Breadcrumb/Breadcrumb.tsx @@ -3,20 +3,18 @@ import { Children } from 'react'; import { createComponent, withNamespace } from '../../utils/component'; import type { PropsOf } from '../../utils/types'; -import { HStack } from '../Box'; -import type { HStackProps } from '../Box'; import { Icon } from '../Icon/Icon'; import { Link } from '../Link/Link'; import type { LinkProps } from '../Link/Link'; -export type BreadcrumbProps = PropsOf<'ul'> & Omit; +export type BreadcrumbProps = PropsOf<'ul'>; export type BreadcrumbItemProps = PropsOf<'li'>; export type BreadcrumbLinkProps = LinkProps; export const BreadcrumbRoot = createComponent({ id: 'Breadcrumb', - baseElement: 'ul', - render: (Comp, { children, ...props }) => { + className: 'flex gap-4 items-stretch', + render: (_, { children, ...props }) => { const newChildren = Children.toArray(children).flatMap((child, index) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const id = (child as any)?.type?.id; @@ -37,11 +35,7 @@ export const BreadcrumbRoot = createComponent({ } return child; }); - return ( - - {newChildren} - - ); + return
    {newChildren}
; }, }); @@ -53,6 +47,7 @@ export const BreadcrumbItem = createComponent({ export const BreadcrumbLink = createComponent( { id: 'BreadcrumbLink', + className: 'inline-flex', baseElement: Link, }, ); diff --git a/packages/ui/src/theme/tailwind-preset.ts b/packages/ui/src/theme/tailwind-preset.ts index a6264c5af..adaa87195 100644 --- a/packages/ui/src/theme/tailwind-preset.ts +++ b/packages/ui/src/theme/tailwind-preset.ts @@ -108,8 +108,12 @@ const preset: Config = { plugin(function ({ addVariant, matchVariant }) { // Add a `third` variant, ie. `third:pb-0` addVariant('not-first', '& ~ &'); - addVariant('not-disabled', '&:not([aria-disabled=true])'); addVariant('not-first-last', '&:not(:first-of-type,:last-of-type)'); + addVariant('not-disabled', '&:not([aria-disabled=true],:disabled)'); + addVariant( + 'not-disabled-hover', + '&:not([aria-disabled=true],:disabled):hover', + ); addVariant('first-type', '&:first-of-type'); addVariant('last-type', '&:last-of-type'); addVariant('dark-theme', ['.dark &', '.dark-theme &']); diff --git a/packages/ui/src/utils/helpers.ts b/packages/ui/src/utils/helpers.ts index 1c14fd826..1fc7c9a95 100644 --- a/packages/ui/src/utils/helpers.ts +++ b/packages/ui/src/utils/helpers.ts @@ -51,8 +51,8 @@ export function toCamelCase(str: string): string { }); } -export function shortAddress(address: string = '') { +export function shortAddress(address: string = '', first = 6, last = 4) { return address.length > 10 - ? `${address.slice(0, 6)}...${address.slice(-4)}` + ? `${address.slice(0, first)}...${address.slice(-last)}` : address; }