diff --git a/src/components/Balance.test.tsx b/src/components/Balance.test.tsx new file mode 100644 index 0000000000..79917c16cc --- /dev/null +++ b/src/components/Balance.test.tsx @@ -0,0 +1,62 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { publicClient } from '../network/client'; +import { render, screen } from '@testing-library/react'; +import { Balance } from './Balance'; +import { useOnchainActionWithCache } from '../hooks/useOnchainActionWithCache'; + +import '@testing-library/jest-dom'; + +jest.mock('../network/client'); +jest.mock('../hooks/useOnchainActionWithCache'); + +describe('Balance', () => { + const testAddress = '0x1234567890abcdef1234567890abcdef12345678'; + + const mockGetBalance = publicClient.getBalance as jest.Mock; + const mockUseOnchainActionWithCache = useOnchainActionWithCache as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays ETH balance', async () => { + const testBalance = 49094208464446850n; + mockGetBalance.mockReturnValue(testBalance); + mockUseOnchainActionWithCache.mockImplementation(() => { + return { + data: testBalance, + isLoading: false, + }; + }); + + render( + + + , + ); + + expect(await screen.findByTestId('test-id-balance')).toHaveTextContent('0.049 ETH'); + }); + + it('displays ETH balance with rounding', async () => { + const testBalance = 49994208464446850n; + mockGetBalance.mockReturnValue(testBalance); + mockUseOnchainActionWithCache.mockImplementation(() => { + return { + data: testBalance, + isLoading: false, + }; + }); + + render( + + + , + ); + + expect(await screen.findByTestId('test-id-balance')).toHaveTextContent('0.050 ETH'); + }); +}); diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx new file mode 100644 index 0000000000..71f5ba0b1a --- /dev/null +++ b/src/components/Balance.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { formatEther, parseEther } from 'viem'; +import type { Address } from 'viem'; +import { publicClient } from '../network/client'; +import { useOnchainActionWithCache } from '../hooks/useOnchainActionWithCache'; + +type BalanceProps = { + address: Address; + className?: string; + decimalDigits?: number; + props?: React.HTMLAttributes; +}; + +const balanceAction = (address: Address) => async (): Promise => { + try { + return await publicClient + .getBalance({ + address, + }) + .toString(); + } catch (err) { + return '0'; + } +}; + +/** + * Balance is a React component that renders the account balance for the given Ethereum address. + * It displays the user's balance, with the no. of decimals specified by the 'decimalDigits' prop. + * By default, 'decimalDigits' is set to 3. + * + * @param {Address} props.address - The Ethereum address for which to display the balance. + * @param {string} [className] - Optional CSS class for custom styling. + * @param {number} [decimalDigits=3] - Determines the no. of decimal digits to be displayed. + * @param {React.HTMLAttributes} [props] - Additional HTML attributes for the span element. + */ +export function Balance({ address, className, decimalDigits = 3, ...props }: BalanceProps) { + const balanceKey = `balance-${address}`; + const { data: balance, isLoading } = useOnchainActionWithCache( + balanceAction(address), + balanceKey, + ); + + if (isLoading) { + return null; + } + + return ( + + {parseFloat(formatEther(BigInt(balance ?? '0'))).toFixed(decimalDigits)} + {' ETH'} + + ); +}