Skip to content

Commit 858c1e5

Browse files
feat: refresh metamask pay quotes (MetaMask#19365)
## **Description** Automatically refresh quotes in confirmation based on the refresh rate in the existing Bridge feature flags. Automatically refresh quotes for all but first bridge in the `PayHook`. Increase buffer on subsequent bridges using new `bufferSubsequent` feature flag. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [MetaMask#5734](MetaMask/MetaMask-planning#5734) ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent 1a02740 commit 858c1e5

17 files changed

Lines changed: 304 additions & 135 deletions

File tree

app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function PayWithRow() {
3333
const { styles } = useStyles(styleSheet, {});
3434
const navigation = useNavigation();
3535
const { payToken } = useTransactionPayToken();
36-
const { totalFiat } = useTransactionRequiredFiat();
36+
const { totalFiat } = useTransactionRequiredFiat({ log: true });
3737

3838
useTransactionBridgeQuotes();
3939

app/components/Views/confirmations/components/rows/total-row/total-row.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react';
1+
import React from 'react';
22
import Text from '../../../../../../component-library/components/Texts/Text';
33
import InfoRow from '../../UI/info-row';
44
import { useTransactionTotalFiat } from '../../../hooks/pay/useTransactionTotalFiat';
@@ -12,23 +12,15 @@ import AnimatedSpinner, {
1212
} from '../../../../../UI/AnimatedSpinner';
1313
import { View } from 'react-native';
1414
import { TransactionType } from '@metamask/transaction-controller';
15-
import { createProjectLogger } from '@metamask/utils';
16-
17-
const log = createProjectLogger('transaction-pay');
1815

1916
export function TotalRow() {
2017
const { id: transactionId, type } = useTransactionMetadataOrThrow();
21-
const totals = useTransactionTotalFiat();
22-
const { totalFormatted } = totals;
18+
const { totalFormatted } = useTransactionTotalFiat({ log: true });
2319

2420
const isQuotesLoading = useSelector((state: RootState) =>
2521
selectIsTransactionBridgeQuotesLoadingById(state, transactionId),
2622
);
2723

28-
useEffect(() => {
29-
log('Total fiat', totals);
30-
}, [totals]);
31-
3224
return (
3325
<View testID="total-row">
3426
<InfoRow

app/components/Views/confirmations/external/perps-temp/components/deposit/deposit.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jest.mock('../../hooks/usePerpsDepositView');
2323
jest.mock('../../../../hooks/useTokenAsset');
2424
jest.mock('../../../../hooks/useTokenAmount');
2525
jest.mock('../../../../hooks/ui/useClearConfirmationOnBackSwipe');
26+
jest.mock('../../../../hooks/pay/useTransactionBridgeQuotes');
2627

2728
jest.mock('@react-navigation/native', () => ({
2829
...jest.requireActual('@react-navigation/native'),

app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function useAutomaticTransactionPayToken({
3434
);
3535

3636
const tokens = useTokensWithBalance({ chainIds });
37-
const requiredTokens = useTransactionRequiredTokens();
37+
const requiredTokens = useTransactionRequiredTokens({ log: true });
3838
const { chainId } = useTransactionMetadataRequest() ?? {};
3939
const { setPayToken } = useTransactionPayToken();
4040
const { totalFiat } = useTransactionRequiredFiat();

app/components/Views/confirmations/hooks/pay/useTransactionBridgeQuotes.test.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,26 @@ import {
1515
ATTEMPTS_MAX_DEFAULT,
1616
BUFFER_INITIAL_DEFAULT,
1717
BUFFER_STEP_DEFAULT,
18-
SLIPPAGE_INITIAL_DEFAULT,
19-
SLIPPAGE_SUBSEQUENT_DEFAULT,
18+
BUFFER_SUBSEQUENT_DEFAULT,
19+
SLIPPAGE_DEFAULT,
2020
} from '../../../../../selectors/featureFlagController/confirmations';
21+
import { initialState } from '../../../../UI/Bridge/_mocks_/initialState';
2122

2223
jest.mock('./useTransactionPayToken');
2324
jest.mock('./useTransactionPayTokenAmounts');
2425
jest.mock('../transactions/useTransactionMetadataRequest');
2526
jest.mock('../../utils/bridge');
2627
jest.mock('../../context/alert-system-context');
2728

29+
jest.mock(
30+
'../../../../../core/redux/slices/bridge/utils/hasMinimumRequiredVersion',
31+
() => ({
32+
hasMinimumRequiredVersion: jest.fn().mockReturnValue(true),
33+
}),
34+
);
35+
36+
jest.useFakeTimers();
37+
2838
const TRANSACTION_ID_MOCK = '1234-5678';
2939
const CHAIN_ID_SOURCE_MOCK = '0x1';
3040
const CHAIN_ID_TARGET_MOCK = '0x2';
@@ -43,8 +53,9 @@ const QUOTE_MOCK = {
4353
} as TransactionBridgeQuote;
4454

4555
function runHook() {
46-
return renderHookWithProvider(useTransactionBridgeQuotes, { state: {} })
47-
.result;
56+
return renderHookWithProvider(useTransactionBridgeQuotes, {
57+
state: initialState,
58+
});
4859
}
4960

5061
describe('useTransactionBridgeQuotes', () => {
@@ -115,9 +126,9 @@ describe('useTransactionBridgeQuotes', () => {
115126
attemptsMax: ATTEMPTS_MAX_DEFAULT,
116127
bufferInitial: BUFFER_INITIAL_DEFAULT,
117128
bufferStep: BUFFER_STEP_DEFAULT,
129+
bufferSubsequent: BUFFER_SUBSEQUENT_DEFAULT,
118130
from: ACCOUNT_ADDRESS_MOCK,
119-
slippageInitial: SLIPPAGE_INITIAL_DEFAULT,
120-
slippageSubsequent: SLIPPAGE_SUBSEQUENT_DEFAULT,
131+
slippage: SLIPPAGE_DEFAULT,
121132
sourceBalanceRaw: SOURCE_BALANCE_RAW_MOCK,
122133
sourceChainId: CHAIN_ID_SOURCE_MOCK,
123134
sourceTokenAddress: TOKEN_ADDRESS_SOURCE_MOCK,
@@ -130,9 +141,9 @@ describe('useTransactionBridgeQuotes', () => {
130141
attemptsMax: ATTEMPTS_MAX_DEFAULT,
131142
bufferInitial: BUFFER_INITIAL_DEFAULT,
132143
bufferStep: BUFFER_STEP_DEFAULT,
144+
bufferSubsequent: BUFFER_SUBSEQUENT_DEFAULT,
133145
from: ACCOUNT_ADDRESS_MOCK,
134-
slippageInitial: SLIPPAGE_INITIAL_DEFAULT,
135-
slippageSubsequent: SLIPPAGE_SUBSEQUENT_DEFAULT,
146+
slippage: SLIPPAGE_DEFAULT,
136147
sourceBalanceRaw: SOURCE_BALANCE_RAW_MOCK,
137148
sourceChainId: CHAIN_ID_SOURCE_MOCK,
138149
sourceTokenAddress: TOKEN_ADDRESS_SOURCE_MOCK,
@@ -145,7 +156,7 @@ describe('useTransactionBridgeQuotes', () => {
145156
});
146157

147158
it('returns bridge quotes', async () => {
148-
const result = runHook();
159+
const { result } = runHook();
149160

150161
await act(async () => {
151162
// Intentionally empty
@@ -177,7 +188,7 @@ describe('useTransactionBridgeQuotes', () => {
177188
payToken: undefined,
178189
} as unknown as ReturnType<typeof useTransactionPayToken>);
179190

180-
const result = runHook();
191+
const { result } = runHook();
181192

182193
await act(async () => {
183194
// Intentionally empty
@@ -196,7 +207,7 @@ describe('useTransactionBridgeQuotes', () => {
196207
],
197208
} as unknown as ReturnType<typeof useAlerts>);
198209

199-
const result = runHook();
210+
const { result } = runHook();
200211

201212
await act(async () => {
202213
// Intentionally empty
@@ -217,7 +228,7 @@ describe('useTransactionBridgeQuotes', () => {
217228
],
218229
} as unknown as ReturnType<typeof useAlerts>);
219230

220-
const result = runHook();
231+
const { result } = runHook();
221232

222233
await act(async () => {
223234
// Intentionally empty
@@ -226,4 +237,24 @@ describe('useTransactionBridgeQuotes', () => {
226237
expect(result.current.quotes).toStrictEqual([QUOTE_MOCK, QUOTE_MOCK]);
227238
},
228239
);
240+
241+
it('refreshes quotes', async () => {
242+
runHook();
243+
244+
await act(async () => {
245+
// Intentionally empty
246+
});
247+
248+
await act(async () => {
249+
await jest.advanceTimersByTimeAsync(29000);
250+
});
251+
252+
expect(getBridgeQuotesMock).toHaveBeenCalledTimes(1);
253+
254+
await act(async () => {
255+
await jest.advanceTimersByTimeAsync(2000);
256+
});
257+
258+
expect(getBridgeQuotesMock).toHaveBeenCalledTimes(2);
259+
});
229260
});

app/components/Views/confirmations/hooks/pay/useTransactionBridgeQuotes.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useTransactionPayTokenAmounts } from './useTransactionPayTokenAmounts';
22
import { useAsyncResult } from '../../../../hooks/useAsyncResult';
33
import { BridgeQuoteRequest, getBridgeQuotes } from '../../utils/bridge';
4-
import { useEffect } from 'react';
4+
import { useEffect, useRef, useState } from 'react';
55
import { useTransactionPayToken } from './useTransactionPayToken';
66
import { useDispatch, useSelector } from 'react-redux';
77
import {
@@ -14,6 +14,11 @@ import { useDeepMemo } from '../useDeepMemo';
1414
import { useAlerts } from '../../context/alert-system-context';
1515
import { AlertKeys } from '../../constants/alerts';
1616
import { selectMetaMaskPayFlags } from '../../../../../selectors/featureFlagController/confirmations';
17+
import {
18+
getQuoteRefreshRate,
19+
isQuoteExpired,
20+
} from '../../../../UI/Bridge/utils/quoteUtils';
21+
import { selectBridgeFeatureFlags } from '../../../../../core/redux/slices/bridge';
1722

1823
const EXCLUDED_ALERTS = [
1924
AlertKeys.NoPayTokenQuotes,
@@ -26,14 +31,43 @@ export function useTransactionBridgeQuotes() {
2631
const dispatch = useDispatch();
2732
const transactionMeta = useTransactionMetadataOrThrow();
2833
const { alerts } = useAlerts();
34+
const { payToken } = useTransactionPayToken() ?? {};
35+
const { amounts: sourceAmounts } = useTransactionPayTokenAmounts({
36+
log: true,
37+
});
38+
const bridgeFeatureFlags = useSelector(selectBridgeFeatureFlags);
39+
const refreshRate = getQuoteRefreshRate(bridgeFeatureFlags);
40+
const [lastFetched, setLastFetched] = useState(0);
41+
const interval = useRef<NodeJS.Timer>();
42+
const isExpired = useRef(false);
43+
const [refreshIndex, setRefreshIndex] = useState(0);
2944

30-
const {
31-
attemptsMax,
32-
bufferInitial,
33-
bufferStep,
34-
slippageInitial,
35-
slippageSubsequent,
36-
} = useSelector(selectMetaMaskPayFlags);
45+
useEffect(() => {
46+
if (interval.current) {
47+
clearInterval(interval.current as unknown as number);
48+
}
49+
50+
interval.current = setInterval(() => {
51+
if (
52+
!isExpired.current &&
53+
lastFetched &&
54+
isQuoteExpired(false, refreshRate, lastFetched)
55+
) {
56+
log('Quote expired', { refreshRate, lastFetched });
57+
isExpired.current = true;
58+
setRefreshIndex((index) => index + 1);
59+
}
60+
}, 1000);
61+
62+
return () => {
63+
if (interval.current) {
64+
clearInterval(interval.current as unknown as number);
65+
}
66+
};
67+
}, [isExpired, lastFetched, refreshRate]);
68+
69+
const { attemptsMax, bufferInitial, bufferStep, bufferSubsequent, slippage } =
70+
useSelector(selectMetaMaskPayFlags);
3771

3872
const hasBlockingAlert = alerts.some(
3973
(a) => a.isBlocking && !EXCLUDED_ALERTS.includes(a.key as AlertKeys),
@@ -45,9 +79,6 @@ export function useTransactionBridgeQuotes() {
4579
txParams: { from },
4680
} = transactionMeta;
4781

48-
const { payToken } = useTransactionPayToken() ?? {};
49-
const { amounts: sourceAmounts } = useTransactionPayTokenAmounts();
50-
5182
const {
5283
address: sourceTokenAddress,
5384
balanceRaw,
@@ -77,9 +108,9 @@ export function useTransactionBridgeQuotes() {
77108
attemptsMax,
78109
bufferInitial,
79110
bufferStep,
111+
bufferSubsequent,
80112
from: from as Hex,
81-
slippageInitial,
82-
slippageSubsequent,
113+
slippage,
83114
sourceBalanceRaw,
84115
sourceChainId,
85116
sourceTokenAddress,
@@ -95,8 +126,7 @@ export function useTransactionBridgeQuotes() {
95126
bufferStep,
96127
from,
97128
hasBlockingAlert,
98-
slippageInitial,
99-
slippageSubsequent,
129+
slippage,
100130
sourceAmounts,
101131
sourceBalanceRaw,
102132
sourceChainId,
@@ -110,7 +140,7 @@ export function useTransactionBridgeQuotes() {
110140
}
111141

112142
return getBridgeQuotes(requests);
113-
}, [requests]);
143+
}, [requests, refreshIndex]);
114144

115145
useEffect(() => {
116146
dispatch(
@@ -121,6 +151,11 @@ export function useTransactionBridgeQuotes() {
121151
useEffect(() => {
122152
dispatch(setTransactionBridgeQuotes({ transactionId, quotes }));
123153

154+
if (quotes?.length) {
155+
isExpired.current = false;
156+
setLastFetched(Date.now());
157+
}
158+
124159
log(
125160
'Bridge quotes',
126161
quotes?.map((quote) => ({

app/components/Views/confirmations/hooks/pay/useTransactionPayTokenAmounts.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@ import { useTokenFiatRates } from '../tokens/useTokenFiatRates';
55
import { useTransactionRequiredFiat } from './useTransactionRequiredFiat';
66
import { Hex, createProjectLogger } from '@metamask/utils';
77
import { useDeepMemo } from '../useDeepMemo';
8+
import { noop } from 'lodash';
89

9-
const log = createProjectLogger('transaction-pay');
10+
const logger = createProjectLogger('transaction-pay');
1011

1112
/**
1213
* Calculate the amount of the selected pay token, that is needed for each token required by the transaction.
1314
*/
1415
export function useTransactionPayTokenAmounts({
1516
amountOverrides,
17+
log: isLoggingEnabled,
1618
}: {
1719
amountOverrides?: Record<Hex, string>;
20+
log?: boolean;
1821
} = {}) {
1922
const { payToken } = useTransactionPayToken();
2023
const { address, chainId, decimals } = payToken ?? {};
24+
const log = isLoggingEnabled ? logger : noop;
2125

2226
const fiatRequests = useMemo(() => {
2327
if (!address || !chainId) {
@@ -50,14 +54,15 @@ export function useTransactionPayTokenAmounts({
5054

5155
return values
5256
.filter((value) => {
57+
const { address: currentAddress } = value;
5358
const hasBalance = value.balanceFiat > value.amountFiat;
5459

5560
const isSameTokenSelected =
5661
address.toLowerCase() === value.address.toLowerCase();
5762

5863
const hasOtherTokenWithoutBalance = values.some(
5964
(v) =>
60-
v.address.toLowerCase() !== address.toLowerCase() &&
65+
v.address.toLowerCase() !== currentAddress.toLowerCase() &&
6166
v.balanceFiat < v.amountFiat,
6267
);
6368

@@ -121,7 +126,7 @@ export function useTransactionPayTokenAmounts({
121126
totalHuman,
122127
totalRaw,
123128
});
124-
}, [amounts, totalHuman, totalRaw]);
129+
}, [amounts, log, totalHuman, totalRaw]);
125130

126131
return {
127132
amounts,

app/components/Views/confirmations/hooks/pay/useTransactionRequiredFiat.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,16 @@ describe('useTransactionRequiredFiat', () => {
8585
amountFiat: 15,
8686
amountRaw: '3000',
8787
balanceFiat: 100,
88-
feeFiat: 0.375,
88+
feeFiat: 0.75,
8989
skipIfBalance: true,
90-
totalFiat: 15.375,
90+
totalFiat: 15.75,
9191
},
9292
]);
9393
});
9494

9595
it('returns total fiat value', () => {
9696
const { totalFiat } = runHook();
97-
expect(totalFiat).toBe(23.575);
97+
expect(totalFiat).toBe(23.95);
9898
});
9999

100100
it('supports amount overrides', () => {
@@ -119,9 +119,9 @@ describe('useTransactionRequiredFiat', () => {
119119
amountFiat: 15,
120120
amountRaw: '3000',
121121
balanceFiat: 100,
122-
feeFiat: 0.375,
122+
feeFiat: 0.75,
123123
skipIfBalance: true,
124-
totalFiat: 15.375,
124+
totalFiat: 15.75,
125125
},
126126
]);
127127
});

0 commit comments

Comments
 (0)