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[];
};
/**