Skip to content

Commit 0c41159

Browse files
committed
refactor(tx): extract reusable decode presentation
1 parent e6c0db1 commit 0c41159

File tree

4 files changed

+178
-38
lines changed

4 files changed

+178
-38
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useMemo } from 'react'
2+
3+
import type { DecodedContractInput } from '@app/utils/contract-input-decoder'
4+
import SubCardItem from '../SubCardItem'
5+
import type { TxItemVariant } from './TxItem.types'
6+
7+
type Props = Readonly<{
8+
variant: TxItemVariant
9+
shouldDecodeInput: boolean
10+
decodedInput: DecodedContractInput | null
11+
}>
12+
13+
export default function DecodedDataSection({ variant, shouldDecodeInput, decodedInput }: Props) {
14+
const decodedInputJson = useMemo(() => {
15+
if (!decodedInput || decodedInput.status !== 'decoded') return null
16+
return JSON.stringify(decodedInput.value, null, 2)
17+
}, [decodedInput])
18+
19+
if (!shouldDecodeInput || !decodedInput) return null
20+
21+
return (
22+
<SubCardItem
23+
title="Decoded Data"
24+
variant={variant}
25+
content={
26+
<div className="min-w-0 flex-1">
27+
{decodedInput.status === 'decoded' ? (
28+
<pre className="max-h-[20rem] overflow-auto break-all rounded-8 bg-primary-60 p-12 font-space text-sm text-gray-50">
29+
{decodedInputJson}
30+
</pre>
31+
) : (
32+
<>
33+
<p className="break-all font-space text-sm text-gray-50">
34+
Unable to decode this contract input.
35+
</p>
36+
{decodedInput.message && (
37+
<p className="text-gray-40 mt-8 break-all font-space text-xs">
38+
{decodedInput.message}
39+
</p>
40+
)}
41+
</>
42+
)}
43+
</div>
44+
}
45+
/>
46+
)
47+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useMemo, useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
4+
import { clsxTwMerge, formatHex } from '@app/utils'
5+
import SubCardItem from '../SubCardItem'
6+
import type { TxItemVariant } from './TxItem.types'
7+
8+
type Props = Readonly<{
9+
variant: TxItemVariant
10+
showExtendedDetails: boolean
11+
inputData: string | null | undefined
12+
}>
13+
14+
export default function InputDataSection({ variant, showExtendedDetails, inputData }: Props) {
15+
const { t } = useTranslation('network-page')
16+
const [inputDataFormat, setInputDataFormat] = useState<'base64' | 'hex'>('hex')
17+
const inputDataHex = useMemo(() => formatHex(inputData ?? undefined), [inputData])
18+
19+
if (!showExtendedDetails || !inputData || inputData.length === 0) return null
20+
21+
return (
22+
<SubCardItem
23+
title={t('data')}
24+
variant={variant}
25+
content={
26+
<div className="min-w-0 flex-1">
27+
<p className="break-all rounded-8 bg-primary-60 p-12 font-space text-sm text-gray-50">
28+
<span className="float-right mb-4 ml-8 inline-flex overflow-hidden rounded-8 border border-gray-50 bg-primary-80">
29+
{(['hex', 'base64'] as const).map((format, index) => (
30+
<button
31+
key={format}
32+
type="button"
33+
onClick={() => setInputDataFormat(format)}
34+
className={clsxTwMerge(
35+
'px-8 py-2 text-xs font-medium transition-colors',
36+
index > 0 && 'border-l border-gray-50',
37+
inputDataFormat === format
38+
? 'bg-primary-60 text-white'
39+
: 'bg-transparent text-gray-50 hover:bg-primary-70 hover:text-white'
40+
)}
41+
>
42+
{format === 'hex' ? 'Hex' : 'Base64'}
43+
</button>
44+
))}
45+
</span>
46+
{inputDataFormat === 'hex' ? `0x${inputDataHex}` : inputData}
47+
</p>
48+
</div>
49+
}
50+
/>
51+
)
52+
}

src/pages/network/components/TxItem/TransactionDetails.tsx

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
import { useMemo, useState } from 'react'
1+
import { useMemo } from 'react'
22
import { useTranslation } from 'react-i18next'
33

44
import { Alert } from '@app/components/ui'
55
import { COPY_BUTTON_TYPES, CopyTextButton } from '@app/components/ui/buttons'
66
import { useGetAddressName } from '@app/hooks'
77
import { useGetSmartContractsQuery } from '@app/store/apis/qubic-static'
88
import type { QueryServiceTransaction } from '@app/store/apis/query-service'
9-
import { clsxTwMerge, formatDate, formatHex, formatString } from '@app/utils'
9+
import { clsxTwMerge, formatDate, formatString } from '@app/utils'
1010
import { getProcedureName } from '@app/utils/qubic'
11-
import { type AssetTransfer, type Transfer, isSmartContractTx } from '@app/utils/qubic-ts'
11+
import { type AssetTransfer, type Transfer } from '@app/utils/qubic-ts'
1212
import AddressLink from '../AddressLink'
1313
import SubCardItem from '../SubCardItem'
1414
import TickLink from '../TickLink'
1515
import TxLink from '../TxLink'
16+
import DecodedDataSection from './DecodedDataSection'
17+
import InputDataSection from './InputDataSection'
1618
import TransferList from './TransferList/TransferList'
1719
import type { TxItemVariant } from './TxItem.types'
20+
import { useDecodedContractInput } from './useDecodedContractInput'
1821

1922
type Props = {
2023
readonly txDetails: Omit<QueryServiceTransaction, 'inputSize' | 'moneyFlew' | 'timestamp'>
@@ -51,11 +54,9 @@ export default function TransactionDetails({
5154
}: Props) {
5255
const { t } = useTranslation('network-page')
5356
const { data: smartContracts } = useGetSmartContractsQuery()
54-
const [inputDataFormat, setInputDataFormat] = useState<'base64' | 'hex'>('hex')
5557

5658
const isSecondaryVariant = variant === 'secondary'
5759
const { date, time } = useMemo(() => formatDate(timestamp, { split: true }), [timestamp])
58-
const inputDataHex = useMemo(() => formatHex(inputData), [inputData])
5960

6061
const destAddress = assetDetails?.newOwnerAndPossessor ?? destination
6162
const sourceAddressNameData = useGetAddressName(source)
@@ -65,14 +66,20 @@ export default function TransactionDetails({
6566
() => getProcedureName(destination, inputType, smartContracts),
6667
[destination, inputType, smartContracts]
6768
)
69+
const { isContractTransaction, shouldDecodeInput, decodedInput } = useDecodedContractInput({
70+
showExtendedDetails,
71+
destination,
72+
inputType,
73+
inputData
74+
})
6875

6976
const transactionTypeDisplay = useMemo(() => {
7077
const baseType = formatString(inputType)
71-
const txCategory = isSmartContractTx(destination, inputType) ? 'SC' : 'Standard'
78+
const txCategory = isContractTransaction ? 'SC' : 'Standard'
7279
return procedureName
7380
? `${baseType} ${txCategory} (${procedureName})`
7481
: `${baseType} ${txCategory}`
75-
}, [inputType, destination, procedureName])
82+
}, [inputType, isContractTransaction, procedureName])
7683

7784
return (
7885
<TransactionDetailsWrapper variant={variant}>
@@ -184,37 +191,17 @@ export default function TransactionDetails({
184191
/>
185192
)}
186193

187-
{showExtendedDetails && inputData && inputData.length > 0 && (
188-
<SubCardItem
189-
title={t('data')}
190-
variant={variant}
191-
content={
192-
<div className="min-w-0 flex-1">
193-
<p className="break-all rounded-8 bg-primary-60 p-12 font-space text-sm text-gray-50">
194-
<span className="float-right mb-4 ml-8 inline-flex overflow-hidden rounded-8 border border-gray-50 bg-primary-80">
195-
{(['hex', 'base64'] as const).map((format, index) => (
196-
<button
197-
key={format}
198-
type="button"
199-
onClick={() => setInputDataFormat(format)}
200-
className={clsxTwMerge(
201-
'px-8 py-2 text-xs font-medium transition-colors',
202-
index > 0 && 'border-l border-gray-50',
203-
inputDataFormat === format
204-
? 'bg-primary-60 text-white'
205-
: 'bg-transparent text-gray-50 hover:bg-primary-70 hover:text-white'
206-
)}
207-
>
208-
{format === 'hex' ? 'Hex' : 'Base64'}
209-
</button>
210-
))}
211-
</span>
212-
{inputDataFormat === 'hex' ? `0x${inputDataHex}` : inputData}
213-
</p>
214-
</div>
215-
}
216-
/>
217-
)}
194+
<DecodedDataSection
195+
variant={variant}
196+
shouldDecodeInput={shouldDecodeInput}
197+
decodedInput={decodedInput}
198+
/>
199+
200+
<InputDataSection
201+
variant={variant}
202+
showExtendedDetails={showExtendedDetails}
203+
inputData={inputData}
204+
/>
218205

219206
<TransferList entries={entries} variant={variant} />
220207
</TransactionDetailsWrapper>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useMemo } from 'react'
2+
3+
import {
4+
decodeContractInputData,
5+
type DecodedContractInput
6+
} from '@app/utils/contract-input-decoder'
7+
import { isSmartContractTx } from '@app/utils/qubic-ts'
8+
9+
type UseDecodedContractInputParams = Readonly<{
10+
showExtendedDetails: boolean
11+
destination: string
12+
inputType: number
13+
inputData: string | Uint8Array | number[] | null | undefined
14+
}>
15+
16+
type UseDecodedContractInputResult = Readonly<{
17+
isContractTransaction: boolean
18+
shouldDecodeInput: boolean
19+
decodedInput: DecodedContractInput | null
20+
}>
21+
22+
export const useDecodedContractInput = (
23+
params: UseDecodedContractInputParams
24+
): UseDecodedContractInputResult => {
25+
const isContractTransaction = useMemo(
26+
() => isSmartContractTx(params.destination, params.inputType),
27+
[params.destination, params.inputType]
28+
)
29+
30+
const shouldDecodeInput = useMemo(
31+
() =>
32+
params.showExtendedDetails &&
33+
isContractTransaction &&
34+
!!params.inputData &&
35+
params.inputData.length > 0 &&
36+
params.inputType > 0,
37+
[params.showExtendedDetails, isContractTransaction, params.inputData, params.inputType]
38+
)
39+
40+
const decodedInput = useMemo(() => {
41+
if (!shouldDecodeInput) return null
42+
return decodeContractInputData({
43+
inputType: params.inputType,
44+
inputData: params.inputData,
45+
destinationHint: params.destination
46+
})
47+
}, [shouldDecodeInput, params.inputData, params.inputType, params.destination])
48+
49+
return {
50+
isContractTransaction,
51+
shouldDecodeInput,
52+
decodedInput
53+
}
54+
}

0 commit comments

Comments
 (0)