Skip to content

Commit a5384c0

Browse files
authored
feat: add transaction page (#56)
1 parent 4041fc6 commit a5384c0

File tree

35 files changed

+1029
-1687
lines changed

35 files changed

+1029
-1687
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"test:clear": "pnpm turbo:run test -- --clearCache",
4848
"test:coverage": " pnpm turbo:run test -- --coverage",
4949
"test:watch": "pnpm turbo:run test -- --watch",
50-
"ts:check": "pnpm -r ts:check",
50+
"ts:check": "pnpm turbo:run ts:check",
5151
"turbo:run": "./scripts/turbo.sh"
5252
},
5353
"dependencies": {
@@ -61,7 +61,7 @@
6161
"devDependencies": {
6262
"@fuels/tsup-config": "^0.0.11",
6363
"@next/eslint-plugin-next": "^13.5.3",
64-
"@swc/core": "1.3.89",
64+
"@swc/core": "1.3.90",
6565
"@swc/jest": "0.2.29",
6666
"@types/jest": "29.5.5",
6767
"@types/node": "20.7.0",

packages/app/.env.production

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
FUEL_PROVIDER_URL=http://beta-4.fuel.network/graphql
1+
FUEL_PROVIDER_URL=http://beta-3.fuel.network/graphql
22

packages/app/next.config.mjs

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1+
const externals = [
2+
'bcryptjs',
3+
'ws',
4+
'isomorphic-ws',
5+
'node-fetch',
6+
'@whatwg/node-fetch',
7+
'@graphql-tools/delegate',
8+
'@graphql-tools/load',
9+
'@graphql-tools/schema',
10+
'@graphql-tools/stitch',
11+
'@graphql-tools/url-loader',
12+
'@graphql-tools/utils',
13+
];
14+
115
/** @type {import('next').NextConfig} */
216
const config = {
317
reactStrictMode: true,
418
swcMinify: true,
519
transpilePackages: ['@fuel-explorer/graphql'],
620
experimental: {
721
externalDir: true,
8-
serverComponentsExternalPackages: [
9-
'bcryptjs',
10-
'@graphql-tools/delegate',
11-
'@graphql-tools/load',
12-
'@graphql-tools/schema',
13-
'@graphql-tools/stitch',
14-
'@graphql-tools/url-loader',
15-
'@graphql-tools/utils',
16-
],
22+
serverComponentsExternalPackages: externals,
1723
serverActions: true,
1824
esmExternals: true,
1925
},
@@ -49,6 +55,13 @@ const config = {
4955
];
5056
},
5157
webpack: (config) => {
58+
config.externals.push({
59+
'utf-8-validate': 'commonjs utf-8-validate',
60+
bufferutil: 'commonjs bufferutil',
61+
encoding: 'commonjs encoding',
62+
module: 'commonjs module',
63+
});
64+
5265
config.module.rules.push({
5366
test: /\.(graphql|gql)/,
5467
exclude: /node_modules/,

packages/app/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@faker-js/faker": "8.1.0",
1717
"@fontsource-variable/inter": "5.0.8",
1818
"@fuel-explorer/graphql": "workspace:*",
19-
"@fuel-ts/math": "0.59.0",
19+
"@fuel-ts/math": "0.60.0",
2020
"@fuel-wallet/sdk": "0.13.0",
2121
"@fuels/assets": "0.0.11",
2222
"@fuels/ui": "workspace:*",
@@ -26,7 +26,7 @@
2626
"csstype": "3.1.2",
2727
"dayjs": "1.11.10",
2828
"framer-motion": "10.16.4",
29-
"fuels": "0.59.0",
29+
"fuels": "0.60.0",
3030
"graphql": "16.8.1",
3131
"graphql-request": "6.1.0",
3232
"graphql-tag": "2.12.6",
@@ -58,7 +58,7 @@
5858
"@testing-library/jest-dom": "6.1.3",
5959
"@types/node": "20.7.0",
6060
"@types/react": "^18.2.23",
61-
"@types/react-dom": "^18.2.7",
61+
"@types/react-dom": "^18.2.8",
6262
"@xstate/cli": "^0.5.2",
6363
"autoprefixer": "10.4.16",
6464
"postcss": "8.4.30",

packages/app/src/app/globals.css

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
h5,
1515
h6 {
1616
font-weight: 600;
17+
letter-spacing: -0.025em;
1718
}
1819
kbd {
1920
font-size: 0.875rem;

packages/app/src/app/page.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import { getLastTxs } from '~/systems/Transaction/actions/get-last-txs';
44
import { TxList } from '~/systems/Transaction/component/TxList/TxList';
55

66
export default async function Home() {
7-
const transactions = await getLastTxs({});
7+
const transactions = await getLastTxs({ last: 30 });
88
return (
99
<Layout hero>
10-
<Heading as="h2" size="2" className="mb-10">
10+
<Heading
11+
as="h2"
12+
size="2"
13+
className="flex justify-between items-center mb-10"
14+
>
1115
Recent Transactions
1216
</Heading>
13-
<TxList transactions={transactions} />
17+
<TxList transactions={transactions.edges} />
1418
</Layout>
1519
);
1620
}

packages/app/src/app/tx/[id]/page.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Layout } from '~/systems/Core/components/Layout/Layout';
2+
import { getTx } from '~/systems/Transaction/actions/get-tx';
3+
import { TxScreen } from '~/systems/Transaction/screens/TxScreen/TxScreen';
4+
5+
type TransactionProps = {
6+
params: {
7+
id?: string | null;
8+
};
9+
};
10+
11+
export default async function Transaction({
12+
params: { id = null },
13+
}: TransactionProps) {
14+
const tx = await getTx({ id });
15+
return (
16+
<Layout>
17+
<TxScreen transaction={tx} />
18+
</Layout>
19+
);
20+
}
21+
22+
// Revalidate cache every 10 seconds
23+
export const revalidate = 10;

packages/app/src/systems/Core/utils/address.ts

-5
This file was deleted.

packages/app/src/systems/Core/utils/sdk.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ const getBaseUrl = () => {
1212
};
1313

1414
const API_URL = resolve(getBaseUrl(), '/api/graphql');
15-
const client = new GraphQLClient(API_URL);
15+
const client = new GraphQLClient(API_URL, { fetch });
1616
export const sdk = getSdk(client);

packages/app/src/systems/Transaction/actions/get-last-txs.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { act } from '~/systems/Core/utils/act-server';
55
import { sdk } from '~/systems/Core/utils/sdk';
66

77
const schema = z.object({
8-
last: z.number().default(12).optional(),
8+
first: z.number().optional().nullable(),
9+
last: z.number().optional().nullable(),
910
});
1011

11-
export const getLastTxs = act(schema, async ({ last = 12 }) => {
12-
const { data } = await sdk.getLastTransactions({ last }).catch(() => ({
13-
data: { transactions: { nodes: [] } },
14-
}));
15-
return data.transactions.nodes;
12+
export const getLastTxs = act(schema, async (input) => {
13+
const { data } = await sdk.getLastTransactions(input).catch((_) => {
14+
return { data: { transactions: { edges: [] } } };
15+
});
16+
return data.transactions;
1617
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use server';
2+
3+
import { z } from 'zod';
4+
import { act } from '~/systems/Core/utils/act-server';
5+
import { sdk } from '~/systems/Core/utils/sdk';
6+
7+
const schema = z.object({
8+
id: z.string().nullable(),
9+
});
10+
11+
export const getTx = act(schema, async (input) => {
12+
if (!input.id) return null;
13+
const { data } = await sdk.getTransaction(input).catch((_) => {
14+
return { data: { transaction: null } };
15+
});
16+
return data.transaction;
17+
});

packages/app/src/systems/Transaction/component/TxAccountItem/TxAccountItem.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { TxIcon } from '../TxIcon/TxIcon';
1010
export type TxAccountItemProps = CardProps & {
1111
type: TxAccountType;
1212
id: string;
13-
spent: BN;
13+
spent?: BN;
1414
};
1515

1616
const COLOR_MAP = {
@@ -33,9 +33,11 @@ export function TxAccountItem({
3333
<TxIcon color={COLOR_MAP[type]} type={type} />
3434
</EntityItem.Slot>
3535
<EntityItem.Info id={id} title={type}>
36-
<Text as="div" className="text-sm" leftIcon={IconCoins}>
37-
Spent: {bn(spent).format({ units: 3 })}
38-
</Text>
36+
{spent && (
37+
<Text as="div" className="text-sm" leftIcon={IconCoins}>
38+
Spent: {bn(spent).format()}
39+
</Text>
40+
)}
3941
</EntityItem.Info>
4042
</EntityItem>
4143
</Card.Body>

packages/app/src/systems/Transaction/component/TxAssetItem/TxAssetItem.tsx

+17-12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { BN } from 'fuels';
77
import Image from 'next/image';
88
import { useMemo } from 'react';
99

10+
import { TxIcon } from '../TxIcon/TxIcon';
11+
1012
export type TxAssetItemProps = CardProps & {
1113
assetId: string;
1214
amountIn: BN;
@@ -27,36 +29,39 @@ export function TxAssetItem({
2729
() => ASSET_LIST.find((i) => i.assetId === assetId),
2830
[assetId],
2931
);
30-
if (!asset) {
31-
throw new Error(`Asset not found: ${assetId}`);
32-
}
32+
const assetName = asset?.name ?? 'Unknown';
33+
const assetSymbol = asset?.symbol ?? null;
3334
return (
3435
<Card {...props} className={cx('gap-2 pb-2', className)}>
3536
<EntityItem className="px-4 pb-4 border-b border-border">
3637
<EntityItem.Slot>
37-
<Image
38-
alt={asset.name}
39-
height={ICON_SIZE}
40-
src={asset.icon as string}
41-
width={ICON_SIZE}
42-
/>
38+
{asset?.icon ? (
39+
<Image
40+
src={asset.icon as string}
41+
width={ICON_SIZE}
42+
height={ICON_SIZE}
43+
alt={asset.name}
44+
/>
45+
) : (
46+
<TxIcon type="Mint" status="Submitted" />
47+
)}
4348
</EntityItem.Slot>
44-
<EntityItem.Info id={asset.assetId} title={asset.name} />
49+
<EntityItem.Info id={assetId} title={assetName} />
4550
</EntityItem>
4651
<HStack className="px-4 justify-between">
4752
<Text
4853
className="text-sm"
4954
iconColor="text-success"
5055
leftIcon={IconArrowUp}
5156
>
52-
{bn(amountIn).format({ units: 4 })} {asset.symbol}
57+
{bn(amountIn).format()} {assetSymbol}
5358
</Text>
5459
<Text
5560
className="text-sm"
5661
iconColor="text-error"
5762
leftIcon={IconArrowDown}
5863
>
59-
{bn(amountOut).format({ units: 4 })} {asset.symbol}
64+
{bn(amountOut).format()} {assetSymbol}
6065
</Text>
6166
</HStack>
6267
</Card>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use client';
2+
3+
import type { BreadcrumbProps } from '@fuels/ui';
4+
import {
5+
Breadcrumb,
6+
BreadcrumbItem,
7+
BreadcrumbLink,
8+
Copyable,
9+
Icon,
10+
shortAddress,
11+
} from '@fuels/ui';
12+
import { IconHome } from '@tabler/icons-react';
13+
import Link from 'next/link';
14+
15+
import type { TransactionNode } from '../../types';
16+
17+
type TxBreadcrumbProps = BreadcrumbProps & {
18+
transaction: TransactionNode;
19+
};
20+
21+
export function TxBreadcrumb({ transaction: tx, ...props }: TxBreadcrumbProps) {
22+
return (
23+
<Breadcrumb {...props}>
24+
<BreadcrumbLink asChild>
25+
<Link href="/">
26+
<Icon icon={IconHome} size={24} color="text-muted" />
27+
</Link>
28+
</BreadcrumbLink>
29+
<BreadcrumbItem>
30+
<Copyable value={tx.id}>{shortAddress(tx.id)}</Copyable>
31+
</BreadcrumbItem>
32+
</Breadcrumb>
33+
);
34+
}

packages/app/src/systems/Transaction/component/TxCard/TxCard.tsx

+32-29
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
IconTransfer,
1010
IconUsers,
1111
} from '@tabler/icons-react';
12+
import Link from 'next/link';
1213
import { tv } from 'tailwind-variants';
1314

1415
import type { TransactionNode, TxStatus } from '../../types';
@@ -22,35 +23,37 @@ export function TxCard({ transaction: tx, className, ...props }: TxCardProps) {
2223
const classes = styles();
2324
const title = tx.title as string;
2425
return (
25-
<Card {...props} className={classes.root({ className })}>
26-
<Card.Header>
27-
<EntityItem>
28-
<EntityItem.Slot>
29-
<TxIcon status={tx.statusType as TxStatus} type={title} />
30-
</EntityItem.Slot>
31-
<EntityItem.Info id={tx.id} title={title} />
32-
</EntityItem>
33-
</Card.Header>
34-
<Card.Body className={classes.body()}>
35-
<Flex className={classes.row()} justify="between">
36-
<Text leftIcon={IconUsers}>{tx.totalAccounts} accounts</Text>
37-
</Flex>
38-
<Flex className={classes.row()} justify="between">
39-
<Text leftIcon={IconTransfer}>{tx.totalOperations} operations</Text>
40-
<Text className={classes.small()}>
41-
<Badge color={TX_INTENT_MAP[tx.statusType as string]}>
42-
{tx.statusType}
43-
</Badge>
44-
</Text>
45-
</Flex>
46-
<Flex className={classes.row()} justify="between">
47-
<Text leftIcon={IconCoins}>{tx.totalAssets} assets</Text>
48-
<Text className={classes.small()} leftIcon={IconGasStation}>
49-
{bn(tx.gasUsed).format({ units: 3 })} ETH
50-
</Text>
51-
</Flex>
52-
</Card.Body>
53-
</Card>
26+
<Link href={`/tx/${tx.id}`}>
27+
<Card {...props} className={classes.root({ className })}>
28+
<Card.Header>
29+
<EntityItem>
30+
<EntityItem.Slot>
31+
<TxIcon status={tx.statusType as TxStatus} type={title} />
32+
</EntityItem.Slot>
33+
<EntityItem.Info id={tx.id} title={title} />
34+
</EntityItem>
35+
</Card.Header>
36+
<Card.Body className={classes.body()}>
37+
<Flex className={classes.row()} justify="between">
38+
<Text leftIcon={IconUsers}>{tx.totalAccounts} accounts</Text>
39+
</Flex>
40+
<Flex className={classes.row()} justify="between">
41+
<Text leftIcon={IconTransfer}>{tx.totalOperations} operations</Text>
42+
<Text className={classes.small()}>
43+
<Badge color={TX_INTENT_MAP[tx.statusType as string]}>
44+
{tx.statusType}
45+
</Badge>
46+
</Text>
47+
</Flex>
48+
<Flex className={classes.row()} justify="between">
49+
<Text leftIcon={IconCoins}>{tx.totalAssets} assets</Text>
50+
<Text className={classes.small()} leftIcon={IconGasStation}>
51+
{bn(tx.gasUsed).format({ precision: 5 })} ETH
52+
</Text>
53+
</Flex>
54+
</Card.Body>
55+
</Card>
56+
</Link>
5457
);
5558
}
5659

0 commit comments

Comments
 (0)