diff --git a/packages/app/src/systems/Core/components/CopyButton/CopyButton.tsx b/packages/app/src/systems/Core/components/CopyButton/CopyButton.tsx new file mode 100644 index 000000000..038f625f9 --- /dev/null +++ b/packages/app/src/systems/Core/components/CopyButton/CopyButton.tsx @@ -0,0 +1,33 @@ +import type { ButtonProps } from '@fuels/ui'; +import { Button, Copyable } from '@fuels/ui'; + +type CopyButtonProps = ButtonProps & { + value: string; + text?: string; +}; + +const COPY_ICON_SIZES = { + '1': 15, + '2': 19, + '3': 24, + '4': 29, +}; + +const CopyButton = ({ value, text = 'Copy', ...props }: CopyButtonProps) => { + const size = props.size || '1'; + const variant = props.variant || 'soft'; + + return ( + + ); +}; + +export default CopyButton; diff --git a/packages/app/src/systems/Core/components/JsonViewer/JsonViewer.tsx b/packages/app/src/systems/Core/components/JsonViewer/JsonViewer.tsx new file mode 100644 index 000000000..a802c1fe7 --- /dev/null +++ b/packages/app/src/systems/Core/components/JsonViewer/JsonViewer.tsx @@ -0,0 +1,34 @@ +import { useRadixTheme } from '@fuels/ui'; +import { + JsonView, + collapseAllNested, + darkStyles, + defaultStyles, +} from 'react-json-view-lite'; +import { tv } from 'tailwind-variants'; + +export type JsonViewerProps = { + data: object | unknown[]; +}; + +export function JsonViewer({ data, ...props }: JsonViewerProps) { + const classes = styles(); + const ctx = useRadixTheme(); + return ( + + ); +} + +const styles = tv({ + slots: { + json: 'bg-transparent text-sm py-2 px-1', + }, +}); diff --git a/packages/app/src/systems/Core/components/ViewMode/ViewMode.tsx b/packages/app/src/systems/Core/components/ViewMode/ViewMode.tsx new file mode 100644 index 000000000..abfd400d7 --- /dev/null +++ b/packages/app/src/systems/Core/components/ViewMode/ViewMode.tsx @@ -0,0 +1,53 @@ +import { Flex, Text } from '@fuels/ui'; +import { tv } from 'tailwind-variants'; + +export enum ViewModes { + Simple = 'Simple', + Advanced = 'Advanced', +} + +export type ViewModeProps = { + mode: ViewModes; + onChange: (mode: ViewModes) => void; +}; + +export function ViewMode({ mode, onChange }: ViewModeProps) { + const classes = styles(); + + return ( + + onChange(ViewModes.Simple)} + > + Simple + + onChange(ViewModes.Advanced)} + > + Advanced + + + ); +} + +const styles = tv({ + slots: { + root: 'bg-gray-3 p-1 rounded h-9', + viewItem: [ + 'flex-1 rounded cursor-pointer', + 'data-[mode=Simple]:px-6', + 'data-[mode=Advanced]:px-3', + 'data-[active=true]:bg-gray-1 data-[active=true]:cursor-default', + ], + }, +}); diff --git a/packages/app/src/systems/Transaction/component/TxScreen/TxScreenAdvanced.tsx b/packages/app/src/systems/Transaction/component/TxScreen/TxScreenAdvanced.tsx new file mode 100644 index 000000000..ceb8fdcc6 --- /dev/null +++ b/packages/app/src/systems/Transaction/component/TxScreen/TxScreenAdvanced.tsx @@ -0,0 +1,66 @@ +'use client'; + +import type { Maybe } from '@fuel-explorer/graphql'; +import { Button, Card, Flex, ScrollArea, Text, VStack } from '@fuels/ui'; +import { IconChevronDown } from '@tabler/icons-react'; +import { useState } from 'react'; +import { tv } from 'tailwind-variants'; +import CopyButton from '~/systems/Core/components/CopyButton/CopyButton'; +import { JsonViewer } from '~/systems/Core/components/JsonViewer/JsonViewer'; + +import type { TransactionNode } from '../../types'; + +type TxScreenProps = { + transaction?: Maybe; +}; + +export function TxScreenAdvanced({ transaction: tx }: TxScreenProps) { + const [compact, setCompact] = useState(true); + const classes = styles(); + if (!tx) return null; + + return ( + + + + + + JSON + + + + + + + + + + + + + ); +} + +const styles = tv({ + slots: { + root: [ + 'group transition-[max-height] max-h-[75vh]', + 'data-[compact=true]:max-h-[400px]', + ], + cardHeader: 'border-b border-card-border py-3 flex-none', + cardMiddle: 'flex-1 [&_.rt-ScrollAreaViewport_>div>div]:max-w-[1120px]', + cardFooter: [ + 'border-t border-card-border', + 'py-3 self-stretch flex-none justify-center', + 'group-data-[compact=true]:[&_svg]:-rotate-180 [&_svg]:transition-transform', + ], + }, +}); diff --git a/packages/app/src/systems/Transaction/component/TxScreen/TxScreenSimple.tsx b/packages/app/src/systems/Transaction/component/TxScreen/TxScreenSimple.tsx new file mode 100644 index 000000000..2ab9ec8ef --- /dev/null +++ b/packages/app/src/systems/Transaction/component/TxScreen/TxScreenSimple.tsx @@ -0,0 +1,144 @@ +'use client'; + +import type { + GroupedInput, + GroupedOutput, + Maybe, +} from '@fuel-explorer/graphql'; +import { + Badge, + Box, + EntityItem, + Flex, + Grid, + Heading, + Icon, + Text, + VStack, +} from '@fuels/ui'; +import { IconArrowDown } from '@tabler/icons-react'; +import { bn } from 'fuels'; +import { EmptyCard } from '~/systems/Core/components/EmptyCard/EmptyCard'; + +import { TxInfo } from '../../component/TxInfo/TxInfo'; +import { TxInput } from '../../component/TxInput/TxInput'; +import { TxOutput } from '../../component/TxOutput/TxOutput'; +import type { TransactionNode, TxStatus } from '../../types'; +import { TX_INTENT_MAP, TxIcon } from '../TxIcon/TxIcon'; +import { TxScripts } from '../TxScripts/TxScripts'; + +type TxScreenProps = { + transaction: TransactionNode; +}; + +export function TxScreenSimple({ transaction: tx }: TxScreenProps) { + const hasInputs = tx.groupedInputs?.length ?? 0 > 0; + const hasOutputs = tx.groupedOutputs?.length ?? 0 > 0; + const title = tx.title as string; + + return ( + + + + + + + + + + + + {tx.statusType} + + + + + + + {tx.time?.fromNow} + + {tx.blockHeight && #{tx.blockHeight}} + + {bn(tx.gasUsed).format()} + + + + + + + + Inputs + + {hasInputs ? ( + tx.groupedInputs?.map((input) => ( + + )) + ) : ( + + No Inputs + + This transaction does not have any inputs. + + + )} + + + + + + + + + + + Outputs + + {hasOutputs ? ( + tx.groupedOutputs?.map((output) => ( + + )) + ) : ( + + No Outputs + + This transaction does not have any outputs. + + + )} + + + + + ); +} + +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; +} + +function getOutputId(output?: Maybe) { + if (!output) return 0; + if (output.type === 'ContractOutput') return output.inputIndex; + if (output.type === 'ContractCreated') return output.contract?.id ?? 0; + if (output.type === 'MessageOutput') return output.recipient; + return output.assetId; +} diff --git a/packages/app/src/systems/Transaction/component/TxScripts/TxScripts.tsx b/packages/app/src/systems/Transaction/component/TxScripts/TxScripts.tsx index b7be12d6c..1752fd2fb 100644 --- a/packages/app/src/systems/Transaction/component/TxScripts/TxScripts.tsx +++ b/packages/app/src/systems/Transaction/component/TxScripts/TxScripts.tsx @@ -13,7 +13,6 @@ import { Icon, Text, VStack, - useRadixTheme, } from '@fuels/ui'; import { IconFold, @@ -23,15 +22,10 @@ import { import { bn } from 'fuels'; import Image from 'next/image'; import { useState } from 'react'; -import { - JsonView, - defaultStyles, - darkStyles, - collapseAllNested, -} from 'react-json-view-lite'; import { tv } from 'tailwind-variants'; import { useAsset } from '~/systems/Asset/hooks/useAsset'; import { EmptyCard } from '~/systems/Core/components/EmptyCard/EmptyCard'; +import { JsonViewer } from '~/systems/Core/components/JsonViewer/JsonViewer'; import type { TransactionNode } from '../../types'; @@ -52,7 +46,6 @@ export type TxScriptRowProps = BaseProps<{ function TxScriptRow({ item }: TxScriptRowProps) { const asset = useAsset(item.assetId); const classes = styles(); - const ctx = useRadixTheme(); const amount = bn(item.amount); const isDanger = item.receiptType === 'PANIC' || @@ -97,14 +90,7 @@ function TxScriptRow({ item }: TxScriptRowProps) { - + ); @@ -198,7 +184,6 @@ const styles = tv({ slots: { icon: 'transition-transform group-data-[state=closed]:hover:rotate-180 group-data-[state=open]:rotate-180', utxos: 'bg-gray-3 mx-3 mb-3 p-0 rounded', - json: 'bg-transparent text-sm py-2 px-1', lines: [ 'relative flex-1 border-t border-b border-border', 'before:h-[1px] before:absolute before:top-1/2 before:left-0 before:w-full before:bg-border', diff --git a/packages/app/src/systems/Transaction/screens/TxScreen/TxScreen.tsx b/packages/app/src/systems/Transaction/screens/TxScreen/TxScreen.tsx index b67f5da16..09e94a75f 100644 --- a/packages/app/src/systems/Transaction/screens/TxScreen/TxScreen.tsx +++ b/packages/app/src/systems/Transaction/screens/TxScreen/TxScreen.tsx @@ -1,155 +1,39 @@ 'use client'; -import type { - GroupedInput, - GroupedOutput, - Maybe, -} from '@fuel-explorer/graphql'; -import { - Badge, - Box, - EntityItem, - Flex, - Grid, - Heading, - Icon, - VStack, - Text, - Address, -} from '@fuels/ui'; -import { IconArrowDown, IconChecklist } from '@tabler/icons-react'; -import { bn } from 'fuels'; -import { EmptyCard } from '~/systems/Core/components/EmptyCard/EmptyCard'; +import type { Maybe } from '@fuel-explorer/graphql'; +import { Address, Flex, VStack } from '@fuels/ui'; +import { IconChecklist } from '@tabler/icons-react'; +import { useState } from 'react'; import { PageTitle } from '~/systems/Core/components/PageTitle/PageTitle'; +import { + ViewMode, + ViewModes, +} from '~/systems/Core/components/ViewMode/ViewMode'; -import { TX_INTENT_MAP, TxIcon } from '../../component/TxIcon/TxIcon'; -import { TxInfo } from '../../component/TxInfo/TxInfo'; -import { TxInput } from '../../component/TxInput/TxInput'; -import { TxOutput } from '../../component/TxOutput/TxOutput'; -import { TxScripts } from '../../component/TxScripts/TxScripts'; -import type { TransactionNode, TxStatus } from '../../types'; +import { TxScreenAdvanced } from '../../component/TxScreen/TxScreenAdvanced'; +import { TxScreenSimple } from '../../component/TxScreen/TxScreenSimple'; +import type { TransactionNode } from '../../types'; type TxScreenProps = { transaction?: Maybe; }; export function TxScreen({ transaction: tx }: TxScreenProps) { + const [viewMode, setViewMode] = useState(ViewModes.Simple); + if (!tx) return null; - const hasInputs = tx.groupedInputs?.length ?? 0 > 0; - const hasOutputs = tx.groupedOutputs?.length ?? 0 > 0; - const title = tx.title as string; return ( - + }> Transaction
+ + + - - - - - - - - - - - - {tx.statusType} - - - - - - - {tx.time?.fromNow} - - {tx.blockHeight && ( - #{tx.blockHeight} - )} - - {bn(tx.gasUsed).format()} - - - - - - - - Inputs - - {hasInputs ? ( - tx.groupedInputs?.map((input) => ( - - )) - ) : ( - - No Inputs - - This transaction does not have any inputs. - - - )} - - - - - - - - - - - Outputs - - {hasOutputs ? ( - tx.groupedOutputs?.map((output) => ( - - )) - ) : ( - - No Outputs - - This transaction does not have any outputs. - - - )} - - - - + {viewMode === ViewModes.Simple && } + {viewMode === ViewModes.Advanced && } ); } - -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; -} - -function getOutputId(output?: Maybe) { - if (!output) return 0; - if (output.type === 'ContractOutput') return output.inputIndex; - if (output.type === 'ContractCreated') return output.contract?.id ?? 0; - if (output.type === 'MessageOutput') return output.recipient; - return output.assetId; -} diff --git a/packages/graphql/src/queries/tx-fragments.graphql b/packages/graphql/src/queries/tx-fragments.graphql index 1b47c0cea..9dfa38f7a 100644 --- a/packages/graphql/src/queries/tx-fragments.graphql +++ b/packages/graphql/src/queries/tx-fragments.graphql @@ -5,17 +5,32 @@ fragment ContractItem on Contract { fragment TransactionStatus on TransactionStatus { __typename + ... on SqueezedOutStatus { + reason + } ... on SuccessStatus { time block { id header { + id + height daHeight + applicationHash + messageReceiptRoot + messageReceiptCount + time } } + programState { + data + } } ... on FailureStatus { time + programState { + data + } } ... on SubmittedStatus { time @@ -32,11 +47,11 @@ fragment TransactionInput on Input { predicateData txPointer utxoId + witnessIndex } ... on InputContract { utxoId balanceRoot - stateRoot txPointer contract { ...ContractItem @@ -62,6 +77,7 @@ fragment TransactionOutput on Output { } ... on ContractOutput { inputIndex + balanceRoot } ... on ChangeOutput { to diff --git a/packages/ui/src/components/Collapsible/Collapsible.tsx b/packages/ui/src/components/Collapsible/Collapsible.tsx index 7921f53dc..87f459326 100644 --- a/packages/ui/src/components/Collapsible/Collapsible.tsx +++ b/packages/ui/src/components/Collapsible/Collapsible.tsx @@ -130,7 +130,7 @@ const styles = tv({ slots: { root: 'py-[10px]', header: 'group grid grid-cols-[1fr_auto] grid-rows-1 gap-4 items-center', - icon: 'transition-transform group-data-[state=closed]:hover:-rotate-180 group-data-[state=opened]:-rotate-180', + icon: 'transition-transform group-data-[state=opened]:-rotate-180', content: 'mx-4 mb-2 border border-border', body: '', title: 'flex items-center gap-2 text-sm font-medium', diff --git a/packages/ui/src/hooks/useVariants.tsx b/packages/ui/src/hooks/useVariants.tsx index b7954f0c4..8122c9f71 100644 --- a/packages/ui/src/hooks/useVariants.tsx +++ b/packages/ui/src/hooks/useVariants.tsx @@ -2,7 +2,13 @@ import { cx } from '../utils/css'; -export type Variant = 'solid' | 'ghost' | 'outline' | 'surface' | 'link'; +export type Variant = + | 'solid' + | 'ghost' + | 'outline' + | 'surface' + | 'link' + | 'soft'; export type VariantProps = { className?: string; variant?: V;