Skip to content

Commit

Permalink
feat: Update useBlockExplorer hook to return information about the bl…
Browse files Browse the repository at this point in the history
…ock explorer (#243)
  • Loading branch information
cgero-eth authored Jul 16, 2024
1 parent 6412f4d commit 207e461
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 75 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Changed

- Update minor and patch NPM dependencies
- Update `useBlockExplorer` hook to return information about the block explorer

## [1.0.38] - 2024-07-16

Expand Down
9 changes: 2 additions & 7 deletions src/modules/components/address/addressInput/addressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,8 @@ export const AddressInput = forwardRef<HTMLTextAreaElement, IAddressInputProps>(

const currentChain = wagmiConfig.chains.find(({ id }) => id === processedChainId);

const { getChainEntityUrl } = useBlockExplorer();

const addressUrl = getChainEntityUrl({
type: ChainEntityType.ADDRESS,
chainId: processedChainId,
id: value,
});
const { buildEntityUrl } = useBlockExplorer({ chainId: processedChainId });
const addressUrl = buildEntityUrl({ type: ChainEntityType.ADDRESS, id: value });

const supportEnsNames = currentChain?.contracts?.ensRegistry != null;

Expand Down
28 changes: 8 additions & 20 deletions src/modules/components/asset/assetTransfer/assetTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ export const AssetTransfer: React.FC<IAssetTransferProps> = (props) => {
wagmiConfig,
} = props;

const { getChainEntityUrl } = useBlockExplorer(wagmiConfig);
const { buildEntityUrl } = useBlockExplorer({ chains: wagmiConfig?.chains, chainId });

const senderUrl = buildEntityUrl({ type: ChainEntityType.ADDRESS, id: sender.address });
const recipientUrl = buildEntityUrl({ type: ChainEntityType.ADDRESS, id: recipient.address });
const transactionUrl = buildEntityUrl({ type: ChainEntityType.TRANSACTION, id: hash });

const formattedTokenValue = formatterUtils.formatNumber(assetAmount, {
format: NumberFormat.TOKEN_AMOUNT_SHORT,
Expand All @@ -82,15 +86,7 @@ export const AssetTransfer: React.FC<IAssetTransferProps> = (props) => {
return (
<div className="flex size-full flex-col gap-y-2 md:gap-y-3">
<div className="relative flex h-full flex-col rounded-xl bg-neutral-0 md:flex-row">
<AssetTransferAddress
txRole="sender"
participant={sender}
addressUrl={getChainEntityUrl({
chainId,
type: ChainEntityType.ADDRESS,
id: sender.address,
})}
/>
<AssetTransferAddress txRole="sender" participant={sender} addressUrl={senderUrl} />
<div className="border-t border-neutral-100 md:border-l" />
<AvatarIcon
icon={IconType.CHEVRON_DOWN}
Expand All @@ -100,18 +96,10 @@ export const AssetTransfer: React.FC<IAssetTransferProps> = (props) => {
'md:left-1/2 md:-translate-x-1/2 md:-rotate-90', //responsive
)}
/>
<AssetTransferAddress
txRole="recipient"
participant={recipient}
addressUrl={getChainEntityUrl({
chainId,
type: ChainEntityType.ADDRESS,
id: recipient.address,
})}
/>
<AssetTransferAddress txRole="recipient" participant={recipient} addressUrl={recipientUrl} />
</div>
<LinkBase
href={getChainEntityUrl({ chainId, type: ChainEntityType.TRANSACTION, id: hash })}
href={transactionUrl}
target="_blank"
rel="noopener noreferrer"
className={assetTransferClassNames}
Expand Down
7 changes: 6 additions & 1 deletion src/modules/hooks/useBlockExplorer/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export { ChainEntityType, useBlockExplorer, type IGetChainEntityUrlParams } from './useBlockExplorer';
export {
ChainEntityType,
useBlockExplorer,
type IBuildEntityUrlParams,
type IUseBlockExplorerParams,
} from './useBlockExplorer';
86 changes: 56 additions & 30 deletions src/modules/hooks/useBlockExplorer/useBlockExplorer.test.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,76 @@
import { renderHook } from '@testing-library/react';
import * as wagmi from 'wagmi';
import { mainnet, polygon } from 'wagmi/chains';
import { type Chain, mainnet, polygon, sepolia } from 'wagmi/chains';
import { ChainEntityType, useBlockExplorer } from './useBlockExplorer';

describe('useBlockExplorer hook', () => {
const useChainsSpy = jest.spyOn(wagmi, 'useChains');

beforeEach(() => {
useChainsSpy.mockReturnValue([mainnet]);
});

afterEach(() => {
useChainsSpy.mockReset();
});

it('generates correct URL for different entity types and chain IDs', () => {
useChainsSpy.mockReturnValue([mainnet, polygon]);
describe('blockExplorer', () => {
it('returns the block explorer definitions for the given chain id from the specified list of chains', () => {
const chains: [Chain, ...Chain[]] = [mainnet, polygon, sepolia];
const chainId = polygon.id;
const { result } = renderHook(() => useBlockExplorer({ chains, chainId }));
expect(result.current.blockExplorer).toEqual(polygon.blockExplorers.default);
});

const { result } = renderHook(() => useBlockExplorer());
expect(result.current.getChainEntityUrl({ type: ChainEntityType.ADDRESS, chainId: 1, id: '0x123' })).toEqual(
'https://etherscan.io/address/0x123',
);
expect(
result.current.getChainEntityUrl({ type: ChainEntityType.TRANSACTION, id: '0xabc', chainId: 137 }),
).toEqual('https://polygonscan.com/tx/0xabc');
});
it('returns the block explorer definitions for the given chain id from the chains defined on wagmi provider', () => {
useChainsSpy.mockReturnValue([polygon, sepolia]);
const chainId = sepolia.id;
const { result } = renderHook(() => useBlockExplorer({ chainId }));
expect(result.current.blockExplorer).toEqual(sepolia.blockExplorers.default);
});

it('throws an error when block explorer URL is missing', () => {
const { blockExplorers, ...chainWithoutBlockExplorer } = mainnet;
useChainsSpy.mockReturnValue([chainWithoutBlockExplorer]);
it('returns the block explorer of the first chain in the chains prop when chainId is not defined', () => {
const chains: [Chain, ...Chain[]] = [mainnet, sepolia, polygon];
const { result } = renderHook(() => useBlockExplorer({ chains }));
expect(result.current.blockExplorer).toEqual(chains[0].blockExplorers?.default);
});

const { result } = renderHook(() => useBlockExplorer());
expect(() =>
result.current.getChainEntityUrl({ type: ChainEntityType.ADDRESS, chainId: 1, id: '0x123' }),
).toThrow('useBlockExplorer: Block explorer URL not found for chain with id 1');
});
it('returns the block explorer of the first chain in the wagmi config when chainId and chains are not defined', () => {
useChainsSpy.mockReturnValue([sepolia, polygon]);
const { result } = renderHook(() => useBlockExplorer());
expect(result.current.blockExplorer).toEqual(sepolia.blockExplorers?.default);
});

it('uses the first chain set on the wagmi provider when chainId property is not set', () => {
useChainsSpy.mockReturnValue([mainnet, polygon]);
it('returns the block explorer set to undefined when chain definitions cannot be found for the given chain id', () => {
const chains: [Chain, ...Chain[]] = [mainnet, polygon];
const { result } = renderHook(() => useBlockExplorer({ chains, chainId: sepolia.id }));
expect(result.current.blockExplorer).toBeUndefined();
});

const { result } = renderHook(() => useBlockExplorer());
expect(result.current.getChainEntityUrl({ type: ChainEntityType.ADDRESS, id: '0x123' })).toEqual(
'https://etherscan.io/address/0x123',
);
it('returns the block explorer set to undefined when related chain definitions does not include info about the block explorer', () => {
const { blockExplorers, ...mainnetWithoutBlockExplorer } = mainnet;
useChainsSpy.mockReturnValue([mainnetWithoutBlockExplorer]);
const { result } = renderHook(() => useBlockExplorer({ chainId: mainnet.id }));
expect(result.current.blockExplorer).toBeUndefined();
});
});

it('uses the first chain passed as parameters when chainId is not defined', () => {
const { result } = renderHook(() => useBlockExplorer({ chains: [polygon, mainnet] }));
expect(result.current.getChainEntityUrl({ type: ChainEntityType.ADDRESS, id: '0x123' })).toEqual(
'https://polygonscan.com/address/0x123',
);
describe('buildEntityUrl', () => {
it('generates correct URL for different entity types', () => {
useChainsSpy.mockReturnValue([mainnet, polygon]);

const { result } = renderHook(() => useBlockExplorer({ chainId: mainnet.id }));
const addressUrl = result.current.buildEntityUrl({ type: ChainEntityType.ADDRESS, id: '0x123' });
expect(addressUrl).toMatch(/address\/0x123/);

const transactionUrl = result.current.buildEntityUrl({ type: ChainEntityType.TRANSACTION, id: '0xabc' });
expect(transactionUrl).toMatch(/tx\/0xabc/);
});

it('returns undefined when block explorer info is missing', () => {
useChainsSpy.mockReturnValue([sepolia]);
const { result } = renderHook(() => useBlockExplorer({ chainId: mainnet.id }));
expect(result.current.buildEntityUrl({ type: ChainEntityType.ADDRESS, id: '0x123' })).toBeUndefined();
});
});
});
43 changes: 26 additions & 17 deletions src/modules/hooks/useBlockExplorer/useBlockExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,46 @@ export enum ChainEntityType {
TOKEN = 'token',
}

export interface IGetChainEntityUrlParams {
export interface IUseBlockExplorerParams {
/**
* The type of the chain entity (address, tx, token)
* Chains definitions to use for building the block explorer URLs. Defaults to the chains defined on the Wagmi
* context provider.
*/
type: ChainEntityType;
chains?: Config['chains'];
/**
* The ID of the chain (optional), if not provided, the first chain is used
* Chain ID to build the URLs for. Defaults to the id of the first chain on the chains list.
*/
chainId?: number;
}

export interface IBuildEntityUrlParams {
/**
* The type of the entity (e.g. address, transaction, token)
*/
type: ChainEntityType;
/**
* The ID of the entity (e.g. tx hash for a tx)
* The ID of the entity (e.g. transaction hash for a transaction)
*/
id?: string;
}

export const useBlockExplorer = (wagmiConfig?: Pick<Config, 'chains'>) => {
export const useBlockExplorer = (params?: IUseBlockExplorerParams) => {
const { chains, chainId } = params ?? {};

const globalChains = useChains();
const chains = wagmiConfig?.chains ?? globalChains;
const processedChains = chains ?? globalChains;

const chainDefinitions = chainId ? processedChains.find((chain) => chain.id === chainId) : processedChains[0];
const { default: blockExplorer } = chainDefinitions?.blockExplorers ?? {};

const getChainEntityUrl = useCallback(
({ type, chainId, id }: IGetChainEntityUrlParams) => {
const chain = chainId ? chains.find((chain) => chain.id === chainId) : chains[0];
const baseUrl = chain?.blockExplorers?.default?.url;
if (!baseUrl) {
throw new Error(`useBlockExplorer: Block explorer URL not found for chain with id ${chainId}`);
}
const buildEntityUrl = useCallback(
({ type, id }: IBuildEntityUrlParams) => {
const baseUrl = blockExplorer?.url;

return `${baseUrl}/${type}/${id}`;
return baseUrl != null ? `${baseUrl}/${type}/${id}` : undefined;
},
[chains],
[blockExplorer],
);

return { getChainEntityUrl };
return { blockExplorer, buildEntityUrl };
};

0 comments on commit 207e461

Please sign in to comment.