Skip to content

Commit

Permalink
feat: updated txn success handler to pass multiple receipts for multi…
Browse files Browse the repository at this point in the history
…ple contracts (EOA) (#945)

Co-authored-by: Alissa Crane <[email protected]>
  • Loading branch information
abcrane123 and alissacrane-cb authored Aug 5, 2024
1 parent baa0781 commit bfea1bf
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 25 deletions.
8 changes: 4 additions & 4 deletions site/docs/pages/transaction/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
```

Expand All @@ -67,17 +67,17 @@ 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
};
```

## `TransactionResponse`

```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`
Expand Down
26 changes: 26 additions & 0 deletions src/swap/components/Swap.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div data-testid="mock-SwapProvider">{children}</div>
),
useSwapContext: vi.fn(),
}));

describe('Swap Component', () => {
it('should render the title correctly', () => {
render(<Swap title="Test Swap" />);

const title = screen.getByTestId('ockSwap_Title');
expect(title).toHaveTextContent('Test Swap');
});

it('should pass className to container div', () => {
render(<Swap className="custom-class" />);

const container = screen.getByTestId('ockSwap_Container');
expect(container).toHaveClass('custom-class');
});
});
31 changes: 31 additions & 0 deletions src/swap/components/SwapToggleButton.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<SwapToggleButton />);

const button = screen.getByTestId('SwapTokensButton');
fireEvent.click(button);

expect(handleToggleMock).toHaveBeenCalled();
});

it('should render with correct classes', () => {
render(<SwapToggleButton className="custom-class" />);

const button = screen.getByTestId('SwapTokensButton');
expect(button).toHaveClass('custom-class');
});
});
49 changes: 49 additions & 0 deletions src/transaction/components/TransactionProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down Expand Up @@ -145,6 +147,53 @@ describe('TransactionProvider', () => {
);
});
});

it('should switch chains when required', async () => {
const switchChainAsyncMock = vi.fn();
(useSwitchChain as ReturnType<typeof vi.fn>).mockReturnValue({
switchChainAsync: switchChainAsyncMock,
});

render(
<TransactionProvider
address="0x123"
chainId={2}
contracts={[]}
onError={() => {}}
>
<TestComponent />
</TransactionProvider>,
);

const button = screen.getByText('Submit');
fireEvent.click(button);

await waitFor(() => {
expect(switchChainAsyncMock).toHaveBeenCalled();
});
});

it('should display toast on error', async () => {
(useWriteContracts as ReturnType<typeof vi.fn>).mockReturnValue({
statusWriteContracts: 'IDLE',
writeContractsAsync: vi.fn().mockRejectedValue(new Error('Test error')),
});

render(
<TransactionProvider address="0x123" contracts={[]} onError={() => {}}>
<TestComponent />
</TransactionProvider>,
);

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', () => {
Expand Down
51 changes: 45 additions & 6 deletions src/transaction/components/TransactionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,7 +56,12 @@ export function TransactionProvider({
const [errorMessage, setErrorMessage] = useState('');
const [transactionId, setTransactionId] = useState('');
const [isToastVisible, setIsToastVisible] = useState(false);
const [transactionHashArray, setTransactionHashArray] = useState<Address[]>(
[],
);
const [receiptArray, setReceiptArray] = useState<TransactionReceipt[]>([]);
const account = useAccount();
const config = useConfig();
const { switchChainAsync } = useSwitchChain();
const { status: statusWriteContracts, writeContractsAsync } =
useWriteContracts({
Expand All @@ -65,7 +76,8 @@ export function TransactionProvider({
} = useWriteContract({
onError,
setErrorMessage,
setTransactionId,
setTransactionHashArray,
transactionHashArray,
});
const { transactionHash, status: callStatus } = useCallsStatus({
onError,
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/transaction/hooks/useCallsStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function useCallsStatus({
refetchInterval: (data) => {
return data.state.data?.status === 'CONFIRMED' ? false : 1000;
},
enabled: !!transactionId,
},
});

Expand Down
12 changes: 6 additions & 6 deletions src/transaction/hooks/useWriteContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -41,7 +41,7 @@ describe('useWriteContract', () => {
const { result } = renderHook(() =>
useWriteContract({
setErrorMessage: mockSetErrorMessage,
setTransactionId: mockSetTransactionId,
setTransactionHashArray: mockSetTransactionHashArray,
onError: mockOnError,
}),
);
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('useWriteContract', () => {
renderHook(() =>
useWriteContract({
setErrorMessage: mockSetErrorMessage,
setTransactionId: mockSetTransactionId,
setTransactionHashArray: mockSetTransactionHashArray,
onError: mockOnError,
}),
);
Expand Down Expand Up @@ -106,15 +106,15 @@ describe('useWriteContract', () => {
renderHook(() =>
useWriteContract({
setErrorMessage: mockSetErrorMessage,
setTransactionId: mockSetTransactionId,
setTransactionHashArray: mockSetTransactionHashArray,
onError: mockOnError,
}),
);

expect(onSuccessCallback).toBeDefined();
onSuccessCallback?.(transactionId);

expect(mockSetTransactionId).toHaveBeenCalledWith(transactionId);
expect(mockSetTransactionHashArray).toHaveBeenCalledWith([transactionId]);
});

it('should handle uncaught errors', () => {
Expand All @@ -129,7 +129,7 @@ describe('useWriteContract', () => {
const { result } = renderHook(() =>
useWriteContract({
setErrorMessage: mockSetErrorMessage,
setTransactionId: mockSetTransactionId,
setTransactionHashArray: mockSetTransactionHashArray,
onError: mockOnError,
}),
);
Expand Down
14 changes: 9 additions & 5 deletions src/transaction/hooks/useWriteContract.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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[];
};

/**
Expand All @@ -21,7 +22,8 @@ type UseWriteContractParams = {
export function useWriteContract({
onError,
setErrorMessage,
setTransactionId,
setTransactionHashArray,
transactionHashArray,
}: UseWriteContractParams) {
try {
const { status, writeContractAsync, data } = useWriteContractWagmi({
Expand All @@ -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],
);
},
},
});
Expand Down
7 changes: 3 additions & 4 deletions src/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

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

/**
Expand Down

0 comments on commit bfea1bf

Please sign in to comment.