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

refactor: render all operations in Simple Transaction Viewer #509

Merged
merged 11 commits into from
Sep 13, 2024
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Text } from '@fuels/ui';
import { IconArrowRight } from '@tabler/icons-react';

export function CountReceipt({ num, op }: { num: number; op: string }) {
const length = new Intl.NumberFormat('en-IN', {
minimumIntegerDigits: 2,
}).format(num);
const text = num > 1 ? `${op}s` : op;
return (
<Text
className="flex items-center gap-2 text-sm text-muted"
leftIcon={IconArrowRight}
iconSize={14}
>
{length} {text}
</Text>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Collapsible, cx } from '@fuels/ui';
import { memo, useState } from 'react';
import { TxReceiptBlock } from '~/systems/Transaction/component/TxScripts/TxReceiptBlock/TxReceiptBlock';
import { TxReceiptHeader } from '~/systems/Transaction/component/TxScripts/TxReceiptHeader/TxReceiptHeader';
import {
ReceiptContext,
type ReceiptItemContext,
} from '~/systems/Transaction/component/TxScripts/context';
import { styles } from './styles';

function _ReceiptItem({
receipt,
isIndented,
hasPanic,
className,
...props
}: ReceiptItemContext) {
const classes = styles({ indent: isIndented });
const [opened, setOpened] = useState(false);

return (
<ReceiptContext.Provider value={{ receipt: receipt, isIndented, hasPanic }}>
<div
className={cx(classes.receiptRow({ className }), 'group')}
data-opened={opened}
>
<Collapsible
{...props}
opened={opened}
className="gap-0"
onOpenChange={setOpened}
>
<TxReceiptHeader />
<TxReceiptBlock />
</Collapsible>
</div>
</ReceiptContext.Provider>
);
}

export const ReceiptItem = memo(_ReceiptItem);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { tv } from 'tailwind-variants';

export const styles = tv({
slots: {
receiptRow: 'peer relative',
},
variants: {
indent: {
true: {
receiptRow: [
'ml-5 before:absolute before:top-[-35px] before:left-[-20px]',
'tablet:ml-10 tablet:before:left-[-40px]',
'before:bottom-[20px] before:right-[100%]',
'before:content-[""] before:block before:border-l before:border-b',
'before:border-border before:border-dashed before:rounded-bl',
'[&[data-opened=true]:before+&]:top-[-120px]',
],
},
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { GQLOperationReceipt } from '@fuel-explorer/graphql';
import { memo } from 'react';
import { ReceiptItem } from '~/systems/Transaction/component/TxScripts/ReceiptItem/ReceiptItem';
import { styles } from './styles';
import { ReceiptItemRProps } from './types';

function _ReceiptItemR(props: ReceiptItemRProps) {
const classes = styles();
return (
<>
{props.receipts?.map((sub, j) => (
<div
key={`${j}-${sub?.item?.receiptType ?? ''}`}
data-nested="true"
className={classes.operationChild()}
>
<ReceiptItem
isIndented
receipt={sub as GQLOperationReceipt}
hasPanic={props.hasPanic}
/>
{sub?.receipts && sub?.receipts?.length > 0 && (
<ReceiptItemR receipts={sub?.receipts} hasPanic={props.hasPanic} />
)}
</div>
))}
</>
);
}

export const ReceiptItemR = memo(_ReceiptItemR);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { tv } from 'tailwind-variants';

export const styles = tv({
slots: {
operationChild: [
'relative flex flex-col gap-3 ml-5',
'tablet:ml-10',
'[&[data-nested=true]]:before:absolute',
'[&[data-nested=true]]:before:content-[""]',
'[&[data-nested=true]]:before:block',
'[&[data-nested=true]]:before:border-l',
'[&[data-nested=true]]:before:border-border',
'[&[data-nested=true]]:before:border-dashed',
'[&[data-nested=true]]:before:top-[40px]',
'[&[data-nested=true]]:before:bottom-[20px]',
'[&[data-nested=true]]:before:left-0',
'[&[data-nested=true]]:before:right-0',
],
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { GQLOperationReceipt } from '@fuel-explorer/graphql';

export interface ReceiptItemRProps {
receipts?: GQLOperationReceipt[];
hasPanic: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { GQLReceipt, Maybe } from '@fuel-explorer/graphql/sdk';
import { Address, Code } from '@fuels/ui';
import { bn } from 'fuels';
import NextLink from 'next/link';
import { memo } from 'react';
import { Amount } from '~/systems/Core/components/Amount/Amount';
import {
ReceiptHeaderOperation,
ReceiptHeaderOperationDataType,
} from '~/systems/Transaction/component/TxScripts/TxReceiptHeader/types';

function _TxOperationHeader({
field,
index,
receipt,
}: {
field: ReceiptHeaderOperation;
index: number;
receipt: Maybe<GQLReceipt> | undefined;
}) {
if (
field == null ||
(field.requiredField && !receipt?.[field.requiredField])
) {
return null;
}

const key = `${field.label}-${index}`;
const value =
(field.field && receipt?.[field.field]) ||
(field.fieldFallback && receipt?.[field.fieldFallback]);
const formattedValue =
field.type === ReceiptHeaderOperationDataType.HEX_ADDRESS &&
value &&
!String(value)?.startsWith('0x')
? bn(value).toHex()
: value;

if (formattedValue == null) {
return null;
}

if (field.type === ReceiptHeaderOperationDataType.AMOUNT) {
return (
<Amount
iconSize={16}
assetId={receipt?.assetId}
value={bn(value)}
className="text-xs tablet:text-sm"
/>
);
}

return String(formattedValue)?.startsWith('0x') ? (
<Address
value={formattedValue}
className="text-xs tablet:text-sm font-mono"
prefix={field.label}
linkProps={{
as: NextLink,
href:
field?.hrefFactory?.(formattedValue) ??
`/contract/${formattedValue}/assets`,
}}
/>
) : (
<Code
key={key}
className="text-xs tablet:text-sm font-mono bg-transparent text-muted p-0"
color="gray"
>
{`${field.label} ${value}`}
</Code>
);
}

export const TxOperationHeader = memo(_TxOperationHeader);
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Address, VStack } from '@fuels/ui';
import { bn } from 'fuels';
import NextLink from 'next/link';
import { useContext } from 'react';
import { Amount } from '~/systems/Core/components/Amount/Amount';
import { ReceiptContext } from '~/systems/Transaction/component/TxScripts/context';

export function TxReceiptAmount() {
const { receipt: item } = useContext(ReceiptContext);
const receipt = item?.item;
const assetId = receipt?.assetId ?? '';
const amount = bn(receipt?.amount);
const contract = receipt?.to ?? receipt?.contractId ?? null;

return (
amount.gt(0) && (
<VStack className="gap-1 items-end mobile:max-tablet:hidden">
<Amount
iconSize={16}
assetId={assetId}
value={amount}
className="text-xs tablet:text-sm"
/>
<Address
iconSize={14}
value={assetId}
className="text-xs tablet:text-sm font-mono"
linkProps={{
as: NextLink,
href: `/contract/${contract}/assets`,
}}
/>
</VStack>
)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Badge } from '@fuels/ui';
import { useContext } from 'react';
import { ReceiptContext } from '~/systems/Transaction/component/TxScripts/context';

import { getBadgeColor } from './utils';

export function TxReceiptBadge() {
const { receipt, hasPanic } = useContext(ReceiptContext);
const type = receipt?.item?.receiptType ?? 'UNKNOWN';
const color = getBadgeColor(Boolean(hasPanic), receipt?.item);
return (
<div>
<Badge size="1" className="font-mono" variant="ghost" color={color}>
{type}
</Badge>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GQLReceiptType } from '@fuel-explorer/graphql/sdk';

export const RETURN_TYPES = [GQLReceiptType.Return, GQLReceiptType.ReturnData];
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
GQLReceiptType,
type GQLTransactionReceiptFragment,
type Maybe,
} from '@fuel-explorer/graphql/sdk';
import { RETURN_TYPES } from './constants';

export function getBadgeColor(
hasError: boolean,
receipt?: Maybe<GQLTransactionReceiptFragment>,
) {
const type = receipt?.receiptType ?? 'UNKNOWN';
if (type === GQLReceiptType.Revert || type === GQLReceiptType.Panic) {
return 'red';
}
if (
RETURN_TYPES.some((t) => t === type) &&
!hasError &&
!receipt?.contractId
) {
return 'green';
}
return 'gray';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Collapsible, ScrollArea } from '@fuels/ui';
import { useContext } from 'react';
import { useMeasure } from 'react-use';
import { JsonViewer } from '~/systems/Core/components/JsonViewer/JsonViewer';
import { ReceiptContext } from '~/systems/Transaction/component/TxScripts/context';
import { parseTXScriptJson } from '~/systems/Transaction/component/TxScripts/utils';

import { styles } from './styles';

export function TxReceiptBlock() {
const { receipt } = useContext(ReceiptContext);
const classes = styles();
const [ref, { width }] = useMeasure();
return (
<Collapsible.Content
ref={ref as React.Ref<HTMLDivElement>}
className={classes.utxos()}
>
<ScrollArea style={{ width }}>
<JsonViewer data={parseTXScriptJson(receipt?.item)} />
</ScrollArea>
</Collapsible.Content>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { tv } from 'tailwind-variants';

export const styles = tv({
slots: {
utxos: 'bg-gray-3 mx-3 my-3 p-0 rounded',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GQLReceiptType } from '@fuel-explorer/graphql/sdk';
import { Collapsible, VStack } from '@fuels/ui';
import { useContext } from 'react';
import { TxOperationHeader } from '~/systems/Transaction/component/TxScripts/TxOperationHeader';
import { TxReceiptAmount } from '~/systems/Transaction/component/TxScripts/TxReceiptAmount';
import { TxReceiptBadge } from '~/systems/Transaction/component/TxScripts/TxReceiptBadge/TxReceiptBadge';
import { ReceiptContext } from '~/systems/Transaction/component/TxScripts/context';

import { RECEIPT_FIELDS_MAP } from './constants';
import { styles } from './styles';

export function TxReceiptHeader() {
const { receipt: item } = useContext(ReceiptContext);
const receipt = item?.item;
const classes = styles();
const type = (receipt?.receiptType ?? 'UNKNOWN') as GQLReceiptType;
const fields = RECEIPT_FIELDS_MAP[type] || [];

return (
<Collapsible.Header className={classes.header()}>
<TxReceiptBadge />
<VStack className="flex-1 gap-[2px]">
{!fields?.length && <TxReceiptBadge />}

{fields?.map((field, index) => (
<TxOperationHeader
key={`operation-header-${field.type}-${field.requiredField ?? ''}-${
field.field
}-${field.fieldFallback}`}
field={field}
index={index}
receipt={receipt}
/>
))}
</VStack>
<TxReceiptAmount />
</Collapsible.Header>
);
}
Loading
Loading