diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 0aba19f5ea9..f4be5a05d87 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fix use of native Polygon as payment token in Bridge strategy ([#7008](https://github.com/MetaMask/core/pull/7008)) + - Ignore required tokens with no quotes when calculating totals. + - Use correct feature flag key. + ## [2.0.0] ### Changed diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 42349af8a27..22fc2af4bdb 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -6,6 +6,7 @@ import { noop } from 'lodash'; import { updatePaymentToken } from './actions/update-payment-token'; import { CONTROLLER_NAME, TransactionPayStrategy } from './constants'; +import { QuoteRefresher } from './helpers/QuoteRefresher'; import type { TransactionData, TransactionPayControllerMessenger, @@ -13,7 +14,7 @@ import type { TransactionPayControllerState, UpdatePaymentTokenRequest, } from './types'; -import { queueRefreshQuotes, updateQuotes } from './utils/quotes'; +import { updateQuotes } from './utils/quotes'; import { updateSourceAmounts } from './utils/source-amounts'; import { pollTransactionChanges } from './utils/transaction'; @@ -61,7 +62,10 @@ export class TransactionPayController extends BaseController< this.#removeTransactionData.bind(this), ); - queueRefreshQuotes(messenger, this.#updateTransactionData.bind(this)); + new QuoteRefresher({ + messenger, + updateTransactionData: this.#updateTransactionData.bind(this), + }); } updatePaymentToken(request: UpdatePaymentTokenRequest) { @@ -119,7 +123,13 @@ export class TransactionPayController extends BaseController< transactionData: this.state.transactionData[transactionId], transactionId, updateTransactionData: this.#updateTransactionData.bind(this), - }).catch(noop); + }) + .finally(() => { + this.#updateTransactionData(transactionId, (data) => { + data.isLoading = false; + }); + }) + .catch(noop); } } diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts new file mode 100644 index 00000000000..36661a3a43d --- /dev/null +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts @@ -0,0 +1,179 @@ +import { createDeferredPromise } from '@metamask/utils'; + +import { QuoteRefresher } from './QuoteRefresher'; +import { flushPromises } from '../../../../tests/helpers'; +import { getMessengerMock } from '../tests/messenger-mock'; +import type { + TransactionData, + TransactionPayControllerMessenger, +} from '../types'; +import { refreshQuotes } from '../utils/quotes'; + +jest.mock('../utils/quotes'); + +jest.useFakeTimers(); + +describe('QuoteRefresher', () => { + const refreshQuotesMock = jest.mocked(refreshQuotes); + let messenger: TransactionPayControllerMessenger; + let publish: ReturnType['publish']; + + /** + * Helper to publish state changes with or without quotes. + * + * @param options - Options object. + * @param options.hasQuotes - Whether to include quotes in the state. + */ + function publishStateChange({ hasQuotes }: { hasQuotes: boolean }) { + const transactionData = { + '123': (hasQuotes ? { quotes: [{}] } : {}) as TransactionData, + }; + + publish( + 'TransactionPayController:stateChange', + { + transactionData, + }, + [], + ); + } + + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllTimers(); + + ({ messenger, publish } = getMessengerMock()); + + refreshQuotesMock.mockResolvedValue(undefined); + }); + + it('polls if quotes detected in state', async () => { + new QuoteRefresher({ + messenger, + updateTransactionData: jest.fn(), + }); + + publishStateChange({ hasQuotes: true }); + + jest.runAllTimers(); + await flushPromises(); + + expect(refreshQuotesMock).toHaveBeenCalledTimes(1); + }); + + it('does not poll if no quotes in state', async () => { + new QuoteRefresher({ + messenger, + updateTransactionData: jest.fn(), + }); + + publishStateChange({ hasQuotes: false }); + + jest.runAllTimers(); + await flushPromises(); + + expect(refreshQuotesMock).not.toHaveBeenCalled(); + }); + + it('polls again after interval', async () => { + new QuoteRefresher({ + messenger, + updateTransactionData: jest.fn(), + }); + + publishStateChange({ hasQuotes: true }); + + jest.runAllTimers(); + await flushPromises(); + + jest.runAllTimers(); + await flushPromises(); + + expect(refreshQuotesMock).toHaveBeenCalledTimes(2); + }); + + it('stops polling if quotes removed', async () => { + new QuoteRefresher({ + messenger, + updateTransactionData: jest.fn(), + }); + + publishStateChange({ hasQuotes: true }); + publishStateChange({ hasQuotes: false }); + + jest.runAllTimers(); + await flushPromises(); + + expect(refreshQuotesMock).toHaveBeenCalledTimes(0); + }); + + it('does not throw if refresh fails', async () => { + const updateTransactionData = jest.fn(); + + new QuoteRefresher({ + messenger, + updateTransactionData, + }); + + publishStateChange({ hasQuotes: true }); + + refreshQuotesMock.mockRejectedValueOnce(new Error('Test error')); + + jest.runAllTimers(); + await flushPromises(); + + jest.runAllTimers(); + await flushPromises(); + + expect(refreshQuotesMock).toHaveBeenCalledTimes(2); + }); + + it('does not update multiple times concurrently', async () => { + const updateTransactionData = jest.fn(); + + new QuoteRefresher({ + messenger, + updateTransactionData, + }); + + publishStateChange({ hasQuotes: true }); + + const promise = createDeferredPromise(); + refreshQuotesMock.mockReturnValue(promise.promise); + + jest.runAllTimers(); + await flushPromises(); + + publishStateChange({ hasQuotes: false }); + publishStateChange({ hasQuotes: true }); + + jest.runAllTimers(); + await flushPromises(); + + expect(refreshQuotesMock).toHaveBeenCalledTimes(1); + }); + + it('does not queue if stopped while polling', async () => { + const updateTransactionData = jest.fn(); + + new QuoteRefresher({ + messenger, + updateTransactionData, + }); + + publishStateChange({ hasQuotes: true }); + + const promise = createDeferredPromise(); + refreshQuotesMock.mockReturnValue(promise.promise); + + jest.runAllTimers(); + await flushPromises(); + + publishStateChange({ hasQuotes: false }); + + promise.resolve(); + await flushPromises(); + + expect(refreshQuotesMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts new file mode 100644 index 00000000000..2526bee511b --- /dev/null +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts @@ -0,0 +1,106 @@ +import { createModuleLogger } from '@metamask/utils'; +import { noop } from 'lodash'; + +import type { + TransactionPayControllerMessenger, + TransactionPayControllerState, +} from '..'; +import { projectLogger } from '../logger'; +import type { UpdateTransactionDataCallback } from '../types'; +import { refreshQuotes } from '../utils/quotes'; + +const CHECK_INTERVAL = 1000; // 1 Second + +const log = createModuleLogger(projectLogger, 'quote-refresh'); + +export class QuoteRefresher { + #isRunning: boolean; + + #isUpdating: boolean; + + readonly #messenger: TransactionPayControllerMessenger; + + #timeoutId: NodeJS.Timeout | undefined; + + readonly #updateTransactionData: UpdateTransactionDataCallback; + + constructor({ + messenger, + updateTransactionData, + }: { + messenger: TransactionPayControllerMessenger; + updateTransactionData: UpdateTransactionDataCallback; + }) { + this.#messenger = messenger; + this.#isRunning = false; + this.#isUpdating = false; + this.#updateTransactionData = updateTransactionData; + + messenger.subscribe( + 'TransactionPayController:stateChange', + this.#onStateChange.bind(this), + ); + } + + #start() { + this.#isRunning = true; + + log('Started'); + + if (this.#isUpdating) { + return; + } + + this.#queueNextInterval(); + } + + #stop() { + if (this.#timeoutId) { + clearTimeout(this.#timeoutId); + } + + this.#isRunning = false; + + log('Stopped'); + } + + async #onInterval() { + this.#isUpdating = true; + + try { + await refreshQuotes(this.#messenger, this.#updateTransactionData); + } catch (error) { + log('Error refreshing quotes', error); + } finally { + this.#isUpdating = false; + + this.#queueNextInterval(); + } + } + + #queueNextInterval() { + if (!this.#isRunning) { + return; + } + + if (this.#timeoutId) { + clearTimeout(this.#timeoutId); + } + + this.#timeoutId = setTimeout(() => { + this.#onInterval().catch(noop); + }, CHECK_INTERVAL); + } + + #onStateChange(state: TransactionPayControllerState) { + const hasQuotes = Object.values(state.transactionData).some((transaction) => + Boolean(transaction.quotes?.length), + ); + + if (hasQuotes && !this.#isRunning) { + this.#start(); + } else if (!hasQuotes && this.#isRunning) { + this.#stop(); + } + } +} diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts index ae9f3601979..8a8fe192859 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts @@ -127,7 +127,7 @@ describe('Bridge Quotes Utils', () => { getRemoteFeatureFlagControllerStateMock.mockImplementation(() => ({ cacheTimestamp: 0, remoteFeatureFlags: { - confirmation_pay: getFeatureFlagsMock(), + confirmations_pay: getFeatureFlagsMock(), }, })); diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts index 416e6c42af6..de1bdc2fe43 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -59,17 +59,22 @@ export async function getBridgeQuotes( const { requests, messenger, transaction } = request; - const finalRequests = getFinalRequests(requests, messenger); + try { + const finalRequests = getFinalRequests(requests, messenger); - const quotes = await Promise.all( - finalRequests.map((r, index) => - getSufficientSingleBridgeQuote(r, index, request), - ), - ); + const quotes = await Promise.all( + finalRequests.map((r, index) => + getSufficientSingleBridgeQuote(r, index, request), + ), + ); - return quotes.map((quote, index) => - normalizeQuote(quote, finalRequests[index], messenger, transaction), - ); + return quotes.map((quote, index) => + normalizeQuote(quote, finalRequests[index], messenger, transaction), + ); + } catch (error) { + log('Error fetching quotes', { error }); + throw new Error(`Failed to fetch bridge quotes: ${String(error)}`); + } } /** @@ -435,7 +440,7 @@ function getFinalRequests( */ function getFeatureFlags(messenger: TransactionPayControllerMessenger) { const featureFlags = messenger.call('RemoteFeatureFlagController:getState') - .remoteFeatureFlags.confirmation_pay as Record | undefined; + .remoteFeatureFlags.confirmations_pay as Record | undefined; return { attemptsMax: featureFlags?.attemptsMax ?? ATTEMPTS_MAX_DEFAULT, @@ -470,7 +475,7 @@ function normalizeQuote( const sourceFiatRate = getTokenFiatRate( messenger, - quote.quote.srcAsset.address as Hex, + request.sourceTokenAddress, toHex(quote.quote.srcChainId), ); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 366b320b95b..f64ea0cea11 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -174,7 +174,7 @@ describe('Relay Quotes Utils', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ cacheTimestamp: 0, remoteFeatureFlags: { - confirmation_pay: { + confirmations_pay: { relayQuoteUrl, }, }, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 52db148e556..d7f824acf51 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -37,21 +37,26 @@ export async function getRelayQuotes( log('Fetching quotes', requests); - const result = requests - // Ignore gas fee token requests - .filter((r) => r.targetAmountMinimum !== '0') - .map((r) => normalizeRequest(r)); - - const normalizedRequests = result.map((r) => r.request); - const isSkipTransaction = result.some((r) => r.isSkipTransaction); - - log('Normalized requests', { normalizedRequests, isSkipTransaction }); - - return await Promise.all( - normalizedRequests.map((r) => - getSingleQuote(r, isSkipTransaction, request), - ), - ); + try { + const result = requests + // Ignore gas fee token requests + .filter((r) => r.targetAmountMinimum !== '0') + .map((r) => normalizeRequest(r)); + + const normalizedRequests = result.map((r) => r.request); + const isSkipTransaction = result.some((r) => r.isSkipTransaction); + + log('Normalized requests', { normalizedRequests, isSkipTransaction }); + + return await Promise.all( + normalizedRequests.map((r) => + getSingleQuote(r, isSkipTransaction, request), + ), + ); + } catch (error) { + log('Error fetching quotes', { error }); + throw new Error(`Failed to fetch Relay quotes: ${String(error)}`); + } } /** @@ -160,7 +165,6 @@ function normalizeQuote( const { messenger, transaction } = fullRequest; const { details } = quote; const { currencyIn, currencyOut } = details; - const params = quote.steps[0].items[0].data; const { usdToFiatRate } = getFiatRates(messenger, request); @@ -174,12 +178,7 @@ function normalizeQuote( usdToFiatRate, ); - const sourceNetwork = calculateGasCost({ - ...params, - maxFeePerGas: undefined, - maxPriorityFeePerGas: undefined, - messenger, - }); + const sourceNetwork = calculateSourceNetworkCost(quote, messenger); const targetNetwork = quote.skipTransaction ? { @@ -289,9 +288,8 @@ function getFeatureFlags(messenger: TransactionPayControllerMessenger) { 'RemoteFeatureFlagController:getState', ); - const featureFlags = featureFlagState.remoteFeatureFlags?.confirmation_pay as - | Record - | undefined; + const featureFlags = featureFlagState.remoteFeatureFlags + ?.confirmations_pay as Record | undefined; const relayQuoteUrl = featureFlags?.relayQuoteUrl ?? RELAY_URL_QUOTE; @@ -299,3 +297,39 @@ function getFeatureFlags(messenger: TransactionPayControllerMessenger) { relayQuoteUrl, }; } + +/** + * Calculates source network cost from a Relay quote. + * + * @param quote - Relay quote. + * @param messenger - Controller messenger. + * @returns Total source network cost in USD and fiat. + */ +function calculateSourceNetworkCost( + quote: RelayQuote, + messenger: TransactionPayControllerMessenger, +) { + const allParams = quote.steps.flatMap((s) => s.items.map((i) => i.data)); + + const result = allParams.reduce( + (total, params) => { + const gasCost = calculateGasCost({ + ...params, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + messenger, + }); + + return { + usd: new BigNumber(total.usd).plus(gasCost.usd), + fiat: new BigNumber(total.fiat).plus(gasCost.fiat), + }; + }, + { usd: new BigNumber(0), fiat: new BigNumber(0) }, + ); + + return { + usd: result.usd.toString(10), + fiat: result.fiat.toString(10), + }; +} diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index ed159ae2288..fbbc419dd8a 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -4,11 +4,10 @@ import type { Hex, Json } from '@metamask/utils'; import { cloneDeep } from 'lodash'; import type { UpdateQuotesRequest } from './quotes'; -import { queueRefreshQuotes, updateQuotes } from './quotes'; +import { refreshQuotes, updateQuotes } from './quotes'; import { getStrategy, getStrategyByName } from './strategy'; import { calculateTotals } from './totals'; import { getTransaction, updateTransaction } from './transaction'; -import { flushPromises } from '../../../../tests/helpers'; import { getMessengerMock } from '../tests/messenger-mock'; import type { TransactionPaySourceAmount, @@ -283,7 +282,7 @@ describe('Quotes Utils', () => { }); }); - describe('queueRefreshQuotes', () => { + describe('refreshQuotes', () => { it('updates quotes after refresh interval', async () => { getControllerStateMock.mockReturnValue({ transactionData: { @@ -296,10 +295,7 @@ describe('Quotes Utils', () => { }, }); - queueRefreshQuotes(messenger, updateTransactionDataMock); - - jest.runAllTimers(); - await flushPromises(); + await refreshQuotes(messenger, updateTransactionDataMock); expect(updateTransactionDataMock).toHaveBeenCalledTimes(2); @@ -323,10 +319,7 @@ describe('Quotes Utils', () => { }, }); - queueRefreshQuotes(messenger, updateTransactionDataMock); - - jest.runAllTimers(); - await flushPromises(); + await refreshQuotes(messenger, updateTransactionDataMock); expect(updateTransactionDataMock).toHaveBeenCalledTimes(2); @@ -351,10 +344,7 @@ describe('Quotes Utils', () => { }, }); - queueRefreshQuotes(messenger, updateTransactionDataMock); - - jest.advanceTimersByTime(1000); - await flushPromises(); + await refreshQuotes(messenger, updateTransactionDataMock); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); }); @@ -371,21 +361,7 @@ describe('Quotes Utils', () => { }, }); - queueRefreshQuotes(messenger, updateTransactionDataMock); - - jest.advanceTimersByTime(1000); - await flushPromises(); - - expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); - }); - - it('does not throw if refresh fails', async () => { - getControllerStateMock.mockReturnValue(undefined as never); - - queueRefreshQuotes(messenger, updateTransactionDataMock); - - jest.advanceTimersByTime(1000); - await flushPromises(); + await refreshQuotes(messenger, updateTransactionDataMock); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); }); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 02c1f0ab304..06fa5546635 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -12,12 +12,12 @@ import type { TransactionData, TransactionPayControllerMessenger, TransactionPayQuote, + TransactionPayRequiredToken, TransactionPayTotals, TransactionPaymentToken, UpdateTransactionDataCallback, } from '../types'; -const QUOTES_CHECK_INTERVAL = 1 * 1000; // 1 Second const DEFAULT_REFRESH_INTERVAL = 30 * 1000; // 30 Seconds const log = createModuleLogger(projectLogger, 'quotes'); @@ -52,22 +52,22 @@ export async function updateQuotes(request: UpdateQuotesRequest) { return; } - const requests: QuoteRequest[] = (sourceAmounts ?? []).map( - (sourceAmount, i) => { - const token = tokens[i]; - - return { - from: transaction.txParams.from as Hex, - sourceBalanceRaw: paymentToken.balanceRaw, - sourceTokenAmount: sourceAmount.sourceAmountRaw, - sourceChainId: paymentToken.chainId, - sourceTokenAddress: paymentToken.address, - targetAmountMinimum: token.allowUnderMinimum ? '0' : token.amountRaw, - targetChainId: token.chainId, - targetTokenAddress: token.address, - }; - }, - ); + const requests: QuoteRequest[] = (sourceAmounts ?? []).map((sourceAmount) => { + const token = tokens.find( + (t) => t.address === sourceAmount.targetTokenAddress, + ) as TransactionPayRequiredToken; + + return { + from: transaction.txParams.from as Hex, + sourceBalanceRaw: paymentToken.balanceRaw, + sourceTokenAmount: sourceAmount.sourceAmountRaw, + sourceChainId: paymentToken.chainId, + sourceTokenAddress: paymentToken.address, + targetAmountMinimum: token.allowUnderMinimum ? '0' : token.amountRaw, + targetChainId: token.chainId, + targetTokenAddress: token.address, + }; + }); if (!requests?.length) { log('No quote requests', { transactionId }); @@ -117,29 +117,9 @@ export async function updateQuotes(request: UpdateQuotesRequest) { data.quotes = quotes as never; data.quotesLastUpdated = Date.now(); data.totals = totals; - data.isLoading = false; }); } -/** - * Poll quotes at regular intervals. - * - * @param messenger - Messenger instance. - * @param updateTransactionData - Callback to update transaction data. - */ -export function queueRefreshQuotes( - messenger: TransactionPayControllerMessenger, - updateTransactionData: UpdateTransactionDataCallback, -) { - setTimeout(() => { - refreshQuotes(messenger, updateTransactionData) - .finally(() => queueRefreshQuotes(messenger, updateTransactionData)) - .catch((error) => { - log('Error polling quotes', { messenger, error }); - }); - }, QUOTES_CHECK_INTERVAL); -} - /** * Sync batch transactions to the transaction meta. * @@ -190,7 +170,7 @@ function syncTransaction({ * @param messenger - Messenger instance. * @param updateTransactionData - Callback to update transaction data. */ -async function refreshQuotes( +export async function refreshQuotes( messenger: TransactionPayControllerMessenger, updateTransactionData: UpdateTransactionDataCallback, ) { diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index 1513e92cf5f..50adc9be304 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -94,6 +94,25 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('51.08'); }); + it('returns total excluding token amount not in quote', () => { + const result = calculateTotals( + [QUOTE_1_MOCK, QUOTE_2_MOCK], + [ + TOKEN_1_MOCK, + { + ...TOKEN_2_MOCK, + balanceRaw: '10', + amountRaw: '9', + skipIfBalance: true, + }, + ], + MESSENGER_MOCK, + ); + + expect(result.total.fiat).toBe('39.97'); + expect(result.total.usd).toBe('46.64'); + }); + it('returns provider fees', () => { const result = calculateTotals( [QUOTE_1_MOCK, QUOTE_2_MOCK], diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index 6cb87f4c317..103bb49c128 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -50,8 +50,13 @@ export function calculateTotals( (quote) => quote.fees.targetNetwork.usd, ); - const amountFiat = sumProperty(tokens, (token) => token.amountFiat); - const amountUsd = sumProperty(tokens, (token) => token.amountUsd); + const quoteTokens = tokens.filter( + (t) => + !t.skipIfBalance || new BigNumber(t.balanceRaw).isLessThan(t.amountRaw), + ); + + const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); + const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); const totalFiat = new BigNumber(providerFeeFiat) .plus(sourceNetworkFeeFiat) diff --git a/packages/transaction-pay-controller/src/utils/transaction.test.ts b/packages/transaction-pay-controller/src/utils/transaction.test.ts index 0943c9175ab..f13f98ef39c 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.test.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.test.ts @@ -157,6 +157,32 @@ describe('Transaction Utils', () => { ); }, ); + + it('removes state if transaction is deleted', () => { + const removeTransactionDataMock = jest.fn(); + + pollTransactionChanges(messenger, noop, removeTransactionDataMock); + + publish( + 'TransactionController:stateChange', + { + transactions: [TRANSACTION_META_MOCK], + } as TransactionControllerState, + [], + ); + + publish( + 'TransactionController:stateChange', + { + transactions: [] as TransactionMeta[], + } as TransactionControllerState, + [], + ); + + expect(removeTransactionDataMock).toHaveBeenCalledWith( + TRANSACTION_ID_MOCK, + ); + }); }); describe('updateTransaction', () => { diff --git a/packages/transaction-pay-controller/src/utils/transaction.ts b/packages/transaction-pay-controller/src/utils/transaction.ts index b01ba2a2543..f7ef5f8aa25 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.ts @@ -85,7 +85,11 @@ export function pollTransactionChanges( ); }); - finalizedTransactions.forEach((tx) => + const deletedTransactions = (previousTransactions ?? []).filter( + (prevTx) => !transactions.find((tx) => tx.id === prevTx.id), + ); + + [...finalizedTransactions, ...deletedTransactions].forEach((tx) => onTransactionFinalized(tx, removeTransactionData), );