Skip to content

Commit 59ffa6a

Browse files
committed
feat(STX-331): refresh STX liveness on bridge flow
1 parent ccfdca7 commit 59ffa6a

File tree

14 files changed

+185
-33
lines changed

14 files changed

+185
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@
371371
"@metamask/selected-network-controller": "^25.0.0",
372372
"@metamask/shield-controller": "^4.0.0",
373373
"@metamask/signature-controller": "^38.0.0",
374-
"@metamask/smart-transactions-controller": "^21.0.0",
374+
"@metamask/smart-transactions-controller": "^21.1.0",
375375
"@metamask/snaps-controllers": "^17.2.0",
376376
"@metamask/snaps-execution-environments": "^10.3.0",
377377
"@metamask/snaps-rpc-methods": "^14.1.1",

shared/modules/selectors/index.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ describe('Selectors', () => {
5959
},
6060
smartTransactionsState: {
6161
liveness: true,
62+
livenessByChainId: {
63+
[CHAIN_IDS.MAINNET]: true,
64+
[CHAIN_IDS.BSC]: true,
65+
[CHAIN_IDS.SEPOLIA]: true,
66+
[CHAIN_IDS.LINEA_MAINNET]: true,
67+
},
6268
},
6369
...mockNetworkState({
6470
id: 'network-configuration-id-1',
@@ -195,10 +201,22 @@ describe('Selectors', () => {
195201
);
196202

197203
jestIt(
198-
'returns false if feature flag is enabled, not a HW, STX liveness is false and is Ethereum network',
204+
'returns false if feature flag is enabled, not a HW, STX liveness is false for chain',
205+
() => {
206+
const state = createSwapsMockStore();
207+
state.metamask.smartTransactionsState.livenessByChainId[
208+
CHAIN_IDS.MAINNET
209+
] = false;
210+
expect(getSmartTransactionsEnabled(state)).toBe(false);
211+
},
212+
);
213+
214+
jestIt(
215+
'returns false if feature flag is enabled, not a HW, STX liveness is not set for chain',
199216
() => {
200217
const state = createSwapsMockStore();
201-
state.metamask.smartTransactionsState.liveness = false;
218+
// @ts-expect-error Testing undefined liveness for chain
219+
state.metamask.smartTransactionsState.livenessByChainId = {};
202220
expect(getSmartTransactionsEnabled(state)).toBe(false);
203221
},
204222
);

shared/modules/selectors/smart-transactions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type SmartTransactionsMetaMaskState = {
5151
};
5252
smartTransactionsState: {
5353
liveness: boolean;
54+
livenessByChainId: Record<string, boolean>;
5455
};
5556
};
5657
};
@@ -176,14 +177,17 @@ export const getSmartTransactionsEnabled = (
176177
state: SmartTransactionsState,
177178
chainId?: string,
178179
): boolean => {
180+
const effectiveChainId = chainId ?? getCurrentChainId(state);
179181
const supportedAccount = accountSupportsSmartTx(state);
180182
// @ts-expect-error Smart transaction selector types does not match controller state
181183
const featureFlagsByChainId = getFeatureFlagsByChainId(state, chainId);
182184
// TODO: Create a new proxy service only for MM feature flags.
183185
const smartTransactionsFeatureFlagEnabled =
184186
featureFlagsByChainId?.smartTransactions?.extensionActive;
185187
const smartTransactionsLiveness =
186-
state.metamask.smartTransactionsState?.liveness;
188+
state.metamask.smartTransactionsState?.livenessByChainId?.[
189+
effectiveChainId
190+
];
187191
return Boolean(
188192
getChainSupportsSmartTransactions(state, chainId) &&
189193
getIsAllowedRpcUrlForSmartTransactions(state, chainId) &&

test/integration/data/integration-init-state.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,10 @@
840840
"smartTransactionsState": {
841841
"fees": {},
842842
"liveness": true,
843+
"livenessByChainId": {
844+
"0x1": true,
845+
"0xaa36a7": true
846+
},
843847
"smartTransactions": {
844848
"0x1": [],
845849
"0xaa36a7": []

test/jest/mock-store.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,12 @@ export const createSwapsMockStore = () => {
661661
userOptIn: true,
662662
userOptInV2: true,
663663
liveness: true,
664+
livenessByChainId: {
665+
[CHAIN_IDS.MAINNET]: true,
666+
[CHAIN_IDS.BSC]: true,
667+
[CHAIN_IDS.SEPOLIA]: true,
668+
[CHAIN_IDS.LINEA_MAINNET]: true,
669+
},
664670
fees: createGetSmartTransactionFeesApiResponse(),
665671
smartTransactions: {
666672
[CHAIN_IDS.MAINNET]: [

ui/ducks/bridge/selectors.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,6 +1910,9 @@ describe('Bridge selectors', () => {
19101910
},
19111911
smartTransactionsState: {
19121912
liveness: true,
1913+
livenessByChainId: {
1914+
'0x1': true,
1915+
},
19131916
},
19141917
swapsState: {
19151918
swapsFeatureFlags: {
@@ -1947,6 +1950,9 @@ describe('Bridge selectors', () => {
19471950
},
19481951
smartTransactionsState: {
19491952
liveness: true,
1953+
livenessByChainId: {
1954+
'0x1': true,
1955+
},
19501956
},
19511957
swapsState: {
19521958
swapsFeatureFlags: {

ui/ducks/swaps/swaps.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
setSwapsFeatureFlags,
2323
setSelectedQuoteAggId,
2424
setSwapsTxGasLimit,
25-
fetchSmartTransactionsLiveness,
2625
signAndSendSmartTransaction,
2726
updateSmartTransaction,
2827
setSmartTransactionsRefreshInterval,
@@ -599,11 +598,6 @@ export const fetchSwapsLivenessAndFeatureFlags = () => {
599598
await dispatch(setSwapsFeatureFlags(swapsFeatureFlags));
600599
if (ALLOWED_SMART_TRANSACTIONS_CHAIN_IDS.includes(chainId)) {
601600
await dispatch(setCurrentSmartTransactionsError(undefined));
602-
await dispatch(
603-
fetchSmartTransactionsLiveness({
604-
networkClientId: getSelectedNetworkClientId(state),
605-
}),
606-
);
607601
const transactions = await getTransactions({
608602
searchCriteria: {
609603
chainId,

ui/ducks/swaps/swaps.test.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const middleware = [thunk];
1515
jest.mock('../../store/actions.ts', () => ({
1616
setSwapsLiveness: jest.fn(),
1717
setSwapsFeatureFlags: jest.fn(),
18-
fetchSmartTransactionsLiveness: jest.fn(),
1918
getTransactions: jest.fn(() => {
2019
return [];
2120
}),
@@ -85,7 +84,7 @@ describe('Ducks - Swaps', () => {
8584
createGetState(),
8685
);
8786
expect(featureFlagApiNock.isDone()).toBe(true);
88-
expect(mockDispatch).toHaveBeenCalledTimes(5);
87+
expect(mockDispatch).toHaveBeenCalledTimes(4);
8988
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
9089
expect(setSwapsFeatureFlags).toHaveBeenCalledWith(featureFlagsResponse);
9190
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
@@ -106,7 +105,7 @@ describe('Ducks - Swaps', () => {
106105
createGetState(),
107106
);
108107
expect(featureFlagApiNock.isDone()).toBe(true);
109-
expect(mockDispatch).toHaveBeenCalledTimes(5);
108+
expect(mockDispatch).toHaveBeenCalledTimes(4);
110109
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
111110
expect(setSwapsFeatureFlags).toHaveBeenCalledWith(featureFlagsResponse);
112111
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
@@ -128,7 +127,7 @@ describe('Ducks - Swaps', () => {
128127
createGetState(),
129128
);
130129
expect(featureFlagApiNock.isDone()).toBe(true);
131-
expect(mockDispatch).toHaveBeenCalledTimes(5);
130+
expect(mockDispatch).toHaveBeenCalledTimes(4);
132131
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
133132
expect(setSwapsFeatureFlags).toHaveBeenCalledWith(featureFlagsResponse);
134133
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
@@ -174,7 +173,7 @@ describe('Ducks - Swaps', () => {
174173
createGetState(),
175174
);
176175
expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead.
177-
expect(mockDispatch).toHaveBeenCalledTimes(10);
176+
expect(mockDispatch).toHaveBeenCalledTimes(8);
178177
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
179178
expect(setSwapsFeatureFlags).toHaveBeenCalledWith(featureFlagsResponse);
180179
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import * as reactRedux from 'react-redux';
3+
import { CHAIN_IDS } from '@metamask/transaction-controller';
4+
import { useRefreshSmartTransactionsLiveness } from './useRefreshSmartTransactionsLiveness';
5+
6+
const mockInnerFn = jest.fn();
7+
8+
jest.mock('react-redux', () => ({
9+
useSelector: jest.fn(),
10+
}));
11+
12+
jest.mock('../../../store/actions', () => ({
13+
fetchSmartTransactionsLiveness: jest.fn(() => mockInnerFn),
14+
}));
15+
16+
const mockFetchSmartTransactionsLiveness = jest.requireMock(
17+
'../../../store/actions',
18+
).fetchSmartTransactionsLiveness;
19+
20+
describe('useRefreshSmartTransactionsLiveness', () => {
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
(reactRedux.useSelector as jest.Mock).mockReturnValue(true); // opted in by default
24+
});
25+
26+
it('does not fetch when chainId is null', () => {
27+
renderHook(() => useRefreshSmartTransactionsLiveness(null));
28+
expect(mockFetchSmartTransactionsLiveness).not.toHaveBeenCalled();
29+
});
30+
31+
it('does not fetch when chainId is undefined', () => {
32+
renderHook(() => useRefreshSmartTransactionsLiveness(undefined));
33+
expect(mockFetchSmartTransactionsLiveness).not.toHaveBeenCalled();
34+
});
35+
36+
it('does not fetch for non-EVM chains', () => {
37+
renderHook(() => useRefreshSmartTransactionsLiveness('solana:mainnet'));
38+
expect(mockFetchSmartTransactionsLiveness).not.toHaveBeenCalled();
39+
});
40+
41+
it('does not fetch for unsupported EVM chains', () => {
42+
renderHook(() => useRefreshSmartTransactionsLiveness('0x999'));
43+
expect(mockFetchSmartTransactionsLiveness).not.toHaveBeenCalled();
44+
});
45+
46+
it('does not fetch when user has not opted in', () => {
47+
(reactRedux.useSelector as jest.Mock).mockReturnValue(false);
48+
renderHook(() => useRefreshSmartTransactionsLiveness(CHAIN_IDS.MAINNET));
49+
expect(mockFetchSmartTransactionsLiveness).not.toHaveBeenCalled();
50+
});
51+
52+
it('fetches smart transactions liveness for mainnet', () => {
53+
renderHook(() => useRefreshSmartTransactionsLiveness(CHAIN_IDS.MAINNET));
54+
expect(mockFetchSmartTransactionsLiveness).toHaveBeenCalledTimes(1);
55+
expect(mockFetchSmartTransactionsLiveness).toHaveBeenCalledWith({
56+
chainId: CHAIN_IDS.MAINNET,
57+
});
58+
expect(mockInnerFn).toHaveBeenCalledTimes(1);
59+
});
60+
61+
it('re-fetches when chainId changes to another supported chain', () => {
62+
const { rerender } = renderHook<{ chainId: string }, void>(
63+
({ chainId }) => useRefreshSmartTransactionsLiveness(chainId),
64+
{ initialProps: { chainId: CHAIN_IDS.MAINNET } },
65+
);
66+
67+
expect(mockFetchSmartTransactionsLiveness).toHaveBeenCalledTimes(1);
68+
expect(mockInnerFn).toHaveBeenCalledTimes(1);
69+
70+
// BSC is in both production and development allowed lists
71+
rerender({ chainId: CHAIN_IDS.BSC });
72+
expect(mockFetchSmartTransactionsLiveness).toHaveBeenCalledTimes(2);
73+
expect(mockInnerFn).toHaveBeenCalledTimes(2);
74+
});
75+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useEffect } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { getAllowedSmartTransactionsChainIds } from '../../../../shared/constants/smartTransactions';
4+
import { getSmartTransactionsPreferenceEnabled } from '../../../../shared/modules/selectors';
5+
import { fetchSmartTransactionsLiveness } from '../../../store/actions';
6+
import { isNonEvmChain } from '../../../ducks/bridge/utils';
7+
8+
/**
9+
* Hook that fetches smart transactions liveness for a given chain.
10+
* Ensures fresh liveness data is fetched when entering the page
11+
* and when the chain changes.
12+
*
13+
* @param chainId - The chain ID to check for STX support (string or null/undefined).
14+
*/
15+
export function useRefreshSmartTransactionsLiveness(
16+
chainId: string | null | undefined,
17+
): void {
18+
const smartTransactionsOptInStatus = useSelector(
19+
getSmartTransactionsPreferenceEnabled,
20+
);
21+
22+
useEffect(() => {
23+
if (!chainId || !smartTransactionsOptInStatus) {
24+
return;
25+
}
26+
27+
if (isNonEvmChain(chainId)) {
28+
return;
29+
}
30+
31+
// TODO: will be replaced with feature flags once we have them.
32+
const allowedChainId = getAllowedSmartTransactionsChainIds().find(
33+
(id) => id === chainId,
34+
);
35+
36+
if (allowedChainId) {
37+
fetchSmartTransactionsLiveness({ chainId: allowedChainId })();
38+
}
39+
}, [chainId, smartTransactionsOptInStatus]);
40+
}

0 commit comments

Comments
 (0)