Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2050fc1
feat: show malicious addresses in the Shield UI
vmosharova Dec 3, 2025
2878ad8
feat: (packages) show malicious addresses in the Shield UI
vmosharova Dec 3, 2025
5db1075
Merge branch 'dev' into feat/malicious-adresses-in-threat-review
vmosharova Dec 8, 2025
99acc54
chore: linting; fix tests
vmosharova Dec 8, 2025
34a5afe
fix: handle edge cases with undefined issues and empty arrays; fix tests
vmosharova Dec 8, 2025
6b680a2
fix: preserve all metadata fields (request_id, etc.) in the response …
vmosharova Dec 8, 2025
9cd4c31
Merge branch 'dev' into feat/malicious-adresses-in-threat-review
vmosharova Dec 9, 2025
e12b91e
feat(mobile): Add address property in the malicious threat builders a…
clovisdasilvaneto Dec 9, 2025
96e425b
feat(mobile): change the underlying layout of the AnalysisIssueDispla…
clovisdasilvaneto Dec 9, 2025
21d2e49
feat(mobile): extract reusable analysis address logic to useAnalysisA…
clovisdasilvaneto Dec 9, 2025
20b5a33
feat(mobile): move AddressItem wrapper to AnalysisPaper component in …
clovisdasilvaneto Dec 9, 2025
beea07f
fix(mobile): do not render 'Show All' component if there are 'issues'…
clovisdasilvaneto Dec 9, 2025
119e6ee
fix(mobile): unit tests
clovisdasilvaneto Dec 9, 2025
f8366ff
fix(mobile): prettier
clovisdasilvaneto Dec 9, 2025
a0f99b4
Merge branch 'dev' into feat/malicious-adresses-in-threat-review
clovisdasilvaneto Dec 9, 2025
5ab7555
fix(mobile): Fixed the index collision in the mobile issues analysis …
vmosharova Dec 9, 2025
a61aec7
refactor: remove obsolete description check as it is always present i…
vmosharova Dec 9, 2025
0375f51
fix: new styles for the Threat description
vmosharova Dec 10, 2025
e9cddb8
Merge branch 'dev' into feat/malicious-adresses-in-threat-review
vmosharova Dec 10, 2025
e0b44f3
Merge branch 'dev' into feat/malicious-adresses-in-threat-review
clovisdasilvaneto Dec 11, 2025
92dc220
fix(mobile): check result.issues truthiness and fix explorer link for…
vmosharova Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ import { AnalysisDetails } from './AnalysisDetails'
import { RecipientAnalysisBuilder, ContractAnalysisBuilder } from '@safe-global/utils/features/safe-shield/builders'
import { FullAnalysisBuilder } from '@safe-global/utils/features/safe-shield/builders'
import { faker } from '@faker-js/faker'
import type { Address } from '@/src/types/address'
import { SafeTransaction } from '@safe-global/types-kit'

describe('AnalysisDetails', () => {
const initialStore = {
activeSafe: {
address: '0x1234567890123456789012345678901234567890' as Address,
chainId: '1',
},
}

it('should render with default OK severity when no data is provided', () => {
const { getByText } = render(
<AnalysisDetails safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction} />,
{ initialStore },
)
expect(getByText('Checks passed')).toBeTruthy()
})
Expand All @@ -22,6 +31,7 @@ describe('AnalysisDetails', () => {
recipient={recipient}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
/>,
{ initialStore },
)

expect(getByText('Checks passed')).toBeTruthy()
Expand All @@ -33,9 +43,10 @@ describe('AnalysisDetails', () => {

const { getByText } = render(
<AnalysisDetails
contract={contract}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
contract={contract}
/>,
{ initialStore },
)

expect(getByText(/Review details|Issues found/i)).toBeTruthy()
Expand All @@ -46,9 +57,10 @@ describe('AnalysisDetails', () => {

const { getByText } = render(
<AnalysisDetails
threat={threat}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
threat={threat}
/>,
{ initialStore },
)

expect(getByText('Risk detected')).toBeTruthy()
Expand All @@ -64,10 +76,13 @@ describe('AnalysisDetails', () => {
const { getByText } = render(
<AnalysisDetails
recipient={recipient}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
contract={contract}
threat={threat}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
/>,
{
initialStore,
},
)

// Should show the highest severity (CRITICAL from threat)
Expand All @@ -84,10 +99,13 @@ describe('AnalysisDetails', () => {
const { getByText } = render(
<AnalysisDetails
recipient={recipient}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
contract={contract}
threat={threat}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
/>,
{
initialStore,
},
)

// Should show OK when all are safe
Expand All @@ -101,6 +119,7 @@ describe('AnalysisDetails', () => {
recipient={recipient}
safeTx={{ txHash: faker.finance.ethereumAddress() } as unknown as SafeTransaction}
/>,
{ initialStore },
)

// Should still render with default OK severity
Expand All @@ -110,7 +129,7 @@ describe('AnalysisDetails', () => {
it('should handle error state', () => {
const error = new Error('Test error')
const recipient: [undefined, Error, boolean] = [undefined, error, false]
const { getByText } = render(<AnalysisDetails recipient={recipient} />)
const { getByText } = render(<AnalysisDetails recipient={recipient} />, { initialStore })

// Should show "Checks unavailable" when there are errors
expect(getByText('Checks unavailable')).toBeTruthy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ import { RecipientAnalysisBuilder, ContractAnalysisBuilder } from '@safe-global/
import { FullAnalysisBuilder } from '@safe-global/utils/features/safe-shield/builders'
import { getPrimaryAnalysisResult } from '@safe-global/utils/features/safe-shield/utils/getPrimaryAnalysisResult'
import { faker } from '@faker-js/faker'
import type { Address } from '@/src/types/address'

describe('AnalysisDetailsContent', () => {
const initialStore = {
activeSafe: {
address: '0x1234567890123456789012345678901234567890' as Address,
chainId: '1',
},
}

it('should render nothing when all data is empty', () => {
const { UNSAFE_root } = render(<AnalysisDetailsContent />)
const { UNSAFE_root } = render(<AnalysisDetailsContent />, { initialStore })
// Should render TransactionSimulation wrapper
expect(UNSAFE_root).toBeTruthy()
})
Expand All @@ -16,7 +24,7 @@ describe('AnalysisDetailsContent', () => {
const address = faker.finance.ethereumAddress()
const recipient = RecipientAnalysisBuilder.knownRecipient(address).build()

const { getByText } = render(<AnalysisDetailsContent recipient={recipient} />)
const { getByText } = render(<AnalysisDetailsContent recipient={recipient} />, { initialStore })

// Should render the analysis description for the known recipient
const primaryResult = getPrimaryAnalysisResult(recipient[0])
Expand All @@ -29,7 +37,7 @@ describe('AnalysisDetailsContent', () => {
const address = faker.finance.ethereumAddress()
const contract = ContractAnalysisBuilder.verifiedContract(address).build()

const { getByText } = render(<AnalysisDetailsContent contract={contract} />)
const { getByText } = render(<AnalysisDetailsContent contract={contract} />, { initialStore })

// Should render contract analysis group
const primaryResult = getPrimaryAnalysisResult(contract[0])
Expand All @@ -41,7 +49,7 @@ describe('AnalysisDetailsContent', () => {
it('should render threat analysis when threat data is provided', () => {
const threat = FullAnalysisBuilder.maliciousThreat().build().threat

const { getByText } = render(<AnalysisDetailsContent threat={threat} />)
const { getByText } = render(<AnalysisDetailsContent threat={threat} />, { initialStore })

// Should render threat analysis - check for actual text rendered
expect(getByText(/Malicious threat detected/i)).toBeTruthy()
Expand All @@ -54,7 +62,9 @@ describe('AnalysisDetailsContent', () => {
const contract = ContractAnalysisBuilder.unverifiedContract(contractAddress).build()
const threat = FullAnalysisBuilder.moderateThreat().build().threat

const { getByText } = render(<AnalysisDetailsContent recipient={recipient} contract={contract} threat={threat} />)
const { getByText } = render(<AnalysisDetailsContent recipient={recipient} contract={contract} threat={threat} />, {
initialStore,
})

// Should render all three analysis groups - check for actual labels rendered
expect(getByText(/Known recipient/i)).toBeTruthy()
Expand All @@ -64,23 +74,23 @@ describe('AnalysisDetailsContent', () => {

it('should not render empty recipient data', () => {
const recipient: [undefined, undefined, false] = [undefined, undefined, false]
const { UNSAFE_root } = render(<AnalysisDetailsContent recipient={recipient} />)
const { UNSAFE_root } = render(<AnalysisDetailsContent recipient={recipient} />, { initialStore })

// Should not crash and should render TransactionSimulation
expect(UNSAFE_root).toBeTruthy()
})

it('should not render empty contract data', () => {
const contract: [undefined, undefined, false] = [undefined, undefined, false]
const { UNSAFE_root } = render(<AnalysisDetailsContent contract={contract} />)
const { UNSAFE_root } = render(<AnalysisDetailsContent contract={contract} />, { initialStore })

// Should not crash and should render TransactionSimulation
expect(UNSAFE_root).toBeTruthy()
})

it('should not render empty threat data', () => {
const threat: [undefined, undefined, false] = [undefined, undefined, false]
const { UNSAFE_root } = render(<AnalysisDetailsContent threat={threat} />)
const { UNSAFE_root } = render(<AnalysisDetailsContent threat={threat} />, { initialStore })

// Should not crash and should render TransactionSimulation
expect(UNSAFE_root).toBeTruthy()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { render } from '@/src/tests/test-utils'
import { fireEvent } from '@testing-library/react-native'
import { AnalysisDisplay } from './AnalysisDisplay'
import {
RecipientAnalysisResultBuilder,
Expand All @@ -8,19 +7,27 @@ import {
import { ThreatAnalysisResultBuilder } from '@safe-global/utils/features/safe-shield/builders/threat-analysis-result.builder'
import { Severity } from '@safe-global/utils/features/safe-shield/types'
import { faker } from '@faker-js/faker'
import type { Address } from '@/src/types/address'

describe('AnalysisDisplay', () => {
const initialStore = {
activeSafe: {
address: '0x1234567890123456789012345678901234567890' as Address,
chainId: '1',
},
}

it('should render description from result', () => {
const result = RecipientAnalysisResultBuilder.knownRecipient().build()
const { getByText } = render(<AnalysisDisplay result={result} />)
const { getByText } = render(<AnalysisDisplay result={result} />, { initialStore })

expect(getByText(result.description)).toBeTruthy()
})

it('should render custom description when provided', () => {
const result = RecipientAnalysisResultBuilder.knownRecipient().build()
const customDescription = 'Custom description'
const { getByText } = render(<AnalysisDisplay result={result} description={customDescription} />)
const { getByText } = render(<AnalysisDisplay result={result} description={customDescription} />, { initialStore })

expect(getByText(customDescription)).toBeTruthy()
expect(() => getByText(result.description)).toThrow()
Expand All @@ -33,7 +40,7 @@ describe('AnalysisDisplay', () => {
})
.build()

const { getByText } = render(<AnalysisDisplay result={result} />)
const { getByText } = render(<AnalysisDisplay result={result} />, { initialStore })

expect(getByText('Critical issue')).toBeTruthy()
})
Expand All @@ -44,7 +51,7 @@ describe('AnalysisDisplay', () => {

const result = ThreatAnalysisResultBuilder.masterCopyChange().changes(beforeAddress, afterAddress).build()

const { getByText } = render(<AnalysisDisplay result={result} />)
const { getByText } = render(<AnalysisDisplay result={result} />, { initialStore })

expect(getByText('CURRENT MASTERCOPY:')).toBeTruthy()
expect(getByText('NEW MASTERCOPY:')).toBeTruthy()
Expand All @@ -59,12 +66,7 @@ describe('AnalysisDisplay', () => {
}

const { getByText } = render(<AnalysisDisplay result={result} />, {
initialStore: {
activeSafe: {
address: '0x1234567890123456789012345678901234567890',
chainId: '1',
},
},
initialStore,
})

// Find and press the "Show all" button
Expand All @@ -87,14 +89,13 @@ describe('AnalysisDisplay', () => {
it('should apply border color based on severity', () => {
const result = ThreatAnalysisResultBuilder.malicious().build()

const { UNSAFE_root } = render(<AnalysisDisplay result={result} severity={Severity.CRITICAL} />)
const { UNSAFE_root } = render(<AnalysisDisplay result={result} severity={Severity.CRITICAL} />, { initialStore })

// Component should render with severity
expect(UNSAFE_root).toBeTruthy()
})

it('should render all components together', () => {
const addresses = [{ address: faker.finance.ethereumAddress() }]
const beforeAddress = faker.finance.ethereumAddress()
const afterAddress = faker.finance.ethereumAddress()

Expand All @@ -105,31 +106,12 @@ describe('AnalysisDisplay', () => {
})
.build()

const resultWithAddresses = {
...result,
addresses,
}

const { getByText } = render(<AnalysisDisplay result={resultWithAddresses} severity={Severity.CRITICAL} />, {
initialStore: {
activeSafe: {
address: '0x1234567890123456789012345678901234567890',
chainId: '1',
},
},
const { getByText } = render(<AnalysisDisplay result={result} severity={Severity.CRITICAL} />, {
initialStore,
})

expect(getByText(result.description)).toBeTruthy()
expect(getByText('Critical issue')).toBeTruthy()
expect(getByText('CURRENT MASTERCOPY:')).toBeTruthy()

const showAllText = getByText('Show all')
const touchableOpacity = showAllText.parent?.parent
if (touchableOpacity) {
fireEvent.press(touchableOpacity)
} else {
fireEvent.press(showAllText)
}
expect(getByText(addresses[0].address)).toBeTruthy()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ export function AnalysisDisplay({ result, description, severity }: AnalysisDispl
<Stack gap="$3">
{renderDescription()}

<AnalysisIssuesDisplay result={result} />

{isAddressChange(result) && <AddressChanges result={result} />}

{result.addresses?.length && <ShowAllAddress addresses={result.addresses.map((a) => a.address)} />}
{'issues' in result ? (
<AnalysisIssuesDisplay result={result} />
) : result.addresses?.length ? (
<ShowAllAddress addresses={result.addresses.map((a) => a.address)} />
) : null}
</Stack>
</View>
</View>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Text, Stack, View } from 'tamagui'
import { Text, View } from 'tamagui'
import { SafeFontIcon } from '@/src/components/SafeFontIcon'
import { TouchableOpacity } from 'react-native'
import { getExplorerLink } from '@safe-global/utils/utils/gateway'
Expand All @@ -25,13 +25,7 @@ export function AddressListItem({
const { displayName } = useDisplayName({ value: address })

return (
<Stack
padding="$2"
paddingRight={explorerLink ? '$3' : '$2'}
gap="$1"
backgroundColor="$background"
borderRadius="$1"
>
<>
{displayName && (
<Text fontSize="$3" color="$color" marginBottom="$1">
{displayName}
Expand All @@ -58,6 +52,6 @@ export function AddressListItem({
</TouchableOpacity>
)}
</View>
</Stack>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export const Basic: Story = {
export const WithIssues: Story = {
args: {
result: ThreatAnalysisResultBuilder.malicious()
.description('This transaction contains potentially malicious activity.')
.description('The transaction contains a known malicious address.')
.issues({
[Severity.CRITICAL]: [
{ description: 'Suspicious token transfer detected' },
{ description: 'Unusual contract interaction pattern' },
{ description: 'Potential phishing attempt' },
{ description: 'This address has recorded malicious activity', address: faker.finance.ethereumAddress() },
{ description: 'Unusual contract interaction pattern', address: faker.finance.ethereumAddress() },
{ description: 'Potential phishing attempt', address: faker.finance.ethereumAddress() },
],
[Severity.WARN]: [{ description: 'High gas usage detected' }],
})
Expand Down
Loading
Loading