diff --git a/packages/ui-components/src/__tests__/OrderDetail.test.ts b/packages/ui-components/src/__tests__/OrderDetail.test.ts index d23475bd8e..3f8e49abc3 100644 --- a/packages/ui-components/src/__tests__/OrderDetail.test.ts +++ b/packages/ui-components/src/__tests__/OrderDetail.test.ts @@ -16,6 +16,7 @@ import type { ComponentProps } from 'svelte'; import { invalidateTanstackQueries } from '$lib/queries/queryClient'; import { useToasts } from '$lib/providers/toasts/useToasts'; import { useRaindexClient } from '$lib/hooks/useRaindexClient'; +import { getExplorerLink } from '$lib/services/getExplorerLink'; vi.mock('$lib/hooks/useRaindexClient', () => ({ useRaindexClient: vi.fn() @@ -46,6 +47,10 @@ vi.mock('$lib/components/charts/OrderTradesChart.svelte', async () => { const mockLightweightCharts = (await import('../lib/__mocks__/MockComponent.svelte')).default; return { default: mockLightweightCharts }; }); + +vi.mock('$lib/services/getExplorerLink', () => ({ + getExplorerLink: vi.fn() +})); const orderbookAddress = '0x123456789012345678901234567890123456abcd'; const orderHash = '0x0234'; @@ -157,6 +162,10 @@ describe('OrderDetail', () => { errToast: mockErrToast, removeToast: vi.fn() }); + + (getExplorerLink as Mock).mockReturnValue( + 'https://etherscan.io/address/0x1234567890123456789012345678901234567890' + ); }); it('calls the order detail query with the correct order hash', async () => { @@ -335,4 +344,40 @@ describe('OrderDetail', () => { expect(mockOnWithdraw).toHaveBeenCalledWith(mockRaindexClient, mockOrder.vaultsList.items[1]); }); + + it('renders owner address as explorer link when explorer is available', async () => { + const explorerUrl = 'https://etherscan.io/address/0x1234567890123456789012345678901234567890'; + (getExplorerLink as Mock).mockReturnValue(explorerUrl); + + render(OrderDetail, { + props: defaultProps, + context: new Map([['$$_queryClient', queryClient]]) + }); + + await waitFor(() => { + const ownerLink = screen.getByRole('link', { + name: /0x1234567890123456789012345678901234567890/i + }); + expect(ownerLink).toBeInTheDocument(); + expect(ownerLink).toHaveAttribute('href', explorerUrl); + expect(ownerLink).toHaveAttribute('target', '_blank'); + expect(ownerLink).toHaveAttribute('rel', 'noopener noreferrer'); + }); + }); + + it('falls back to Hash component when no explorer link is available', async () => { + (getExplorerLink as Mock).mockReturnValue(''); + + render(OrderDetail, { + props: defaultProps, + context: new Map([['$$_queryClient', queryClient]]) + }); + + await waitFor(() => { + expect(screen.getByText('0x1234567890123456789012345678901234567890')).toBeInTheDocument(); + expect( + screen.queryByRole('link', { name: /0x1234567890123456789012345678901234567890/i }) + ).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/ui-components/src/__tests__/VaultDetail.test.ts b/packages/ui-components/src/__tests__/VaultDetail.test.ts index 5f1b279df4..6d76742743 100644 --- a/packages/ui-components/src/__tests__/VaultDetail.test.ts +++ b/packages/ui-components/src/__tests__/VaultDetail.test.ts @@ -12,6 +12,7 @@ import { useAccount } from '../lib/providers/wallet/useAccount'; import { QKEY_VAULT } from '$lib/queries/keys'; import { useToasts } from '../lib/providers/toasts/useToasts'; import { invalidateTanstackQueries } from '$lib/queries/queryClient'; +import { getExplorerLink } from '$lib/services/getExplorerLink'; type VaultDetailProps = ComponentProps; @@ -44,6 +45,10 @@ vi.mock('$lib/providers/toasts/useToasts', () => ({ useToasts: vi.fn() })); +vi.mock('$lib/services/getExplorerLink', () => ({ + getExplorerLink: vi.fn() +})); + const mockErrToast = vi.fn(); const defaultProps: VaultDetailProps = { @@ -78,6 +83,10 @@ describe('VaultDetail', () => { removeToast: vi.fn() }); + (getExplorerLink as Mock).mockReturnValue( + 'https://etherscan.io/address/0x1234567890123456789012345678901234567890' + ); + mockRaindexClient = { getVault: vi.fn() } as unknown as RaindexClient; @@ -214,4 +223,40 @@ describe('VaultDetail', () => { expect(mockErrToast).toHaveBeenCalledWith('Failed to refresh'); }); }); + + it('renders owner address as explorer link when explorer is available', async () => { + const explorerUrl = 'https://etherscan.io/address/0x1234567890123456789012345678901234567890'; + (getExplorerLink as Mock).mockReturnValue(explorerUrl); + + render(VaultDetail, { + props: defaultProps, + context: new Map([['$$_queryClient', queryClient]]) + }); + + await waitFor(() => { + const ownerLink = screen.getByRole('link', { + name: /0x1234567890123456789012345678901234567890/i + }); + expect(ownerLink).toBeInTheDocument(); + expect(ownerLink).toHaveAttribute('href', explorerUrl); + expect(ownerLink).toHaveAttribute('target', '_blank'); + expect(ownerLink).toHaveAttribute('rel', 'noopener noreferrer'); + }); + }); + + it('falls back to Hash component when no explorer link is available', async () => { + (getExplorerLink as Mock).mockReturnValue(''); + + render(VaultDetail, { + props: defaultProps, + context: new Map([['$$_queryClient', queryClient]]) + }); + + await waitFor(() => { + expect(screen.getByTestId('vaultDetailOwnerAddress')).toHaveTextContent('0x123'); + expect( + screen.queryByRole('link', { name: /0x1234567890123456789012345678901234567890/i }) + ).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/ui-components/src/__tests__/getExplorerLink.test.ts b/packages/ui-components/src/__tests__/getExplorerLink.test.ts index 9a363f08d7..b63520eb08 100644 --- a/packages/ui-components/src/__tests__/getExplorerLink.test.ts +++ b/packages/ui-components/src/__tests__/getExplorerLink.test.ts @@ -12,10 +12,10 @@ vi.mock('viem/chains', () => ({ })); describe('getExplorerLink', () => { - it('should return the explorer link', async () => { - expect(await getExplorerLink('0x123', 999, 'tx')).toBe('https://etherscan.io/tx/0x123'); + it('should return the explorer link', () => { + expect(getExplorerLink('0x123', 999, 'tx')).toBe('https://etherscan.io/tx/0x123'); }); - it('should return an empty string if the chain is not found', async () => { - expect(await getExplorerLink('0x123', 1, 'tx')).toBe(''); + it('should return an empty string if the chain is not found', () => { + expect(getExplorerLink('0x123', 1, 'tx')).toBe(''); }); }); diff --git a/packages/ui-components/src/lib/components/detail/OrderDetail.svelte b/packages/ui-components/src/lib/components/detail/OrderDetail.svelte index 1a2673564e..b8a8e2d093 100644 --- a/packages/ui-components/src/lib/components/detail/OrderDetail.svelte +++ b/packages/ui-components/src/lib/components/detail/OrderDetail.svelte @@ -21,8 +21,10 @@ import { ArrowDownToBracketOutline, ArrowUpFromBracketOutline, - InfoCircleOutline + InfoCircleOutline, + WalletOutline } from 'flowbite-svelte-icons'; + import { getExplorerLink } from '$lib/services/getExplorerLink'; import { useAccount } from '$lib/providers/wallet/useAccount'; import { RaindexClient, @@ -155,7 +157,20 @@ Owner - + {@const explorerLink = getExplorerLink(data.owner, chainId, 'address')} + {#if explorerLink} + + + {data.owner} + + {:else} + + {/if} diff --git a/packages/ui-components/src/lib/components/detail/VaultDetail.svelte b/packages/ui-components/src/lib/components/detail/VaultDetail.svelte index d92c424a71..e95296072d 100644 --- a/packages/ui-components/src/lib/components/detail/VaultDetail.svelte +++ b/packages/ui-components/src/lib/components/detail/VaultDetail.svelte @@ -23,7 +23,12 @@ import { invalidateTanstackQueries } from '$lib/queries/queryClient'; import { useAccount } from '$lib/providers/wallet/useAccount'; import { Button } from 'flowbite-svelte'; - import { ArrowDownToBracketOutline, ArrowUpFromBracketOutline } from 'flowbite-svelte-icons'; + import { + ArrowDownToBracketOutline, + ArrowUpFromBracketOutline, + WalletOutline + } from 'flowbite-svelte-icons'; + import { getExplorerLink } from '$lib/services/getExplorerLink'; import { useToasts } from '$lib/providers/toasts/useToasts'; import { useRaindexClient } from '$lib/hooks/useRaindexClient'; @@ -128,7 +133,20 @@ Owner address - + {@const explorerLink = getExplorerLink(data.owner, chainId, 'address')} + {#if explorerLink} + + + {data.owner} + + {:else} + + {/if} diff --git a/packages/ui-components/src/lib/services/getExplorerLink.ts b/packages/ui-components/src/lib/services/getExplorerLink.ts index 8e815bbd06..7de9bfa402 100644 --- a/packages/ui-components/src/lib/services/getExplorerLink.ts +++ b/packages/ui-components/src/lib/services/getExplorerLink.ts @@ -1,10 +1,9 @@ import * as chains from 'viem/chains'; -export const getExplorerLink = async (hash: string, chainId: number, type: 'tx' | 'address') => { +export const getExplorerLink = (hash: string, chainId: number, type: 'tx' | 'address'): string => { const chain = Object.values(chains).find((chain) => chain.id === chainId); if (chain?.blockExplorers) { return chain.blockExplorers.default.url + `/${type}/${hash}`; - } else { - return ''; } + return ''; };