diff --git a/playground/nextjs-app-router/components/demo/Swap.tsx b/playground/nextjs-app-router/components/demo/Swap.tsx
index e3b0b69d5c..ff9df06297 100644
--- a/playground/nextjs-app-router/components/demo/Swap.tsx
+++ b/playground/nextjs-app-router/components/demo/Swap.tsx
@@ -11,8 +11,10 @@ import {
SwapSettingsSlippageTitle,
SwapToggleButton,
} from '@coinbase/onchainkit/swap';
+import type { SwapError } from '@coinbase/onchainkit/swap';
import type { Token } from '@coinbase/onchainkit/token';
import { useCallback, useContext } from 'react';
+import type { TransactionReceipt } from 'viem';
import { base } from 'viem/chains';
import { AppContext } from '../AppProvider';
@@ -65,6 +67,17 @@ function SwapComponent() {
console.log('Status:', lifeCycleStatus);
}, []);
+ const handleOnSuccess = useCallback(
+ (transactionReceipt: TransactionReceipt) => {
+ console.log('Success:', transactionReceipt);
+ },
+ [],
+ );
+
+ const handleOnError = useCallback((swapError: SwapError) => {
+ console.log('Error:', swapError);
+ }, []);
+
return (
{chainId !== base.id ? (
@@ -87,7 +100,12 @@ function SwapComponent() {
) : null}
-
+
Max. slippage
diff --git a/src/swap/components/SwapButton.test.tsx b/src/swap/components/SwapButton.test.tsx
index f9e1e4fa79..89351398d0 100644
--- a/src/swap/components/SwapButton.test.tsx
+++ b/src/swap/components/SwapButton.test.tsx
@@ -31,7 +31,7 @@ describe('SwapButton', () => {
address: '0x123',
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
- loading: false,
+ lifeCycleStatus: { statusName: 'init' },
handleSubmit: mockHandleSubmit,
});
render();
@@ -44,7 +44,33 @@ describe('SwapButton', () => {
useSwapContextMock.mockReturnValue({
to: { loading: true, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
- loading: false,
+ lifeCycleStatus: { statusName: 'init' },
+ handleSubmit: mockHandleSubmit,
+ });
+ render();
+ const button = screen.getByTestId('ockSwapButton_Button');
+ expect(screen.getByTestId('spinner')).toBeInTheDocument();
+ expect(button).toBeDisabled();
+ });
+
+ it('should render Spinner when transaction is pending', () => {
+ useSwapContextMock.mockReturnValue({
+ to: { loading: false, amount: 1, token: 'ETH' },
+ from: { loading: false, amount: 1, token: 'BTC' },
+ lifeCycleStatus: { statusName: 'transactionPending' },
+ handleSubmit: mockHandleSubmit,
+ });
+ render();
+ const button = screen.getByTestId('ockSwapButton_Button');
+ expect(screen.getByTestId('spinner')).toBeInTheDocument();
+ expect(button).toBeDisabled();
+ });
+
+ it('should render Spinner when transaction is approved', () => {
+ useSwapContextMock.mockReturnValue({
+ to: { loading: false, amount: 1, token: 'ETH' },
+ from: { loading: false, amount: 1, token: 'BTC' },
+ lifeCycleStatus: { statusName: 'transactionApproved' },
handleSubmit: mockHandleSubmit,
});
render();
@@ -57,7 +83,7 @@ describe('SwapButton', () => {
useSwapContextMock.mockReturnValue({
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: null, token: 'BTC' },
- loading: false,
+ lifeCycleStatus: { statusName: 'init' },
handleSubmit: mockHandleSubmit,
});
render();
@@ -70,7 +96,7 @@ describe('SwapButton', () => {
address: '0x123',
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
- loading: false,
+ lifeCycleStatus: { statusName: 'init' },
handleSubmit: mockHandleSubmit,
});
render();
@@ -84,7 +110,7 @@ describe('SwapButton', () => {
address: '0x123',
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
- loading: false,
+ lifeCycleStatus: { statusName: 'init' },
handleSubmit: mockHandleSubmit,
});
const customClass = 'custom-class';
@@ -97,7 +123,7 @@ describe('SwapButton', () => {
useSwapContextMock.mockReturnValue({
to: { loading: false, amount: 1, token: 'ETH' },
from: { loading: false, amount: 1, token: 'BTC' },
- loading: false,
+ lifeCycleStatus: { statusName: 'init' },
handleSubmit: mockHandleSubmit,
});
vi.mocked(useAccount).mockReturnValue({
diff --git a/src/swap/components/SwapButton.tsx b/src/swap/components/SwapButton.tsx
index 68dbb7d123..71405f424c 100644
--- a/src/swap/components/SwapButton.tsx
+++ b/src/swap/components/SwapButton.tsx
@@ -5,11 +5,19 @@ import type { SwapButtonReact } from '../types';
import { useSwapContext } from './SwapProvider';
export function SwapButton({ className, disabled = false }: SwapButtonReact) {
- const { address, to, from, loading, isTransactionPending, handleSubmit } =
- useSwapContext();
+ const {
+ address,
+ to,
+ from,
+ lifeCycleStatus: { statusName },
+ handleSubmit,
+ } = useSwapContext();
const isLoading =
- to.loading || from.loading || loading || isTransactionPending;
+ to.loading ||
+ from.loading ||
+ statusName === 'transactionPending' ||
+ statusName === 'transactionApproved';
const isDisabled =
!from.amount ||
diff --git a/src/swap/components/SwapMessage.test.tsx b/src/swap/components/SwapMessage.test.tsx
index 122f95f4de..49e458122c 100644
--- a/src/swap/components/SwapMessage.test.tsx
+++ b/src/swap/components/SwapMessage.test.tsx
@@ -27,9 +27,7 @@ describe('SwapMessage', () => {
const mockContext = {
to: {},
from: {},
- error: null,
- loading: false,
- lifeCycleStatus: { statusData: null },
+ lifeCycleStatus: { statusName: 'init', statusData: null },
};
useSwapContextMock.mockReturnValue(mockContext);
mockGetSwapMessage.mockReturnValue(mockMessage);
@@ -44,9 +42,10 @@ describe('SwapMessage', () => {
const mockContext = {
to: {},
from: {},
- error: 'Error occurred',
- loading: false,
- lifeCycleStatus: { statusData: null },
+ lifeCycleStatus: {
+ statusName: 'error',
+ statusData: { message: 'Error occurred' },
+ },
};
useSwapContextMock.mockReturnValue(mockContext);
mockGetSwapMessage.mockReturnValue(mockMessage);
@@ -55,14 +54,26 @@ describe('SwapMessage', () => {
expect(messageDiv).toHaveTextContent(mockMessage);
});
- it('should render with loading message', () => {
+ it('should render with loading message in transactionPending status', () => {
const mockMessage = 'Loading...';
const mockContext = {
to: {},
from: {},
- error: null,
- loading: true,
- lifeCycleStatus: { statusData: null },
+ lifeCycleStatus: { statusName: 'transactionPending', statusData: null },
+ };
+ useSwapContextMock.mockReturnValue(mockContext);
+ mockGetSwapMessage.mockReturnValue(mockMessage);
+ render();
+ const messageDiv = screen.getByTestId('ockSwapMessage_Message');
+ expect(messageDiv).toHaveTextContent(mockMessage);
+ });
+
+ it('should render with loading message in transactionApproved status', () => {
+ const mockMessage = 'Loading...';
+ const mockContext = {
+ to: {},
+ from: {},
+ lifeCycleStatus: { statusName: 'transactionApproved', statusData: null },
};
useSwapContextMock.mockReturnValue(mockContext);
mockGetSwapMessage.mockReturnValue(mockMessage);
@@ -75,9 +86,7 @@ describe('SwapMessage', () => {
const mockContext = {
to: {},
from: {},
- error: null,
- loading: false,
- lifeCycleStatus: { statusData: null },
+ lifeCycleStatus: { statusName: 'init', statusData: null },
};
useSwapContextMock.mockReturnValue(mockContext);
@@ -89,24 +98,22 @@ describe('SwapMessage', () => {
});
it('should set isMissingRequiredFields to true when reflected in statusData', () => {
+ const mockLifeCycleStatus = {
+ statusName: 'init',
+ statusData: { isMissingRequiredField: true },
+ };
const mockContext = {
to: { amount: 1, token: 'ETH' },
from: { amount: null, token: 'DAI' },
- error: null,
- loading: false,
- isTransactionPending: false,
address: '0x123',
- lifeCycleStatus: { statusData: { isMissingRequiredField: true } },
+ lifeCycleStatus: mockLifeCycleStatus,
};
useSwapContextMock.mockReturnValue(mockContext);
render();
expect(mockGetSwapMessage).toHaveBeenCalledWith({
address: '0x123',
- error: null,
from: { amount: null, token: 'DAI' },
- loading: false,
- isMissingRequiredFields: true,
- isTransactionPending: false,
+ lifeCycleStatus: mockLifeCycleStatus,
to: { amount: 1, token: 'ETH' },
});
});
diff --git a/src/swap/components/SwapMessage.tsx b/src/swap/components/SwapMessage.tsx
index c6ca3ac4a5..da5185077e 100644
--- a/src/swap/components/SwapMessage.tsx
+++ b/src/swap/components/SwapMessage.tsx
@@ -4,28 +4,12 @@ import { getSwapMessage } from '../utils/getSwapMessage';
import { useSwapContext } from './SwapProvider';
export function SwapMessage({ className }: SwapMessageReact) {
- const {
- address,
- to,
- from,
- error,
- loading,
- isTransactionPending,
- lifeCycleStatus: { statusData },
- } = useSwapContext();
-
- const isMissingRequiredFields =
- !!statusData &&
- 'isMissingRequiredField' in statusData &&
- statusData?.isMissingRequiredField;
+ const { address, to, from, lifeCycleStatus } = useSwapContext();
const message = getSwapMessage({
address,
- error,
from,
- loading,
- isMissingRequiredFields,
- isTransactionPending,
+ lifeCycleStatus,
to,
});
diff --git a/src/swap/components/SwapProvider.test.tsx b/src/swap/components/SwapProvider.test.tsx
index 8cc2e6590c..95792343af 100644
--- a/src/swap/components/SwapProvider.test.tsx
+++ b/src/swap/components/SwapProvider.test.tsx
@@ -113,7 +113,7 @@ const TestSwapComponent = () => {
context.to.setToken(DEGEN_TOKEN);
}, [context]);
const handleStatusError = async () => {
- context.setLifeCycleStatus({
+ context.updateLifeCycleStatus({
statusName: 'error',
statusData: {
code: 'code',
@@ -126,7 +126,7 @@ const TestSwapComponent = () => {
});
};
const handleStatusAmountChange = async () => {
- context.setLifeCycleStatus({
+ context.updateLifeCycleStatus({
statusName: 'amountChange',
statusData: {
amountFrom: '',
@@ -138,7 +138,7 @@ const TestSwapComponent = () => {
});
};
const handleStatusTransactionPending = async () => {
- context.setLifeCycleStatus({
+ context.updateLifeCycleStatus({
statusName: 'transactionPending',
statusData: {
// LifecycleStatus shared data
@@ -148,7 +148,7 @@ const TestSwapComponent = () => {
});
};
const handleStatusTransactionApproved = async () => {
- context.setLifeCycleStatus({
+ context.updateLifeCycleStatus({
statusName: 'transactionApproved',
statusData: {
transactionHash: '0x123',
@@ -160,10 +160,10 @@ const TestSwapComponent = () => {
});
};
const handleStatusSuccess = async () => {
- context.setLifeCycleStatus({
+ context.updateLifeCycleStatus({
statusName: 'success',
statusData: {
- receipt: ['0x123'],
+ transactionReceipt: { transactionHash: '0x123' },
// LifecycleStatus shared data
isMissingRequiredField: false,
maxSlippage: 3,
@@ -258,48 +258,13 @@ describe('SwapProvider', () => {
});
});
- it('should call setError when setLifeCycleStatus is called with error', async () => {
- const { result } = renderHook(() => useSwapContext(), { wrapper });
- const errorStatusData = {
- code: 'code',
- error: 'error_long_messages',
- message: 'test',
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
- };
- await act(async () => {
- result.current.setLifeCycleStatus({
- statusName: 'error',
- statusData: errorStatusData,
- });
- });
- expect(result.current.error).toBe(errorStatusData);
- });
-
- it('should call setError with undefined when setLifeCycleStatus is called with success', async () => {
- const { result } = renderHook(() => useSwapContext(), { wrapper });
- await act(async () => {
- result.current.setLifeCycleStatus({
- statusName: 'success',
- statusData: {
- receipt: ['0x123'],
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 5,
- },
- });
- });
- expect(result.current.error).toBeUndefined();
- });
-
it('should reset inputs when setLifeCycleStatus is called with success', async () => {
const { result } = renderHook(() => useSwapContext(), { wrapper });
await act(async () => {
- result.current.setLifeCycleStatus({
+ result.current.updateLifeCycleStatus({
statusName: 'success',
statusData: {
- transactionReceipt: '0x123',
+ transactionReceipt: { transactionHash: '0x123' },
// LifecycleStatus shared data
isMissingRequiredField: false,
maxSlippage: 5,
@@ -456,10 +421,10 @@ describe('SwapProvider', () => {
expect(onStatusMock).toHaveBeenCalledWith(
expect.objectContaining({
statusName: 'init',
- statusData: expect.objectContaining({
- isMissingRequiredField: false,
- maxSlippage: 3,
- }),
+ statusData: {
+ isMissingRequiredField: true,
+ maxSlippage: 10,
+ },
}),
);
});
@@ -594,15 +559,6 @@ describe('SwapProvider', () => {
expect(result.current.to.amount).toBe('10');
});
- it('should handle submit with missing data', async () => {
- const { result } = renderHook(() => useSwapContext(), { wrapper });
- await act(async () => {
- result.current.handleSubmit();
- });
- expect(result.current.error).toBeUndefined();
- expect(result.current.loading).toBe(false);
- });
-
it('should update amount and trigger quote', async () => {
const { result } = renderHook(() => useSwapContext(), { wrapper });
await act(async () => {
@@ -647,14 +603,11 @@ describe('SwapProvider', () => {
});
expect(result.current.lifeCycleStatus).toEqual({
statusName: 'error',
- statusData: {
+ statusData: expect.objectContaining({
code: 'TmSPc01',
error: JSON.stringify(mockError),
message: '',
- // LifecycleStatus shared data
- isMissingRequiredField: true,
- maxSlippage: 5,
- },
+ }),
});
});
@@ -670,14 +623,11 @@ describe('SwapProvider', () => {
});
expect(result.current.lifeCycleStatus).toEqual({
statusName: 'error',
- statusData: {
+ statusData: expect.objectContaining({
code: 'UNCAUGHT_SWAP_QUOTE_ERROR',
error: 'Something went wrong',
message: '',
- // LifecycleStatus shared data
- isMissingRequiredField: true,
- maxSlippage: 5,
- },
+ }),
});
});
@@ -730,9 +680,6 @@ describe('SwapProvider', () => {
code: getSwapErrorCode('uncaught-swap'),
error: 'Something went wrong',
message: '',
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
});
renderWithProviders({ Component: TestSwapComponent });
fireEvent.click(screen.getByText('Swap'));
diff --git a/src/swap/components/SwapProvider.tsx b/src/swap/components/SwapProvider.tsx
index e9f9a7d980..fce639dd7d 100644
--- a/src/swap/components/SwapProvider.tsx
+++ b/src/swap/components/SwapProvider.tsx
@@ -18,8 +18,8 @@ import { useFromTo } from '../hooks/useFromTo';
import { useResetInputs } from '../hooks/useResetInputs';
import type {
LifeCycleStatus,
+ LifeCycleStatusUpdate,
SwapContextType,
- SwapError,
SwapProviderReact,
} from '../types';
import { isSwapError } from '../utils/isSwapError';
@@ -52,9 +52,6 @@ export function SwapProvider({
const { useAggregator } = experimental;
// Core Hooks
const accountConfig = useConfig();
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState();
- const [isTransactionPending, setPendingTransaction] = useState(false);
const [lifeCycleStatus, setLifeCycleStatus] = useState({
statusName: 'init',
statusData: {
@@ -62,6 +59,30 @@ export function SwapProvider({
maxSlippage: config.maxSlippage,
},
}); // Component lifecycle
+
+ // Update lifecycle status, statusData will be persisted for the full lifeCycle
+ const updateLifeCycleStatus = useCallback(
+ (newStatus: LifeCycleStatusUpdate) => {
+ setLifeCycleStatus((prevStatus: LifeCycleStatus) => {
+ // do not persist errors
+ const persistedStatusData =
+ prevStatus.statusName === 'error'
+ ? (({ error, code, message, ...statusData }) => statusData)(
+ prevStatus.statusData,
+ )
+ : prevStatus.statusData;
+ return {
+ statusName: newStatus.statusName,
+ statusData: {
+ ...persistedStatusData,
+ ...newStatus.statusData,
+ },
+ } as LifeCycleStatus;
+ });
+ },
+ [],
+ );
+
const [hasHandledSuccess, setHasHandledSuccess] = useState(false);
const { from, to } = useFromTo(address);
const { sendTransactionAsync } = useSendTransaction(); // Sending the transaction (and approval, if applicable)
@@ -73,26 +94,10 @@ export function SwapProvider({
useEffect(() => {
// Error
if (lifeCycleStatus.statusName === 'error') {
- setLoading(false);
- setPendingTransaction(false);
- setError(lifeCycleStatus.statusData);
onError?.(lifeCycleStatus.statusData);
}
- if (lifeCycleStatus.statusName === 'amountChange') {
- setError(undefined);
- }
- if (lifeCycleStatus.statusName === 'transactionPending') {
- setLoading(true);
- setPendingTransaction(true);
- }
- if (lifeCycleStatus.statusName === 'transactionApproved') {
- setPendingTransaction(false);
- }
// Success
if (lifeCycleStatus.statusName === 'success') {
- setError(undefined);
- setLoading(false);
- setPendingTransaction(false);
onSuccess?.(lifeCycleStatus.statusData.transactionReceipt);
setHasHandledSuccess(true);
}
@@ -118,22 +123,21 @@ export function SwapProvider({
}, [hasHandledSuccess, lifeCycleStatus.statusName, resetInputs]);
useEffect(() => {
- const maxSlippage = lifeCycleStatus.statusData.maxSlippage;
// Reset status to init after success has been handled
if (lifeCycleStatus.statusName === 'success' && hasHandledSuccess) {
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'init',
statusData: {
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage,
+ isMissingRequiredField: true,
+ maxSlippage: config.maxSlippage,
},
});
}
}, [
+ config.maxSlippage,
hasHandledSuccess,
- lifeCycleStatus.statusData,
lifeCycleStatus.statusName,
+ updateLifeCycleStatus,
]);
const handleToggle = useCallback(() => {
@@ -141,7 +145,20 @@ export function SwapProvider({
to.setAmount(from.amount);
from.setToken(to.token);
to.setToken(from.token);
- }, [from, to]);
+
+ updateLifeCycleStatus({
+ statusName: 'amountChange',
+ statusData: {
+ amountFrom: from.amount,
+ amountTo: to.amount,
+ tokenFrom: from.token,
+ tokenTo: to.token,
+ // token is missing
+ isMissingRequiredField:
+ !from.token || !to.token || !from.amount || !to.amount,
+ },
+ });
+ }, [from, to, updateLifeCycleStatus]);
const handleAmountChange = useCallback(
async (
@@ -151,7 +168,6 @@ export function SwapProvider({
dToken?: Token,
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO Refactor this component
) => {
- const maxSlippage = lifeCycleStatus.statusData.maxSlippage;
const source = type === 'from' ? from : to;
const destination = type === 'from' ? to : from;
@@ -160,12 +176,11 @@ export function SwapProvider({
// if token is missing alert user via isMissingRequiredField
if (source.token === undefined || destination.token === undefined) {
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'amountChange',
statusData: {
amountFrom: from.amount,
amountTo: to.amount,
- maxSlippage,
tokenFrom: from.token,
tokenTo: to.token,
// token is missing
@@ -181,23 +196,23 @@ export function SwapProvider({
// When toAmount changes we fetch quote for fromAmount
// so set isFromQuoteLoading to true
destination.setLoading(true);
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'amountChange',
statusData: {
// when fetching quote, the previous
// amount is irrelevant
amountFrom: type === 'from' ? amount : '',
amountTo: type === 'to' ? amount : '',
+ tokenFrom: from.token,
+ tokenTo: to.token,
// when fetching quote, the destination
// amount is missing
isMissingRequiredField: true,
- maxSlippage,
- tokenFrom: from.token,
- tokenTo: to.token,
},
});
try {
+ const maxSlippage = lifeCycleStatus.statusData.maxSlippage;
const response = await getSwapQuote({
amount,
amountReference: 'from',
@@ -209,16 +224,12 @@ export function SwapProvider({
// If request resolves to error response set the quoteError
// property of error state to the SwapError response
if (isSwapError(response)) {
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'error',
statusData: {
code: response.code,
error: response.error,
message: '',
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage,
},
});
return;
@@ -228,30 +239,25 @@ export function SwapProvider({
response.to.decimals,
);
destination.setAmount(formattedAmount);
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'amountChange',
statusData: {
amountFrom: type === 'from' ? amount : formattedAmount,
amountTo: type === 'to' ? amount : formattedAmount,
+ tokenFrom: from.token,
+ tokenTo: to.token,
// if quote was fetched successfully, we
// have all required fields
isMissingRequiredField: !formattedAmount,
- maxSlippage,
- tokenFrom: from.token,
- tokenTo: to.token,
},
});
} catch (err) {
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'error',
statusData: {
code: 'TmSPc01', // Transaction module SwapProvider component 01 error
error: JSON.stringify(err),
message: '',
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage,
},
});
} finally {
@@ -259,23 +265,16 @@ export function SwapProvider({
destination.setLoading(false);
}
},
- [from, lifeCycleStatus, to, useAggregator],
+ [from, to, lifeCycleStatus, updateLifeCycleStatus, useAggregator],
);
const handleSubmit = useCallback(async () => {
if (!address || !from.token || !to.token || !from.amount) {
return;
}
- const maxSlippage = lifeCycleStatus.statusData.maxSlippage;
- setLifeCycleStatus({
- statusName: 'init',
- statusData: {
- isMissingRequiredField: false,
- maxSlippage,
- },
- });
try {
+ const maxSlippage = lifeCycleStatus.statusData.maxSlippage;
const response = await buildSwapTransaction({
amount: from.amount,
fromAddress: address,
@@ -285,25 +284,20 @@ export function SwapProvider({
useAggregator,
});
if (isSwapError(response)) {
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'error',
statusData: {
code: response.code,
error: response.error,
message: response.message,
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage,
},
});
return;
}
await processSwapTransaction({
config: accountConfig,
- lifeCycleStatus,
sendTransactionAsync,
- setLifeCycleStatus,
+ updateLifeCycleStatus,
swapTransaction: response,
useAggregator,
});
@@ -313,16 +307,12 @@ export function SwapProvider({
const errorMessage = isUserRejectedRequestError(err)
? 'Request denied.'
: GENERIC_ERROR_MESSAGE;
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'error',
statusData: {
code: 'TmSPc02', // Transaction module SwapProvider component 02 error
error: JSON.stringify(err),
message: errorMessage,
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage,
},
});
}
@@ -334,20 +324,18 @@ export function SwapProvider({
lifeCycleStatus,
sendTransactionAsync,
to.token,
+ updateLifeCycleStatus,
useAggregator,
]);
const value = useValue({
address,
- error,
from,
- loading,
handleAmountChange,
handleToggle,
handleSubmit,
lifeCycleStatus,
- isTransactionPending,
- setLifeCycleStatus,
+ updateLifeCycleStatus,
to,
});
diff --git a/src/swap/components/SwapSettingsSlippageInput.test.tsx b/src/swap/components/SwapSettingsSlippageInput.test.tsx
index 721dcd56cc..7f7084d46f 100644
--- a/src/swap/components/SwapSettingsSlippageInput.test.tsx
+++ b/src/swap/components/SwapSettingsSlippageInput.test.tsx
@@ -13,7 +13,7 @@ let mockLifeCycleStatus = {
vi.mock('./SwapProvider', () => ({
useSwapContext: () => ({
- setLifeCycleStatus: mockSetLifeCycleStatus,
+ updateLifeCycleStatus: mockSetLifeCycleStatus,
lifeCycleStatus: mockLifeCycleStatus,
}),
}));
@@ -63,7 +63,6 @@ describe('SwapSettingsSlippageInput', () => {
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'slippageChange',
statusData: {
- isMissingRequiredField: false,
maxSlippage: 2.5,
},
});
@@ -88,7 +87,6 @@ describe('SwapSettingsSlippageInput', () => {
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'slippageChange',
statusData: {
- isMissingRequiredField: false,
maxSlippage: 1.5,
},
});
@@ -112,7 +110,6 @@ describe('SwapSettingsSlippageInput', () => {
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'slippageChange',
statusData: {
- isMissingRequiredField: false,
maxSlippage: 2.75,
},
});
@@ -195,7 +192,6 @@ describe('SwapSettingsSlippageInput', () => {
expect(mockSetLifeCycleStatus).toHaveBeenLastCalledWith({
statusName: 'slippageChange',
statusData: {
- isMissingRequiredField: false,
maxSlippage: 3,
},
});
diff --git a/src/swap/components/SwapSettingsSlippageInput.tsx b/src/swap/components/SwapSettingsSlippageInput.tsx
index becd646657..96d1d2f84b 100644
--- a/src/swap/components/SwapSettingsSlippageInput.tsx
+++ b/src/swap/components/SwapSettingsSlippageInput.tsx
@@ -12,7 +12,7 @@ export function SwapSettingsSlippageInput({
className,
defaultSlippage = 3,
}: SwapSettingsSlippageInputReact) {
- const { setLifeCycleStatus, lifeCycleStatus } = useSwapContext();
+ const { updateLifeCycleStatus, lifeCycleStatus } = useSwapContext();
const getMaxSlippage = useCallback(() => {
if (lifeCycleStatus.statusName !== 'error') {
return lifeCycleStatus.statusData.maxSlippage;
@@ -32,15 +32,14 @@ export function SwapSettingsSlippageInput({
const updateSlippage = useCallback(
(newSlippage: number) => {
setSlippage(newSlippage);
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'slippageChange',
statusData: {
- isMissingRequiredField: false,
maxSlippage: newSlippage,
},
});
},
- [setLifeCycleStatus],
+ [updateLifeCycleStatus],
);
// Handles user input for custom slippage
diff --git a/src/swap/types.ts b/src/swap/types.ts
index 0a5f5fd482..aa29c2c0bc 100644
--- a/src/swap/types.ts
+++ b/src/swap/types.ts
@@ -25,10 +25,7 @@ export type FromTo = {
export type GetSwapMessageParams = {
address?: Address;
- error?: SwapError;
- loading?: boolean;
- isTransactionPending?: boolean;
- isMissingRequiredFields?: boolean;
+ lifeCycleStatus: LifeCycleStatus;
to: SwapUnit;
from: SwapUnit;
};
@@ -42,7 +39,7 @@ export type QuoteWarning = {
type?: string; // The type of the warning
};
-type LifecycleStatusDataShared = {
+type LifeCycleStatusDataShared = {
isMissingRequiredField: boolean;
maxSlippage: number;
};
@@ -56,11 +53,11 @@ type LifecycleStatusDataShared = {
export type LifeCycleStatus =
| {
statusName: 'init';
- statusData: LifecycleStatusDataShared;
+ statusData: LifeCycleStatusDataShared;
}
| {
statusName: 'error';
- statusData: SwapError & LifecycleStatusDataShared;
+ statusData: SwapError & LifeCycleStatusDataShared;
}
| {
statusName: 'amountChange';
@@ -69,34 +66,69 @@ export type LifeCycleStatus =
amountTo: string;
tokenFrom?: Token;
tokenTo?: Token;
- } & LifecycleStatusDataShared;
+ } & LifeCycleStatusDataShared;
}
| {
statusName: 'slippageChange';
- statusData: LifecycleStatusDataShared;
+ statusData: LifeCycleStatusDataShared;
}
| {
statusName: 'transactionPending';
- statusData: LifecycleStatusDataShared;
+ statusData: LifeCycleStatusDataShared;
}
| {
statusName: 'transactionApproved';
statusData: {
transactionHash: Hex;
transactionType: 'ERC20' | 'Permit2';
- } & LifecycleStatusDataShared;
+ } & LifeCycleStatusDataShared;
}
| {
statusName: 'success';
statusData: {
transactionReceipt: TransactionReceipt;
- } & LifecycleStatusDataShared;
+ } & LifeCycleStatusDataShared;
};
+// make all keys in T optional if they are in K
+type PartialKeys = Omit &
+ Partial> extends infer O
+ ? { [P in keyof O]: O[P] }
+ : never;
+
+// check if all keys in T are a key of LifeCycleStatusDataShared
+type AllKeysInShared = keyof T extends keyof LifeCycleStatusDataShared
+ ? true
+ : false;
+
+/**
+ * LifeCycleStatus updater type
+ * Used to type the statuses used to update LifeCycleStatus
+ * LifeCycleStatusData is persisted across state updates allowing SharedData to be optional except for in init step
+ */
+export type LifeCycleStatusUpdate = LifeCycleStatus extends infer T
+ ? T extends { statusName: infer N; statusData: infer D }
+ ? { statusName: N } & (N extends 'init' // statusData required in statusName "init"
+ ? { statusData: D }
+ : AllKeysInShared extends true // is statusData is LifeCycleStatusDataShared, make optional
+ ? {
+ statusData?: PartialKeys<
+ D,
+ keyof D & keyof LifeCycleStatusDataShared
+ >;
+ } // make all keys in LifeCycleStatusDataShared optional
+ : {
+ statusData: PartialKeys<
+ D,
+ keyof D & keyof LifeCycleStatusDataShared
+ >;
+ })
+ : never
+ : never;
+
export type ProcessSwapTransactionParams = {
config: Config;
- lifeCycleStatus: LifeCycleStatus;
- setLifeCycleStatus: (state: LifeCycleStatus) => void;
+ updateLifeCycleStatus: (state: LifeCycleStatusUpdate) => void;
sendTransactionAsync: SendTransactionMutateAsync;
swapTransaction: BuildSwapTransaction;
useAggregator: boolean;
@@ -132,11 +164,8 @@ export type SwapButtonReact = {
export type SwapContextType = {
address?: Address; // Used to check if user is connected in SwapButton
- error?: SwapError;
from: SwapUnit;
lifeCycleStatus: LifeCycleStatus;
- loading: boolean;
- isTransactionPending: boolean;
handleAmountChange: (
t: 'from' | 'to',
amount: string,
@@ -145,7 +174,7 @@ export type SwapContextType = {
) => void;
handleSubmit: () => void;
handleToggle: () => void;
- setLifeCycleStatus: (state: LifeCycleStatus) => void; // A function to set the lifecycle status of the component
+ updateLifeCycleStatus: (state: LifeCycleStatusUpdate) => void; // A function to set the lifecycle status of the component
to: SwapUnit;
};
diff --git a/src/swap/utils/getSwapMessage.test.ts b/src/swap/utils/getSwapMessage.test.ts
index 0516bbe1d1..b22985e1d3 100644
--- a/src/swap/utils/getSwapMessage.test.ts
+++ b/src/swap/utils/getSwapMessage.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, vi } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import {
LOW_LIQUIDITY_ERROR_CODE,
TOO_MANY_REQUESTS_ERROR_CODE,
@@ -12,8 +12,7 @@ import { SwapMessage, getSwapMessage } from './getSwapMessage';
describe('getSwapMessage', () => {
const baseParams = {
- address: '0x123',
- error: undefined,
+ address: '0x123' as `0x${string}`,
from: {
error: undefined,
balance: '0',
@@ -33,8 +32,10 @@ describe('getSwapMessage', () => {
setLoading: vi.fn(),
setToken: vi.fn(),
},
- loading: false,
- isMissingRequiredFields: false,
+ lifeCycleStatus: {
+ statusName: 'init',
+ statusData: { isMissingRequiredField: false, maxSlippage: 3 },
+ },
};
it('should return BALANCE_ERROR when from or to has an error', () => {
@@ -68,7 +69,7 @@ describe('getSwapMessage', () => {
it('should return CONFIRM IN WALLET when pending transaction', () => {
const params = {
...baseParams,
- isTransactionPending: true,
+ lifeCycleStatus: { statusName: 'transactionPending', statusData: null },
};
expect(getSwapMessage(params)).toBe(SwapMessage.CONFIRM_IN_WALLET);
});
@@ -76,7 +77,7 @@ describe('getSwapMessage', () => {
it('should return SWAP_IN_PROGRESS when loading is true', () => {
const params = {
...baseParams,
- loading: true,
+ lifeCycleStatus: { statusName: 'transactionApproved', statusData: null },
};
expect(getSwapMessage(params)).toBe(SwapMessage.SWAP_IN_PROGRESS);
});
@@ -98,7 +99,10 @@ describe('getSwapMessage', () => {
it('should return INCOMPLETE_FIELD when required fields are missing', () => {
const params = {
...baseParams,
- isMissingRequiredFields: true,
+ lifeCycleStatus: {
+ statusName: 'init',
+ statusData: { isMissingRequiredField: true },
+ },
};
expect(getSwapMessage(params)).toBe(SwapMessage.INCOMPLETE_FIELD);
});
@@ -113,10 +117,13 @@ describe('getSwapMessage', () => {
token: ETH_TOKEN,
},
to: { ...baseParams.to, amount: '5', token: USDC_TOKEN },
- error: {
- code: TOO_MANY_REQUESTS_ERROR_CODE,
- error: 'Too many requests error',
- message: '',
+ lifeCycleStatus: {
+ statusName: 'error',
+ statusData: {
+ code: TOO_MANY_REQUESTS_ERROR_CODE,
+ error: 'Too many requests error',
+ message: '',
+ },
},
};
expect(getSwapMessage(params)).toBe(SwapMessage.TOO_MANY_REQUESTS);
@@ -132,10 +139,13 @@ describe('getSwapMessage', () => {
token: ETH_TOKEN,
},
to: { ...baseParams.to, amount: '5', token: USDC_TOKEN },
- error: {
- code: LOW_LIQUIDITY_ERROR_CODE,
- error: 'Low liquidity error',
- message: '',
+ lifeCycleStatus: {
+ statusName: 'error',
+ statusData: {
+ code: LOW_LIQUIDITY_ERROR_CODE,
+ error: 'Low liquidity error',
+ message: '',
+ },
},
};
expect(getSwapMessage(params)).toBe(SwapMessage.LOW_LIQUIDITY);
@@ -151,10 +161,13 @@ describe('getSwapMessage', () => {
token: ETH_TOKEN,
},
to: { ...baseParams.to, amount: '5', token: USDC_TOKEN },
- error: {
- code: USER_REJECTED_ERROR_CODE,
- error: 'User rejected error',
- message: '',
+ lifeCycleStatus: {
+ statusName: 'error',
+ statusData: {
+ code: USER_REJECTED_ERROR_CODE,
+ error: 'User rejected error',
+ message: '',
+ },
},
};
expect(getSwapMessage(params)).toBe(SwapMessage.USER_REJECTED);
@@ -170,10 +183,13 @@ describe('getSwapMessage', () => {
token: ETH_TOKEN,
},
to: { ...baseParams.to, amount: '5', token: USDC_TOKEN },
- error: {
- code: 'general_error_code',
- error: 'General error occurred',
- message: '',
+ lifeCycleStatus: {
+ statusName: 'error',
+ statusData: {
+ code: 'general_error_code',
+ error: 'General error occurred',
+ message: '',
+ },
},
};
expect(getSwapMessage(params)).toBe('');
diff --git a/src/swap/utils/getSwapMessage.ts b/src/swap/utils/getSwapMessage.ts
index 55b75e59ba..83985d6014 100644
--- a/src/swap/utils/getSwapMessage.ts
+++ b/src/swap/utils/getSwapMessage.ts
@@ -16,11 +16,8 @@ export enum SwapMessage {
export function getSwapMessage({
address,
- error,
from,
- loading,
- isMissingRequiredFields,
- isTransactionPending,
+ lifeCycleStatus,
to,
}: GetSwapMessageParams) {
// handle balance error
@@ -32,23 +29,25 @@ export function getSwapMessage({
return SwapMessage.INSUFFICIENT_BALANCE;
}
// handle pending transaction
- if (isTransactionPending) {
+ if (lifeCycleStatus.statusName === 'transactionPending') {
return SwapMessage.CONFIRM_IN_WALLET;
}
// handle loading states
- if (loading) {
+ if (lifeCycleStatus.statusName === 'transactionApproved') {
return SwapMessage.SWAP_IN_PROGRESS;
}
if (to.loading || from.loading) {
return SwapMessage.FETCHING_QUOTE;
}
// missing required fields
- if (isMissingRequiredFields) {
+ if (lifeCycleStatus.statusData.isMissingRequiredField) {
return SwapMessage.INCOMPLETE_FIELD;
}
- if (!error) {
- return '';
- }
+
// handle specific error codes
- return getErrorMessage(error);
+ if (lifeCycleStatus.statusName === 'error') {
+ return getErrorMessage(lifeCycleStatus.statusData);
+ }
+
+ return '';
}
diff --git a/src/swap/utils/processSwapTransaction.test.ts b/src/swap/utils/processSwapTransaction.test.ts
index ea51cea8bf..74cfd9282f 100644
--- a/src/swap/utils/processSwapTransaction.test.ts
+++ b/src/swap/utils/processSwapTransaction.test.ts
@@ -1,11 +1,11 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { http, createConfig } from 'wagmi';
import { waitForTransactionReceipt } from 'wagmi/actions';
import { mainnet, sepolia } from 'wagmi/chains';
import { mock } from 'wagmi/connectors';
+import type { BuildSwapTransaction } from '../../api/types';
import { PERMIT2_CONTRACT_ADDRESS } from '../constants';
import { DEGEN_TOKEN, ETH_TOKEN, USDC_TOKEN } from '../mocks';
-import type { BuildSwapTransaction, LifeCycleStatus } from '../types';
import { processSwapTransaction } from './processSwapTransaction';
vi.mock('wagmi/actions', () => ({
@@ -13,16 +13,9 @@ vi.mock('wagmi/actions', () => ({
}));
describe('processSwapTransaction', () => {
- const setLifeCycleStatus = vi.fn();
- const sendTransactionAsync = vi
- .fn()
- .mockResolvedValueOnce('approveTxHash')
- .mockResolvedValueOnce('txHash');
- const sendTransactionAsyncPermit2 = vi
- .fn()
- .mockResolvedValueOnce('approveTxHash')
- .mockResolvedValueOnce('permit2TxHash')
- .mockResolvedValueOnce('txHash');
+ const updateLifeCycleStatus = vi.fn();
+ let sendTransactionAsync: Mock;
+ let sendTransactionAsyncPermit2: Mock;
const config = createConfig({
chains: [mainnet, sepolia],
@@ -41,16 +34,19 @@ describe('processSwapTransaction', () => {
},
});
- const defaultLifeCycleStatus: LifeCycleStatus = {
- statusName: 'init',
- statusData: {
- isMissingRequiredField: false,
- maxSlippage: 3,
- },
- };
-
beforeEach(() => {
vi.clearAllMocks();
+
+ sendTransactionAsync = vi
+ .fn()
+ .mockResolvedValueOnce('approveTxHash')
+ .mockResolvedValueOnce('txHash');
+
+ sendTransactionAsyncPermit2 = vi
+ .fn()
+ .mockResolvedValueOnce('approveTxHash')
+ .mockResolvedValueOnce('permit2TxHash')
+ .mockResolvedValueOnce('txHash');
});
it('should request approval and make the swap for ERC-20 tokens', async () => {
@@ -105,35 +101,29 @@ describe('processSwapTransaction', () => {
await processSwapTransaction({
config,
sendTransactionAsync,
- setLifeCycleStatus,
+ updateLifeCycleStatus,
swapTransaction,
useAggregator: true,
- lifeCycleStatus: defaultLifeCycleStatus,
});
- expect(setLifeCycleStatus).toHaveBeenCalledTimes(4);
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(1, {
+ expect(updateLifeCycleStatus).toHaveBeenCalledTimes(5);
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(1, {
statusName: 'transactionPending',
- statusData: {
- isMissingRequiredField: false,
- maxSlippage: 3,
- },
});
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(2, {
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(2, {
statusName: 'transactionApproved',
statusData: {
transactionHash: 'approveTxHash',
transactionType: 'ERC20',
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
},
});
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(3, {
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(3, {
statusName: 'transactionPending',
+ });
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(4, {
+ statusName: 'transactionApproved',
statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
+ transactionHash: 'txHash',
+ transactionType: 'ERC20',
},
});
expect(sendTransactionAsync).toHaveBeenCalledTimes(2);
@@ -170,18 +160,19 @@ describe('processSwapTransaction', () => {
await processSwapTransaction({
config,
sendTransactionAsync,
- setLifeCycleStatus,
+ updateLifeCycleStatus,
swapTransaction,
useAggregator: true,
- lifeCycleStatus: defaultLifeCycleStatus,
});
- expect(setLifeCycleStatus).toHaveBeenCalledTimes(2);
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(1, {
+ expect(updateLifeCycleStatus).toHaveBeenCalledTimes(3);
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(1, {
statusName: 'transactionPending',
+ });
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(2, {
+ statusName: 'transactionApproved',
statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
+ transactionHash: 'approveTxHash',
+ transactionType: 'ERC20',
},
});
expect(sendTransactionAsync).toHaveBeenCalledTimes(1);
@@ -224,46 +215,39 @@ describe('processSwapTransaction', () => {
await processSwapTransaction({
config,
sendTransactionAsync: sendTransactionAsyncPermit2,
- setLifeCycleStatus,
+ updateLifeCycleStatus,
swapTransaction,
useAggregator: false,
- lifeCycleStatus: defaultLifeCycleStatus,
});
- expect(setLifeCycleStatus).toHaveBeenCalledTimes(6);
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(1, {
+ expect(updateLifeCycleStatus).toHaveBeenCalledTimes(7);
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(1, {
statusName: 'transactionPending',
- statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
- },
});
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(2, {
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(2, {
statusName: 'transactionApproved',
statusData: {
transactionHash: 'approveTxHash',
transactionType: 'Permit2',
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
},
});
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(3, {
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(3, {
statusName: 'transactionPending',
- statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
- },
});
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(4, {
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(4, {
statusName: 'transactionApproved',
statusData: {
transactionHash: 'permit2TxHash',
transactionType: 'ERC20',
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
+ },
+ });
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(5, {
+ statusName: 'transactionPending',
+ });
+ expect(updateLifeCycleStatus).toHaveBeenNthCalledWith(6, {
+ statusName: 'transactionApproved',
+ statusData: {
+ transactionHash: 'txHash',
+ transactionType: 'Permit2',
},
});
expect(sendTransactionAsyncPermit2).toHaveBeenCalledTimes(3);
@@ -274,63 +258,4 @@ describe('processSwapTransaction', () => {
value: 0n,
});
});
-
- it('should use default maxSlippage when lifeCycleStatus is error', async () => {
- const errorLifeCycleStatus: LifeCycleStatus = {
- statusName: 'error',
- statusData: {
- code: 'UNKNOWN_ERROR',
- error: 'Some error occurred',
- message: 'Some error occurred',
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
- },
- };
- const swapTransaction: BuildSwapTransaction = {
- transaction: {
- to: '0x123',
- value: 0n,
- data: '0x',
- chainId: 8453,
- gas: 0n,
- },
- approveTransaction: undefined,
- quote: {
- from: ETH_TOKEN,
- to: DEGEN_TOKEN,
- fromAmount: '100000000000000',
- toAmount: '19395353519910973703',
- amountReference: 'from',
- priceImpact: '0.94',
- hasHighPriceImpact: false,
- slippage: '3',
- warning: undefined,
- },
- fee: {
- baseAsset: DEGEN_TOKEN,
- percentage: '1',
- amount: '195912661817282562',
- },
- };
- await processSwapTransaction({
- config,
- sendTransactionAsync,
- setLifeCycleStatus,
- swapTransaction,
- useAggregator: true,
- lifeCycleStatus: errorLifeCycleStatus,
- });
- expect(setLifeCycleStatus).toHaveBeenCalledTimes(2);
- expect(setLifeCycleStatus).toHaveBeenNthCalledWith(1, {
- statusName: 'transactionPending',
- statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField: false,
- maxSlippage: 3,
- },
- });
- expect(sendTransactionAsync).toHaveBeenCalledTimes(1);
- expect(waitForTransactionReceipt).toHaveBeenCalledTimes(1);
- });
});
diff --git a/src/swap/utils/processSwapTransaction.ts b/src/swap/utils/processSwapTransaction.ts
index 65af6c4df8..e031728c78 100644
--- a/src/swap/utils/processSwapTransaction.ts
+++ b/src/swap/utils/processSwapTransaction.ts
@@ -9,9 +9,8 @@ import type { ProcessSwapTransactionParams } from '../types';
export async function processSwapTransaction({
config,
- lifeCycleStatus,
sendTransactionAsync,
- setLifeCycleStatus,
+ updateLifeCycleStatus,
swapTransaction,
useAggregator,
}: ProcessSwapTransactionParams) {
@@ -23,29 +22,19 @@ export async function processSwapTransaction({
// for V1 API, `approveTx` will be an ERC-20 approval against the Router
// for V2 API, `approveTx` will be an ERC-20 approval against the `Permit2` contract
if (approveTransaction?.data) {
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'transactionPending',
- statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage: lifeCycleStatus.statusData.maxSlippage,
- },
});
const approveTxHash = await sendTransactionAsync({
to: approveTransaction.to,
value: approveTransaction.value,
data: approveTransaction.data,
});
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'transactionApproved',
statusData: {
transactionHash: approveTxHash,
transactionType: useAggregator ? 'ERC20' : 'Permit2',
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage: lifeCycleStatus.statusData.maxSlippage,
},
});
await waitForTransactionReceipt(config, {
@@ -59,14 +48,8 @@ export async function processSwapTransaction({
// this would typically be a (gasless) signature, but we're using a transaction here to allow batching for Smart Wallets
// read more: https://blog.uniswap.org/permit2-and-universal-router
if (!useAggregator) {
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'transactionPending',
- statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage: lifeCycleStatus.statusData.maxSlippage,
- },
});
const permit2ContractAbi = parseAbi([
'function approve(address token, address spender, uint160 amount, uint48 expiration) external',
@@ -86,15 +69,11 @@ export async function processSwapTransaction({
data: data,
value: 0n,
});
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'transactionApproved',
statusData: {
transactionHash: permitTxnHash,
transactionType: 'ERC20',
- // LifecycleStatus shared data
- isMissingRequiredField:
- lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage: lifeCycleStatus.statusData.maxSlippage,
},
});
await waitForTransactionReceipt(config, {
@@ -105,32 +84,30 @@ export async function processSwapTransaction({
}
// make the swap
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'transactionPending',
- statusData: {
- // LifecycleStatus shared data
- isMissingRequiredField: lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage: lifeCycleStatus.statusData.maxSlippage,
- },
});
const txHash = await sendTransactionAsync({
to: transaction.to,
value: transaction.value,
data: transaction.data,
});
-
+ updateLifeCycleStatus({
+ statusName: 'transactionApproved',
+ statusData: {
+ transactionHash: txHash,
+ transactionType: useAggregator ? 'ERC20' : 'Permit2',
+ },
+ });
// wait for swap to land onchain
const transactionReceipt = await waitForTransactionReceipt(config, {
hash: txHash,
confirmations: 1,
});
- setLifeCycleStatus({
+ updateLifeCycleStatus({
statusName: 'success',
statusData: {
transactionReceipt: transactionReceipt,
- // LifecycleStatus shared data
- isMissingRequiredField: lifeCycleStatus.statusData.isMissingRequiredField,
- maxSlippage: lifeCycleStatus.statusData.maxSlippage,
},
});
}