From bfea1bf66d1378c824d6ec0e4b4db0e588574478 Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 5 Aug 2024 16:59:50 -0700 Subject: [PATCH] feat: updated txn success handler to pass multiple receipts for multiple contracts (EOA) (#945) Co-authored-by: Alissa Crane --- site/docs/pages/transaction/types.mdx | 8 +-- src/swap/components/Swap.test.tsx | 26 ++++++++++ src/swap/components/SwapToggleButton.test.tsx | 31 +++++++++++ .../components/TransactionProvider.test.tsx | 49 ++++++++++++++++++ .../components/TransactionProvider.tsx | 51 ++++++++++++++++--- src/transaction/hooks/useCallsStatus.ts | 1 + .../hooks/useWriteContract.test.ts | 12 ++--- src/transaction/hooks/useWriteContract.ts | 14 +++-- src/transaction/types.ts | 7 ++- 9 files changed, 174 insertions(+), 25 deletions(-) create mode 100644 src/swap/components/Swap.test.tsx create mode 100644 src/swap/components/SwapToggleButton.test.tsx diff --git a/site/docs/pages/transaction/types.mdx b/site/docs/pages/transaction/types.mdx index 4b5f92ac60..378891c0c4 100644 --- a/site/docs/pages/transaction/types.mdx +++ b/site/docs/pages/transaction/types.mdx @@ -54,7 +54,7 @@ type TransactionProviderReact = { children: ReactNode; // The child components to be rendered within the provider component. contracts: ContractFunctionParameters[]; // An array of contract function parameters provided to the child components. onError?: (e: TransactionError) => void; // An optional callback function that handles errors within the provider. - onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes transaction hash + onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes the transaction receipts }; ``` @@ -67,7 +67,7 @@ type TransactionReact = { className?: string; // An optional CSS class name for styling the component. contracts: ContractFunctionParameters[]; // An array of contract function parameters for the transaction. onError?: (e: TransactionError) => void; // An optional callback function that handles transaction errors. - onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes transaction hash + onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes the transaction receipts }; ``` @@ -75,9 +75,9 @@ type TransactionReact = { ```ts type TransactionResponse = { - transactionHash: string; // Proof that a transaction was validated and added to the blockchain - receipt: TransactionReceipt; // The receipt of the transaction + transactionReceipts: TransactionReceipt[]; }; + ``` ## `TransactionSponsorReact` diff --git a/src/swap/components/Swap.test.tsx b/src/swap/components/Swap.test.tsx new file mode 100644 index 0000000000..3df9109668 --- /dev/null +++ b/src/swap/components/Swap.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import { Swap } from './Swap'; + +vi.mock('./SwapProvider', () => ({ + SwapProvider: ({ children }) => ( +
{children}
+ ), + useSwapContext: vi.fn(), +})); + +describe('Swap Component', () => { + it('should render the title correctly', () => { + render(); + + const title = screen.getByTestId('ockSwap_Title'); + expect(title).toHaveTextContent('Test Swap'); + }); + + it('should pass className to container div', () => { + render(); + + const container = screen.getByTestId('ockSwap_Container'); + expect(container).toHaveClass('custom-class'); + }); +}); diff --git a/src/swap/components/SwapToggleButton.test.tsx b/src/swap/components/SwapToggleButton.test.tsx new file mode 100644 index 0000000000..e65779733b --- /dev/null +++ b/src/swap/components/SwapToggleButton.test.tsx @@ -0,0 +1,31 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import { useSwapContext } from './SwapProvider'; +import { SwapToggleButton } from './SwapToggleButton'; + +vi.mock('./SwapProvider', () => ({ + useSwapContext: vi.fn(), +})); + +describe('SwapToggleButton', () => { + it('should call handleToggle when clicked', () => { + const handleToggleMock = vi.fn(); + (useSwapContext as jest.Mock).mockReturnValue({ + handleToggle: handleToggleMock, + }); + + render(); + + const button = screen.getByTestId('SwapTokensButton'); + fireEvent.click(button); + + expect(handleToggleMock).toHaveBeenCalled(); + }); + + it('should render with correct classes', () => { + render(); + + const button = screen.getByTestId('SwapTokensButton'); + expect(button).toHaveClass('custom-class'); + }); +}); diff --git a/src/transaction/components/TransactionProvider.test.tsx b/src/transaction/components/TransactionProvider.test.tsx index 1d3da03864..02fd2b8f0e 100644 --- a/src/transaction/components/TransactionProvider.test.tsx +++ b/src/transaction/components/TransactionProvider.test.tsx @@ -17,6 +17,8 @@ vi.mock('wagmi', () => ({ useAccount: vi.fn(), useSwitchChain: vi.fn(), useWaitForTransactionReceipt: vi.fn(), + useConfig: vi.fn(), + waitForTransactionReceipt: vi.fn(), })); vi.mock('../hooks/useCallsStatus', () => ({ @@ -145,6 +147,53 @@ describe('TransactionProvider', () => { ); }); }); + + it('should switch chains when required', async () => { + const switchChainAsyncMock = vi.fn(); + (useSwitchChain as ReturnType).mockReturnValue({ + switchChainAsync: switchChainAsyncMock, + }); + + render( + {}} + > + + , + ); + + const button = screen.getByText('Submit'); + fireEvent.click(button); + + await waitFor(() => { + expect(switchChainAsyncMock).toHaveBeenCalled(); + }); + }); + + it('should display toast on error', async () => { + (useWriteContracts as ReturnType).mockReturnValue({ + statusWriteContracts: 'IDLE', + writeContractsAsync: vi.fn().mockRejectedValue(new Error('Test error')), + }); + + render( + {}}> + + , + ); + + const button = screen.getByText('Submit'); + fireEvent.click(button); + + await waitFor(() => { + const testComponent = screen.getByTestId('context-value'); + const updatedContext = JSON.parse(testComponent.textContent || '{}'); + expect(updatedContext.isToastVisible).toBe(true); + }); + }); }); describe('useTransactionContext', () => { diff --git a/src/transaction/components/TransactionProvider.tsx b/src/transaction/components/TransactionProvider.tsx index a1a0f52234..521b7da937 100644 --- a/src/transaction/components/TransactionProvider.tsx +++ b/src/transaction/components/TransactionProvider.tsx @@ -5,12 +5,18 @@ import { useEffect, useState, } from 'react'; -import type { TransactionExecutionError } from 'viem'; +import type { + Address, + TransactionExecutionError, + TransactionReceipt, +} from 'viem'; import { useAccount, + useConfig, useSwitchChain, useWaitForTransactionReceipt, } from 'wagmi'; +import { waitForTransactionReceipt } from 'wagmi/actions'; import { useValue } from '../../internal/hooks/useValue'; import { GENERIC_ERROR_MESSAGE, @@ -50,7 +56,12 @@ export function TransactionProvider({ const [errorMessage, setErrorMessage] = useState(''); const [transactionId, setTransactionId] = useState(''); const [isToastVisible, setIsToastVisible] = useState(false); + const [transactionHashArray, setTransactionHashArray] = useState( + [], + ); + const [receiptArray, setReceiptArray] = useState([]); const account = useAccount(); + const config = useConfig(); const { switchChainAsync } = useSwitchChain(); const { status: statusWriteContracts, writeContractsAsync } = useWriteContracts({ @@ -65,7 +76,8 @@ export function TransactionProvider({ } = useWriteContract({ onError, setErrorMessage, - setTransactionId, + setTransactionHashArray, + transactionHashArray, }); const { transactionHash, status: callStatus } = useCallsStatus({ onError, @@ -76,6 +88,32 @@ export function TransactionProvider({ hash: writeContractTransactionHash || transactionHash, }); + const getTransactionReceipts = useCallback(async () => { + const receipts = []; + for (const hash of transactionHashArray) { + try { + const txnReceipt = await waitForTransactionReceipt(config, { + hash, + chainId, + }); + receipts.push(txnReceipt); + } catch (err) { + console.error('getTransactionReceiptsError', err); + setErrorMessage(GENERIC_ERROR_MESSAGE); + } + } + setReceiptArray(receipts); + }, [chainId, config, transactionHashArray]); + + useEffect(() => { + if ( + transactionHashArray.length === contracts.length && + contracts?.length > 1 + ) { + getTransactionReceipts(); + } + }, [contracts, getTransactionReceipts, transactionHashArray]); + const fallbackToWriteContract = useCallback(async () => { // EOAs don't support batching, so we process contracts individually. // This gracefully handles accidental batching attempts with EOAs. @@ -151,11 +189,12 @@ export function TransactionProvider({ }, [chainId, executeContracts, handleSubmitErrors, switchChain]); useEffect(() => { - const txnHash = transactionHash || writeContractTransactionHash; - if (txnHash && receipt) { - onSuccess?.({ transactionHash: txnHash, receipt }); + if (receiptArray?.length) { + onSuccess?.({ transactionReceipts: receiptArray }); + } else if (receipt) { + onSuccess?.({ transactionReceipts: [receipt] }); } - }, [onSuccess, receipt, transactionHash, writeContractTransactionHash]); + }, [onSuccess, receipt, receiptArray]); const value = useValue({ address, diff --git a/src/transaction/hooks/useCallsStatus.ts b/src/transaction/hooks/useCallsStatus.ts index f6f116d724..a5e0800549 100644 --- a/src/transaction/hooks/useCallsStatus.ts +++ b/src/transaction/hooks/useCallsStatus.ts @@ -19,6 +19,7 @@ export function useCallsStatus({ refetchInterval: (data) => { return data.state.data?.status === 'CONFIRMED' ? false : 1000; }, + enabled: !!transactionId, }, }); diff --git a/src/transaction/hooks/useWriteContract.test.ts b/src/transaction/hooks/useWriteContract.test.ts index 10ce006e95..bfb4c35cef 100644 --- a/src/transaction/hooks/useWriteContract.test.ts +++ b/src/transaction/hooks/useWriteContract.test.ts @@ -22,7 +22,7 @@ type MockUseWriteContractReturn = { describe('useWriteContract', () => { const mockSetErrorMessage = vi.fn(); - const mockSetTransactionId = vi.fn(); + const mockSetTransactionHashArray = vi.fn(); const mockOnError = vi.fn(); beforeEach(() => { @@ -41,7 +41,7 @@ describe('useWriteContract', () => { const { result } = renderHook(() => useWriteContract({ setErrorMessage: mockSetErrorMessage, - setTransactionId: mockSetTransactionId, + setTransactionHashArray: mockSetTransactionHashArray, onError: mockOnError, }), ); @@ -70,7 +70,7 @@ describe('useWriteContract', () => { renderHook(() => useWriteContract({ setErrorMessage: mockSetErrorMessage, - setTransactionId: mockSetTransactionId, + setTransactionHashArray: mockSetTransactionHashArray, onError: mockOnError, }), ); @@ -106,7 +106,7 @@ describe('useWriteContract', () => { renderHook(() => useWriteContract({ setErrorMessage: mockSetErrorMessage, - setTransactionId: mockSetTransactionId, + setTransactionHashArray: mockSetTransactionHashArray, onError: mockOnError, }), ); @@ -114,7 +114,7 @@ describe('useWriteContract', () => { expect(onSuccessCallback).toBeDefined(); onSuccessCallback?.(transactionId); - expect(mockSetTransactionId).toHaveBeenCalledWith(transactionId); + expect(mockSetTransactionHashArray).toHaveBeenCalledWith([transactionId]); }); it('should handle uncaught errors', () => { @@ -129,7 +129,7 @@ describe('useWriteContract', () => { const { result } = renderHook(() => useWriteContract({ setErrorMessage: mockSetErrorMessage, - setTransactionId: mockSetTransactionId, + setTransactionHashArray: mockSetTransactionHashArray, onError: mockOnError, }), ); diff --git a/src/transaction/hooks/useWriteContract.ts b/src/transaction/hooks/useWriteContract.ts index d07d37e64a..80382b415a 100644 --- a/src/transaction/hooks/useWriteContract.ts +++ b/src/transaction/hooks/useWriteContract.ts @@ -1,4 +1,4 @@ -import type { TransactionExecutionError } from 'viem'; +import type { Address, TransactionExecutionError } from 'viem'; import { useWriteContract as useWriteContractWagmi } from 'wagmi'; import { GENERIC_ERROR_MESSAGE, @@ -10,7 +10,8 @@ import type { TransactionError } from '../types'; type UseWriteContractParams = { onError?: (e: TransactionError) => void; setErrorMessage: (error: string) => void; - setTransactionId: (id: string) => void; + setTransactionHashArray: (ids: Address[]) => void; + transactionHashArray?: Address[]; }; /** @@ -21,7 +22,8 @@ type UseWriteContractParams = { export function useWriteContract({ onError, setErrorMessage, - setTransactionId, + setTransactionHashArray, + transactionHashArray, }: UseWriteContractParams) { try { const { status, writeContractAsync, data } = useWriteContractWagmi({ @@ -37,8 +39,10 @@ export function useWriteContract({ } onError?.({ code: WRITE_CONTRACT_ERROR_CODE, error: e.message }); }, - onSuccess: (id) => { - setTransactionId(id); + onSuccess: (hash: Address) => { + setTransactionHashArray( + transactionHashArray ? transactionHashArray?.concat(hash) : [hash], + ); }, }, }); diff --git a/src/transaction/types.ts b/src/transaction/types.ts index 247bd3c55d..174be2e2a3 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -62,7 +62,7 @@ export type TransactionProviderReact = { children: ReactNode; // The child components to be rendered within the provider component. contracts: ContractFunctionParameters[]; // An array of contract function parameters provided to the child components. onError?: (e: TransactionError) => void; // An optional callback function that handles errors within the provider. - onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes transaction hash + onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes the transaction receipts }; /** @@ -76,15 +76,14 @@ export type TransactionReact = { className?: string; // An optional CSS class name for styling the component. contracts: ContractFunctionParameters[]; // An array of contract function parameters for the transaction. onError?: (e: TransactionError) => void; // An optional callback function that handles transaction errors. - onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes transaction hash + onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes the transaction receipts }; /** * Note: exported as public Type */ export type TransactionResponse = { - transactionHash: string; // Proof that a transaction was validated and added to the blockchain - receipt: TransactionReceipt; // The receipt of the transaction + transactionReceipts: TransactionReceipt[]; }; /**