From 4cb7ea27acd309ad240490b13bcce2148a6b2bb0 Mon Sep 17 00:00:00 2001 From: Leonardo Zizzamia Date: Thu, 15 Aug 2024 00:17:32 -0700 Subject: [PATCH] feat: keep polishing onStatus lifecycle (#1055) --- .../components/TransactionProvider.test.tsx | 127 ++++++++---------- .../components/TransactionProvider.tsx | 79 +++++++---- .../hooks/useTransactionReceipts.ts | 0 .../hooks/useWriteContract.test.ts | 48 ++++--- src/transaction/hooks/useWriteContract.ts | 12 +- src/transaction/types.ts | 15 ++- vitest.config.ts | 8 +- 7 files changed, 163 insertions(+), 126 deletions(-) delete mode 100644 src/transaction/hooks/useTransactionReceipts.ts diff --git a/src/transaction/components/TransactionProvider.test.tsx b/src/transaction/components/TransactionProvider.test.tsx index 037c083ec2..7832b57180 100644 --- a/src/transaction/components/TransactionProvider.test.tsx +++ b/src/transaction/components/TransactionProvider.test.tsx @@ -37,12 +37,20 @@ vi.mock('../hooks/useWriteContracts', () => ({ const TestComponent = () => { const context = useTransactionContext(); - const handleSetLifeCycleStatus = async () => { + const handleStatusError = async () => { context.setLifeCycleStatus({ statusName: 'error', statusData: { code: 'code', error: 'error_long_messages' }, }); }; + const handleStatusTransactionLegacyExecuted = async () => { + context.setLifeCycleStatus({ + statusName: 'transactionLegacyExecuted', + statusData: { + transactionHashList: ['hash12345678'], + }, + }); + }; return (
+
); }; @@ -99,7 +110,7 @@ describe('TransactionProvider', () => { expect(onErrorMock).toHaveBeenCalled(); }); - it('should emit onStatus when setLifeCycleStatus is called', async () => { + it('should emit onStatus when setLifeCycleStatus is called with transactionLegacyExecuted', async () => { const onStatusMock = vi.fn(); render( { , ); - const button = screen.getByText('setLifeCycleStatus.error'); + const button = screen.getByText( + 'setLifeCycleStatus.transactionLegacyExecuted', + ); fireEvent.click(button); - expect(onStatusMock).toHaveBeenCalled(); + expect(onStatusMock).toHaveBeenCalledWith({ + statusName: 'transactionLegacyExecuted', + statusData: { + transactionHashList: ['hash12345678'], + }, + }); }); - it('should update context on handleSubmit', async () => { - const writeContractsAsyncMock = vi.fn(); - (useWriteContracts as ReturnType).mockReturnValue({ - statusWriteContracts: 'IDLE', - writeContractsAsync: writeContractsAsyncMock, - }); + it('should emit onStatus when setLifeCycleStatus is called', async () => { + const onStatusMock = vi.fn(); render( - + , ); - const button = screen.getByText('Submit'); + const button = screen.getByText('setLifeCycleStatus.error'); fireEvent.click(button); - await waitFor(() => { - expect(writeContractsAsyncMock).toHaveBeenCalled(); - }); + expect(onStatusMock).toHaveBeenCalled(); }); - it('should call onsuccess when receipt exists', async () => { + it('should emit onSuccess when one receipt exist', async () => { const onSuccessMock = vi.fn(); (useWaitForTransactionReceipt as ReturnType).mockReturnValue({ - data: '123', + data: { status: 'success' }, }); (useCallsStatus as ReturnType).mockReturnValue({ transactionHash: 'hash', @@ -144,7 +160,7 @@ describe('TransactionProvider', () => { render( @@ -154,6 +170,27 @@ describe('TransactionProvider', () => { fireEvent.click(button); await waitFor(() => { expect(onSuccessMock).toHaveBeenCalled(); + expect(onSuccessMock).toHaveBeenCalledWith({ + transactionReceipts: [{ status: 'success' }], + }); + }); + }); + + it('should update context on handleSubmit', async () => { + const writeContractsAsyncMock = vi.fn(); + (useWriteContracts as ReturnType).mockReturnValue({ + statusWriteContracts: 'IDLE', + writeContractsAsync: writeContractsAsyncMock, + }); + render( + + + , + ); + const button = screen.getByText('Submit'); + fireEvent.click(button); + await waitFor(() => { + expect(writeContractsAsyncMock).toHaveBeenCalled(); }); }); @@ -252,30 +289,6 @@ describe('TransactionProvider', () => { }); }); - it('should call onSuccess when receipts are available', async () => { - const onSuccessMock = vi.fn(); - (useWaitForTransactionReceipt as ReturnType).mockReturnValue({ - data: { status: 'success' }, - }); - (useCallsStatus as ReturnType).mockReturnValue({ - transactionHash: 'hash', - }); - render( - - - , - ); - await waitFor(() => { - expect(onSuccessMock).toHaveBeenCalledWith({ - transactionReceipts: [{ status: 'success' }], - }); - }); - }); - it('should handle chain switching', async () => { const switchChainAsyncMock = vi.fn(); (useSwitchChain as ReturnType).mockReturnValue({ @@ -382,34 +395,6 @@ describe('TransactionProvider', () => { ); }); }); - - it('should call onSuccess when receiptArray has receipts', async () => { - const onSuccessMock = vi.fn(); - const mockReceipt = { status: 'success' }; - - (useWaitForTransactionReceipt as ReturnType).mockReturnValue({ - data: mockReceipt, - }); - - render( - - - , - ); - - const button = screen.getByText('Submit'); - fireEvent.click(button); - - await waitFor(() => { - expect(onSuccessMock).toHaveBeenCalledWith({ - transactionReceipts: [mockReceipt], - }); - }); - }); }); describe('useTransactionContext', () => { diff --git a/src/transaction/components/TransactionProvider.tsx b/src/transaction/components/TransactionProvider.tsx index 99c2326abd..31ce6ae0a0 100644 --- a/src/transaction/components/TransactionProvider.tsx +++ b/src/transaction/components/TransactionProvider.tsx @@ -5,7 +5,7 @@ import { useEffect, useState, } from 'react'; -import type { Address, TransactionReceipt } from 'viem'; +import type { Address } from 'viem'; import { useAccount, useConfig, @@ -62,11 +62,9 @@ export function TransactionProvider({ statusName: 'init', statusData: null, }); // Component lifecycle - const [receiptArray, setReceiptArray] = useState([]); const [transactionId, setTransactionId] = useState(''); - const [transactionHashArray, setTransactionHashArray] = useState( - [], - ); + const [transactionHashList, setTransactionHashList] = useState([]); + const { switchChainAsync } = useSwitchChain(); // Hooks that depend from Core Hooks @@ -81,8 +79,7 @@ export function TransactionProvider({ data: writeContractTransactionHash, } = useWriteContract({ setLifeCycleStatus, - setTransactionHashArray, - transactionHashArray, + transactionHashList, }); const { transactionHash, status: callStatus } = useCallsStatus({ setLifeCycleStatus, @@ -94,25 +91,61 @@ export function TransactionProvider({ // Component lifecycle emitters useEffect(() => { - // Emit Error + setErrorMessage(''); + // Error if (lifeCycleStatus.statusName === 'error') { setErrorMessage(lifeCycleStatus.statusData.message); setErrorCode(lifeCycleStatus.statusData.code); onError?.(lifeCycleStatus.statusData); } + // Transaction Legacy Executed + if (lifeCycleStatus.statusName === 'transactionLegacyExecuted') { + setTransactionHashList(lifeCycleStatus.statusData.transactionHashList); + } + // Success + if (lifeCycleStatus.statusName === 'success') { + onSuccess?.({ + transactionReceipts: lifeCycleStatus.statusData.transactionReceipts, + }); + } // Emit State onStatus?.(lifeCycleStatus); }, [ onError, onStatus, + onSuccess, lifeCycleStatus, lifeCycleStatus.statusData, // Keep statusData, so that the effect runs when it changes lifeCycleStatus.statusName, // Keep statusName, so that the effect runs when it changes ]); - const getTransactionReceipts = useCallback(async () => { + // Trigger success status when receipt is generated by useWaitForTransactionReceipt + useEffect(() => { + if (!receipt) { + return; + } + setLifeCycleStatus({ + statusName: 'success', + statusData: { + transactionReceipts: [receipt], + }, + }); + }, [receipt]); + + // When all transactions are succesful, get the receipts + useEffect(() => { + if ( + transactionHashList.length !== contracts.length || + contracts.length < 2 + ) { + return; + } + getTransactionLegacyReceipts(); + }, [contracts, transactionHashList]); + + const getTransactionLegacyReceipts = useCallback(async () => { const receipts = []; - for (const hash of transactionHashArray) { + for (const hash of transactionHashList) { try { const txnReceipt = await waitForTransactionReceipt(config, { hash, @@ -130,17 +163,13 @@ export function TransactionProvider({ }); } } - setReceiptArray(receipts); - }, [chainId, config, transactionHashArray]); - - useEffect(() => { - if ( - transactionHashArray.length === contracts.length && - contracts?.length > 1 - ) { - getTransactionReceipts(); - } - }, [contracts, getTransactionReceipts, transactionHashArray]); + setLifeCycleStatus({ + statusName: 'success', + statusData: { + transactionReceipts: receipts, + }, + }); + }, [chainId, config, transactionHashList]); const fallbackToWriteContract = useCallback(async () => { // EOAs don't support batching, so we process contracts individually. @@ -209,14 +238,6 @@ export function TransactionProvider({ } }, [chainId, executeContracts, fallbackToWriteContract, switchChain]); - useEffect(() => { - if (receiptArray?.length) { - onSuccess?.({ transactionReceipts: receiptArray }); - } else if (receipt) { - onSuccess?.({ transactionReceipts: [receipt] }); - } - }, [onSuccess, receipt, receiptArray]); - const value = useValue({ address, chainId, diff --git a/src/transaction/hooks/useTransactionReceipts.ts b/src/transaction/hooks/useTransactionReceipts.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/transaction/hooks/useWriteContract.test.ts b/src/transaction/hooks/useWriteContract.test.ts index 804e0f36c1..965fd8f5e0 100644 --- a/src/transaction/hooks/useWriteContract.test.ts +++ b/src/transaction/hooks/useWriteContract.test.ts @@ -27,7 +27,6 @@ type MockUseWriteContractReturn = { describe('useWriteContract', () => { const mockSetLifeCycleStatus = vi.fn(); - const mockSetTransactionHashArray = vi.fn(); beforeEach(() => { vi.resetAllMocks(); @@ -44,7 +43,7 @@ describe('useWriteContract', () => { const { result } = renderHook(() => useWriteContract({ setLifeCycleStatus: mockSetLifeCycleStatus, - setTransactionHashArray: mockSetTransactionHashArray, + transactionHashList: [], }), ); expect(result.current.status).toBe('idle'); @@ -68,7 +67,7 @@ describe('useWriteContract', () => { renderHook(() => useWriteContract({ setLifeCycleStatus: mockSetLifeCycleStatus, - setTransactionHashArray: mockSetTransactionHashArray, + transactionHashList: [], }), ); expect(onErrorCallback).toBeDefined(); @@ -100,7 +99,7 @@ describe('useWriteContract', () => { renderHook(() => useWriteContract({ setLifeCycleStatus: mockSetLifeCycleStatus, - setTransactionHashArray: mockSetTransactionHashArray, + transactionHashList: [], }), ); expect(onErrorCallback).toBeDefined(); @@ -116,7 +115,7 @@ describe('useWriteContract', () => { }); it('should handle successful transaction', () => { - const transactionId = '0x123'; + const transactionId = '0x123456'; let onSuccessCallback: ((id: string) => void) | undefined; (useWriteContractWagmi as ReturnType).mockImplementation( ({ mutation }: UseWriteContractConfig) => { @@ -131,16 +130,21 @@ describe('useWriteContract', () => { renderHook(() => useWriteContract({ setLifeCycleStatus: mockSetLifeCycleStatus, - setTransactionHashArray: mockSetTransactionHashArray, + transactionHashList: [], }), ); expect(onSuccessCallback).toBeDefined(); onSuccessCallback?.(transactionId); - expect(mockSetTransactionHashArray).toHaveBeenCalledWith([transactionId]); + expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ + statusName: 'transactionLegacyExecuted', + statusData: { + transactionHashList: [transactionId], + }, + }); }); it('should handle multiple successful transactions', () => { - const transactionId = '0x123'; + const transactionId = '0x12345678'; let onSuccessCallback: ((id: string) => void) | undefined; (useWriteContractWagmi as ReturnType).mockImplementation( ({ mutation }: UseWriteContractConfig) => { @@ -155,16 +159,30 @@ describe('useWriteContract', () => { renderHook(() => useWriteContract({ setLifeCycleStatus: mockSetLifeCycleStatus, - setTransactionHashArray: mockSetTransactionHashArray, - transactionHashArray: ['0x1234'], + transactionHashList: [], }), ); expect(onSuccessCallback).toBeDefined(); onSuccessCallback?.(transactionId); - expect(mockSetTransactionHashArray).toHaveBeenCalledWith([ - '0x1234', - transactionId, - ]); + expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ + statusName: 'transactionLegacyExecuted', + statusData: { + transactionHashList: [transactionId], + }, + }); + renderHook(() => + useWriteContract({ + setLifeCycleStatus: mockSetLifeCycleStatus, + transactionHashList: [transactionId], + }), + ); + onSuccessCallback?.(transactionId); + expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ + statusName: 'transactionLegacyExecuted', + statusData: { + transactionHashList: [transactionId, transactionId], + }, + }); }); it('should handle uncaught errors', () => { @@ -177,7 +195,7 @@ describe('useWriteContract', () => { const { result } = renderHook(() => useWriteContract({ setLifeCycleStatus: mockSetLifeCycleStatus, - setTransactionHashArray: mockSetTransactionHashArray, + transactionHashList: [], }), ); expect(result.current.status).toBe('error'); diff --git a/src/transaction/hooks/useWriteContract.ts b/src/transaction/hooks/useWriteContract.ts index 385aee697e..6a001f4613 100644 --- a/src/transaction/hooks/useWriteContract.ts +++ b/src/transaction/hooks/useWriteContract.ts @@ -11,8 +11,7 @@ import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError' */ export function useWriteContract({ setLifeCycleStatus, - setTransactionHashArray, - transactionHashArray, + transactionHashList, }: UseWriteContractParams) { try { const { status, writeContractAsync, data } = useWriteContractWagmi({ @@ -31,9 +30,12 @@ export function useWriteContract({ }); }, onSuccess: (hash: Address) => { - setTransactionHashArray( - transactionHashArray ? transactionHashArray?.concat(hash) : [hash], - ); + setLifeCycleStatus({ + statusName: 'transactionLegacyExecuted', + statusData: { + transactionHashList: [...transactionHashList, hash], + }, + }); }, }, }); diff --git a/src/transaction/types.ts b/src/transaction/types.ts index b7c3ec95ce..4d829f3a2a 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -17,6 +17,18 @@ export type LifeCycleStatus = | { statusName: 'error'; statusData: TransactionError; + } + | { + statusName: 'success'; + statusData: { + transactionReceipts: TransactionReceipt[]; + }; + } + | { + statusName: 'transactionLegacyExecuted'; + statusData: { + transactionHashList: Address[]; + }; }; export type IsSpinnerDisplayedProps = { @@ -174,8 +186,7 @@ export type UseCallsStatusParams = { export type UseWriteContractParams = { setLifeCycleStatus: (state: LifeCycleStatus) => void; - setTransactionHashArray: (ids: Address[]) => void; - transactionHashArray?: Address[]; + transactionHashList: Address[]; }; export type UseWriteContractsParams = { diff --git a/vitest.config.ts b/vitest.config.ts index d44a093dae..d83a345ace 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,10 +22,10 @@ export default defineConfig({ ], reportOnFailure: true, thresholds: { - statements: 99.59, - branches: 99.18, - functions: 97.84, - lines: 99.59, + statements: 99.57, + branches: 99.23, + functions: 97.85, + lines: 99.57, }, }, environment: 'jsdom',