Skip to content

Commit

Permalink
feat: keep polishing onStatus lifecycle (#1055)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zizzamia authored Aug 15, 2024
1 parent b9773c5 commit 4cb7ea2
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 126 deletions.
127 changes: 56 additions & 71 deletions src/transaction/components/TransactionProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div data-testid="test-component">
<button type="button" onClick={context.onSubmit}>
Expand All @@ -55,9 +63,12 @@ const TestComponent = () => {
<span data-testid="context-value-isToastVisible">
{`${context.isToastVisible}`}
</span>
<button type="button" onClick={handleSetLifeCycleStatus}>
<button type="button" onClick={handleStatusError}>
setLifeCycleStatus.error
</button>
<button type="button" onClick={handleStatusTransactionLegacyExecuted}>
setLifeCycleStatus.transactionLegacyExecuted
</button>
</div>
);
};
Expand Down Expand Up @@ -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(
<TransactionProvider
Expand All @@ -110,41 +121,46 @@ describe('TransactionProvider', () => {
<TestComponent />
</TransactionProvider>,
);
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<typeof vi.fn>).mockReturnValue({
statusWriteContracts: 'IDLE',
writeContractsAsync: writeContractsAsyncMock,
});
it('should emit onStatus when setLifeCycleStatus is called', async () => {
const onStatusMock = vi.fn();
render(
<TransactionProvider address="0x123" contracts={[]}>
<TransactionProvider
address="0x123"
contracts={[]}
onStatus={onStatusMock}
>
<TestComponent />
</TransactionProvider>,
);
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<typeof vi.fn>).mockReturnValue({
data: '123',
data: { status: 'success' },
});
(useCallsStatus as ReturnType<typeof vi.fn>).mockReturnValue({
transactionHash: 'hash',
});
render(
<TransactionProvider
address="0x123"
contracts={[]}
contracts={[{ address: '0x123', method: 'method' }]}
onSuccess={onSuccessMock}
>
<TestComponent />
Expand All @@ -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<typeof vi.fn>).mockReturnValue({
statusWriteContracts: 'IDLE',
writeContractsAsync: writeContractsAsyncMock,
});
render(
<TransactionProvider address="0x123" contracts={[]}>
<TestComponent />
</TransactionProvider>,
);
const button = screen.getByText('Submit');
fireEvent.click(button);
await waitFor(() => {
expect(writeContractsAsyncMock).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -252,30 +289,6 @@ describe('TransactionProvider', () => {
});
});

it('should call onSuccess when receipts are available', async () => {
const onSuccessMock = vi.fn();
(useWaitForTransactionReceipt as ReturnType<typeof vi.fn>).mockReturnValue({
data: { status: 'success' },
});
(useCallsStatus as ReturnType<typeof vi.fn>).mockReturnValue({
transactionHash: 'hash',
});
render(
<TransactionProvider
address="0x123"
contracts={[]}
onSuccess={onSuccessMock}
>
<TestComponent />
</TransactionProvider>,
);
await waitFor(() => {
expect(onSuccessMock).toHaveBeenCalledWith({
transactionReceipts: [{ status: 'success' }],
});
});
});

it('should handle chain switching', async () => {
const switchChainAsyncMock = vi.fn();
(useSwitchChain as ReturnType<typeof vi.fn>).mockReturnValue({
Expand Down Expand Up @@ -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<typeof vi.fn>).mockReturnValue({
data: mockReceipt,
});

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

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

await waitFor(() => {
expect(onSuccessMock).toHaveBeenCalledWith({
transactionReceipts: [mockReceipt],
});
});
});
});

describe('useTransactionContext', () => {
Expand Down
79 changes: 50 additions & 29 deletions src/transaction/components/TransactionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
useEffect,
useState,
} from 'react';
import type { Address, TransactionReceipt } from 'viem';
import type { Address } from 'viem';
import {
useAccount,
useConfig,
Expand Down Expand Up @@ -62,11 +62,9 @@ export function TransactionProvider({
statusName: 'init',
statusData: null,
}); // Component lifecycle
const [receiptArray, setReceiptArray] = useState<TransactionReceipt[]>([]);
const [transactionId, setTransactionId] = useState('');
const [transactionHashArray, setTransactionHashArray] = useState<Address[]>(
[],
);
const [transactionHashList, setTransactionHashList] = useState<Address[]>([]);

const { switchChainAsync } = useSwitchChain();

// Hooks that depend from Core Hooks
Expand All @@ -81,8 +79,7 @@ export function TransactionProvider({
data: writeContractTransactionHash,
} = useWriteContract({
setLifeCycleStatus,
setTransactionHashArray,
transactionHashArray,
transactionHashList,
});
const { transactionHash, status: callStatus } = useCallsStatus({
setLifeCycleStatus,
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
Empty file.
Loading

0 comments on commit 4cb7ea2

Please sign in to comment.