diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 26885faddbf..116408033ac 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING:** Include transactions in Relay quotes via EIP-7702 and delegation ([#7122](https://github.com/MetaMask/core/pull/7122)) + - Requires new `getDelegationTransaction` constructor option. + ### Fixed - Read Relay provider fees directly from response ([#7098](https://github.com/MetaMask/core/pull/7098)) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index aff56cd4c6a..59095e59bc4 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -37,6 +37,7 @@ describe('TransactionPayController', () => { */ function createController() { return new TransactionPayController({ + getDelegationTransaction: jest.fn(), messenger, }); } @@ -85,6 +86,7 @@ describe('TransactionPayController', () => { it('returns callback value if provided', async () => { new TransactionPayController({ + getDelegationTransaction: jest.fn(), getStrategy: async () => TransactionPayStrategy.Test, messenger, }); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 883e223670c..eac0d916eb2 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -8,6 +8,7 @@ import { updatePaymentToken } from './actions/update-payment-token'; import { CONTROLLER_NAME, TransactionPayStrategy } from './constants'; import { QuoteRefresher } from './helpers/QuoteRefresher'; import type { + GetDelegationTransactionCallback, TransactionData, TransactionPayControllerMessenger, TransactionPayControllerOptions, @@ -36,11 +37,14 @@ export class TransactionPayController extends BaseController< TransactionPayControllerState, TransactionPayControllerMessenger > { + readonly #getDelegationTransaction: GetDelegationTransactionCallback; + readonly #getStrategy?: ( transaction: TransactionMeta, ) => Promise; constructor({ + getDelegationTransaction, getStrategy, messenger, state, @@ -52,6 +56,7 @@ export class TransactionPayController extends BaseController< state: { ...getDefaultState(), ...state }, }); + this.#getDelegationTransaction = getDelegationTransaction; this.#getStrategy = getStrategy; this.#registerActionHandlers(); @@ -127,6 +132,11 @@ export class TransactionPayController extends BaseController< } #registerActionHandlers() { + this.messenger.registerActionHandler( + 'TransactionPayController:getDelegationTransaction', + this.#getDelegationTransaction.bind(this), + ); + this.messenger.registerActionHandler( 'TransactionPayController:getStrategy', this.#getStrategy ?? (async () => TransactionPayStrategy.Relay), diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 113cb55aceb..04b8fe4d86b 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -1,6 +1,7 @@ export type { TransactionPayControllerActions, TransactionPayControllerEvents, + TransactionPayControllerGetDelegationTransactionAction, TransactionPayControllerGetStateAction, TransactionPayControllerGetStrategyAction, TransactionPayControllerMessenger, diff --git a/packages/transaction-pay-controller/src/strategy/relay/constants.ts b/packages/transaction-pay-controller/src/strategy/relay/constants.ts index 663ab690bc1..9d41f1836e0 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/constants.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/constants.ts @@ -2,6 +2,8 @@ export const ARBITRUM_USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; export const CHAIN_ID_ARBITRUM = '0xa4b1'; export const CHAIN_ID_POLYGON = '0x89'; +export const CHAIN_ID_HYPERCORE = '0x539'; export const RELAY_URL_BASE = 'https://api.relay.link'; export const RELAY_URL_QUOTE = `${RELAY_URL_BASE}/quote`; export const RELAY_FALLBACK_GAS_LIMIT = 900000; +export const RELAY_POLLING_INTERVAL = 1000; // 1 Second 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 fba08ec8c4d..49a5e654658 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 @@ -1,5 +1,6 @@ import { successfulFetch } from '@metamask/controller-utils'; import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; import { @@ -12,7 +13,10 @@ import { getRelayQuotes } from './relay-quotes'; import type { RelayQuote } from './types'; import { NATIVE_TOKEN_ADDRESS } from '../../constants'; import { getMessengerMock } from '../../tests/messenger-mock'; -import type { QuoteRequest } from '../../types'; +import type { + GetDelegationTransactionCallback, + QuoteRequest, +} from '../../types'; import { calculateGasCost, calculateTransactionGasCost } from '../../utils/gas'; import { getNativeToken, getTokenFiatRate } from '../../utils/token'; @@ -25,14 +29,14 @@ jest.mock('@metamask/controller-utils', () => ({ })); const QUOTE_REQUEST_MOCK: QuoteRequest = { - from: '0x123', + from: '0x1234567890123456789012345678901234567891', sourceBalanceRaw: '10000000000000000000', sourceChainId: '0x1', sourceTokenAddress: '0xabc', sourceTokenAmount: '1000000000000000000', targetAmountMinimum: '123', targetChainId: '0x2', - targetTokenAddress: '0xdef', + targetTokenAddress: '0x1234567890123456789012345678901234567890', }; const QUOTE_MOCK = { @@ -62,12 +66,12 @@ const QUOTE_MOCK = { }, data: { chainId: 1, - data: '0x123', - from: '0x1', + data: '0x123' as Hex, + from: '0x1' as Hex, gas: '21000', maxFeePerGas: '1000000000', maxPriorityFeePerGas: '2000000000', - to: '0x2', + to: '0x2' as Hex, value: '300000', }, status: 'complete', @@ -78,7 +82,20 @@ const QUOTE_MOCK = { ], } as RelayQuote; -const TRANSACTION_META_MOCK = {} as TransactionMeta; +const TRANSACTION_META_MOCK = { txParams: {} } as TransactionMeta; + +const DELEGATION_RESULT_MOCK = { + authorizationList: [ + { + chainId: '0x1' as Hex, + nonce: '0x2' as Hex, + yParity: '0x1' as Hex, + }, + ], + data: '0x111' as Hex, + to: '0x222' as Hex, + value: '0x333' as Hex, +} as Awaited>; describe('Relay Quotes Utils', () => { const successfulFetchMock = jest.mocked(successfulFetch); @@ -90,8 +107,11 @@ describe('Relay Quotes Utils', () => { calculateTransactionGasCost, ); - const { messenger, getRemoteFeatureFlagControllerStateMock } = - getMessengerMock(); + const { + messenger, + getDelegationTransactionMock, + getRemoteFeatureFlagControllerStateMock, + } = getMessengerMock(); beforeEach(() => { jest.resetAllMocks(); @@ -115,6 +135,8 @@ describe('Relay Quotes Utils', () => { cacheTimestamp: 0, remoteFeatureFlags: {}, }); + + getDelegationTransactionMock.mockResolvedValue(DELEGATION_RESULT_MOCK); }); describe('getRelayQuotes', () => { @@ -164,6 +186,52 @@ describe('Relay Quotes Utils', () => { ); }); + it('includes transactions in request', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body).toStrictEqual( + expect.objectContaining({ + authorizationList: [ + { + chainId: 1, + nonce: 2, + yParity: 1, + }, + ], + tradeType: 'EXACT_OUTPUT', + txs: [ + { + to: QUOTE_REQUEST_MOCK.targetTokenAddress, + data: '0xa9059cbb0000000000000000000000001234567890123456789012345678901234567891000000000000000000000000000000000000000000000000000000000000007b', + value: '0x0', + }, + { + to: DELEGATION_RESULT_MOCK.to, + data: DELEGATION_RESULT_MOCK.data, + value: DELEGATION_RESULT_MOCK.value, + }, + ], + }), + ); + }); + it('sends request to url from feature flag', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, @@ -325,8 +393,8 @@ describe('Relay Quotes Utils', () => { }); expect(result[0].fees.targetNetwork).toStrictEqual({ - usd: '1.23', - fiat: '2.34', + usd: '0', + fiat: '0', }); }); 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 58eb8e75f75..0d723ec5795 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1,16 +1,20 @@ -import { successfulFetch, toHex } from '@metamask/controller-utils'; +import { Interface } from '@ethersproject/abi'; +import { successfulFetch } from '@metamask/controller-utils'; +import type { Json } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM, + CHAIN_ID_HYPERCORE, CHAIN_ID_POLYGON, RELAY_FALLBACK_GAS_LIMIT, RELAY_URL_QUOTE, } from './constants'; import type { RelayQuote } from './types'; import { TransactionPayStrategy } from '../..'; +import type { TransactionMeta } from '../../../../transaction-controller/src'; import { NATIVE_TOKEN_ADDRESS } from '../../constants'; import { projectLogger } from '../../logger'; import type { @@ -20,7 +24,7 @@ import type { TransactionPayControllerMessenger, TransactionPayQuote, } from '../../types'; -import { calculateGasCost, calculateTransactionGasCost } from '../../utils/gas'; +import { calculateGasCost } from '../../utils/gas'; import { getNativeToken, getTokenFiatRate } from '../../utils/token'; const log = createModuleLogger(projectLogger, 'relay-strategy'); @@ -39,20 +43,15 @@ export async function getRelayQuotes( log('Fetching quotes', requests); try { - const result = requests + const normalizedRequests = 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 }); + log('Normalized requests', normalizedRequests); return await Promise.all( - normalizedRequests.map((r) => - getSingleQuote(r, isSkipTransaction, request), - ), + normalizedRequests.map((r) => getSingleQuote(r, request)), ); } catch (error) { log('Error fetching quotes', { error }); @@ -64,16 +63,14 @@ export async function getRelayQuotes( * Fetches a single Relay quote. * * @param request - Quote request. - * @param isSkipTransaction - Whether to skip the transaction. * @param fullRequest - Full quotes request. * @returns Single quote. */ async function getSingleQuote( request: QuoteRequest, - isSkipTransaction: boolean, fullRequest: PayStrategyGetQuotesRequest, ): Promise> { - const { messenger } = fullRequest; + const { messenger, transaction } = fullRequest; try { const body = { @@ -87,8 +84,12 @@ async function getSingleQuote( user: request.from, }; + await processTransactions(transaction, request, body, messenger); + const url = getFeatureFlags(messenger).relayQuoteUrl; + log('Request body', { body, url }); + const response = await successfulFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -96,9 +97,8 @@ async function getSingleQuote( }); const quote = (await response.json()) as RelayQuote; - quote.skipTransaction = isSkipTransaction; - log('Fetched relay quote', { quote, url }); + log('Fetched relay quote', quote); return normalizeQuote(quote, request, fullRequest); } catch (e) { @@ -107,6 +107,71 @@ async function getSingleQuote( } } +/** + * Add tranasction data to request body if needed. + * + * @param transaction - Transaction metadata. + * @param request - Quote request. + * @param requestBody - Request body to populate. + * @param messenger - Controller messenger. + */ +async function processTransactions( + transaction: TransactionMeta, + request: QuoteRequest, + requestBody: Record, + messenger: TransactionPayControllerMessenger, +) { + const { data, value } = transaction.txParams; + + /* istanbul ignore next */ + const hasNoParams = (!data || data === '0x') && (!value || value === '0x0'); + + const skipDelegation = + hasNoParams || request.targetChainId === CHAIN_ID_HYPERCORE; + + if (skipDelegation) { + log('Skipping delegation as no transaction data'); + return; + } + + const delegation = await messenger.call( + 'TransactionPayController:getDelegationTransaction', + { transaction }, + ); + + const normalizedAuthorizationList = delegation.authorizationList?.map( + (a) => ({ + ...a, + chainId: Number(a.chainId), + nonce: Number(a.nonce), + yParity: Number(a.yParity), + }), + ); + + const tokenTransferData = new Interface([ + 'function transfer(address to, uint256 amount)', + ]).encodeFunctionData('transfer', [ + request.from, + request.targetAmountMinimum, + ]); + + requestBody.authorizationList = normalizedAuthorizationList; + requestBody.tradeType = 'EXACT_OUTPUT'; + + requestBody.txs = [ + { + to: request.targetTokenAddress, + data: tokenTransferData, + value: '0x0', + }, + { + to: delegation.to, + data: delegation.data, + value: delegation.value, + }, + ]; +} + /** * Normalizes requests for Relay. * @@ -128,7 +193,9 @@ function normalizeRequest(request: QuoteRequest) { sourceTokenAddress: isPolygonNativeSource ? NATIVE_TOKEN_ADDRESS : request.sourceTokenAddress, - targetChainId: isHyperliquidDeposit ? toHex(1337) : request.targetChainId, + targetChainId: isHyperliquidDeposit + ? CHAIN_ID_HYPERCORE + : request.targetChainId, targetTokenAddress: isHyperliquidDeposit ? '0x00000000000000000000000000000000' : request.targetTokenAddress, @@ -144,10 +211,7 @@ function normalizeRequest(request: QuoteRequest) { }); } - return { - request: requestOutput, - isSkipTransaction: isHyperliquidDeposit, - }; + return requestOutput; } /** @@ -163,7 +227,7 @@ function normalizeQuote( request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, ): TransactionPayQuote { - const { messenger, transaction } = fullRequest; + const { messenger } = fullRequest; const { details, fees } = quote; const { usdToFiatRate } = getFiatRates(messenger, request); @@ -180,12 +244,10 @@ function normalizeQuote( const sourceNetwork = calculateSourceNetworkCost(quote, messenger); - const targetNetwork = quote.skipTransaction - ? { - usd: '0', - fiat: '0', - } - : calculateTransactionGasCost(transaction, messenger); + const targetNetwork = { + usd: '0', + fiat: '0', + }; return { dust, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 0c39e89f2ca..a97d5792621 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -41,6 +41,18 @@ const TRANSACTION_META_MOCK = { } as TransactionMeta; const ORIGINAL_QUOTE_MOCK = { + details: { + currencyIn: { + currency: { + chainId: 1, + }, + }, + currencyOut: { + currency: { + chainId: 2, + }, + }, + }, steps: [ { kind: 'transaction', @@ -67,6 +79,11 @@ const ORIGINAL_QUOTE_MOCK = { ], } as RelayQuote; +const STATUS_RESPONSE_MOCK = { + status: 'success', + txHashes: [TRANSACTION_HASH_MOCK], +}; + const REQUEST_MOCK: PayStrategyExecuteRequest = { quotes: [ { @@ -120,7 +137,7 @@ describe('Relay Submit Utils', () => { ); successfulFetchMock.mockResolvedValue({ - json: async () => ({ status: 'success' }), + json: async () => STATUS_RESPONSE_MOCK, } as Response); request = cloneDeep(REQUEST_MOCK); @@ -228,6 +245,14 @@ describe('Relay Submit Utils', () => { ); }); + it('does not wait for relay status if same chain', async () => { + request.quotes[0].original.details.currencyOut.currency.chainId = 1; + + await submitRelayQuotes(request); + + expect(successfulFetchMock).toHaveBeenCalledTimes(0); + }); + it('throws if transaction fails to confirm', async () => { waitForTransactionConfirmedMock.mockRejectedValue( new Error('Transaction failed'), @@ -251,9 +276,7 @@ describe('Relay Submit Utils', () => { }, ); - it('updates transaction if skipTransaction is true', async () => { - request.quotes[0].original.skipTransaction = true; - + it('updates transaction', async () => { await submitRelayQuotes(request); expect(updateTransactionMock).toHaveBeenCalledWith( @@ -277,23 +300,28 @@ describe('Relay Submit Utils', () => { }); }); - it('returns hash if skipTransaction is true', async () => { - request.quotes[0].original.skipTransaction = true; + it('returns target hash', async () => { const result = await submitRelayQuotes(request); expect(result.transactionHash).toBe(TRANSACTION_HASH_MOCK); }); - it('does not return hash if skipTransaction is false', async () => { + it('returns fallback hash if none included', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => ({ + ...STATUS_RESPONSE_MOCK, + txHashes: [], + }), + } as Response); + const result = await submitRelayQuotes(request); - expect(result.transactionHash).toBeUndefined(); + expect(result.transactionHash).toBe('0x0'); }); it('adds required transaction IDs', async () => { await submitRelayQuotes(request); - const updateFn = updateTransactionMock.mock.calls[0][1]; - const txDraft = {} as TransactionMeta; - updateFn(txDraft); + const txDraft = { txParams: {} } as TransactionMeta; + updateTransactionMock.mock.calls.map((call) => call[1](txDraft)); expect(txDraft.requiredTransactionIds).toStrictEqual([ TRANSACTION_META_MOCK.id, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 555d23f5e40..45030386d97 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -11,7 +11,11 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; -import { RELAY_FALLBACK_GAS_LIMIT, RELAY_URL_BASE } from './constants'; +import { + RELAY_FALLBACK_GAS_LIMIT, + RELAY_POLLING_INTERVAL, + RELAY_URL_BASE, +} from './constants'; import type { RelayQuote, RelayStatus } from './types'; import { projectLogger } from '../../logger'; import type { @@ -25,6 +29,8 @@ import { waitForTransactionConfirmed, } from '../../utils/transaction'; +const FALLBACK_HASH = '0x0' as Hex; + const log = createModuleLogger(projectLogger, 'relay-strategy'); /** @@ -50,14 +56,7 @@ export async function submitRelayQuotes( )); } - const isSkipTransaction = quotes.some((q) => q.original.skipTransaction); - - if (isSkipTransaction) { - log('Skipping original transaction', transactionHash); - return { transactionHash }; - } - - return { transactionHash: undefined }; + return { transactionHash }; } /** @@ -85,47 +84,35 @@ async function executeSingleQuote( const chainId = toHex(transactionParams.chainId); const from = transactionParams.from as Hex; - if (quote.skipTransaction) { - updateTransaction( - { - transactionId: transaction.id, - messenger, - note: 'Remove nonce from skipped transaction', - }, - (tx) => { - tx.txParams.nonce = undefined; - }, - ); - } - - const transactionHash = await submitTransactions( - quote, - chainId, - from, - transaction.id, - messenger, + updateTransaction( + { + transactionId: transaction.id, + messenger, + note: 'Remove nonce from skipped transaction', + }, + (tx) => { + tx.txParams.nonce = undefined; + }, ); - await waitForRelayCompletion(quote); + await submitTransactions(quote, chainId, from, transaction.id, messenger); - log('Relay request completed'); + const targetHash = await waitForRelayCompletion(quote); - if (quote.skipTransaction) { - log('Updating intent complete flag on transaction', transaction.id); + log('Relay request completed', targetHash); - updateTransaction( - { - transactionId: transaction.id, - messenger, - note: 'Intent complete after Relay completion', - }, - (tx) => { - tx.isIntentComplete = true; - }, - ); - } + updateTransaction( + { + transactionId: transaction.id, + messenger, + note: 'Intent complete after Relay completion', + }, + (tx) => { + tx.isIntentComplete = true; + }, + ); - return { transactionHash }; + return { transactionHash: targetHash }; } /** @@ -134,7 +121,15 @@ async function executeSingleQuote( * @param quote - Relay quote associated with the request. * @returns A promise that resolves when the Relay request is complete. */ -async function waitForRelayCompletion(quote: RelayQuote) { +async function waitForRelayCompletion(quote: RelayQuote): Promise { + if ( + quote.details.currencyIn.currency.chainId === + quote.details.currencyOut.currency.chainId + ) { + log('Skipping polling as same chain'); + return FALLBACK_HASH; + } + const { endpoint, method } = quote.steps .slice(-1)[0] .items.slice(-1)[0].check; @@ -148,14 +143,15 @@ async function waitForRelayCompletion(quote: RelayQuote) { log('Polled status', status.status, status); if (status.status === 'success') { - return; + const targetHash = status.txHashes?.slice(-1)[0] as Hex; + return targetHash ?? FALLBACK_HASH; } - if (['failure', 'refund'].includes(status.status)) { + if (['failure', 'refund', 'fallback'].includes(status.status)) { throw new Error(`Relay request failed with status: ${status.status}`); } - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, RELAY_POLLING_INTERVAL)); } } diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index c0f581f1dae..5d35ccc4e32 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -2,10 +2,19 @@ import type { Hex } from '@metamask/utils'; export type RelayQuote = { details: { + currencyIn: { + amountFormatted: string; + amountUsd: string; + currency: { + chainId: number; + decimals: number; + }; + }; currencyOut: { amountFormatted: string; amountUsd: string; currency: { + chainId: number; decimals: number; }; minimumAmount: string; @@ -37,7 +46,6 @@ export type RelayQuote = { }[]; kind: 'transaction'; }[]; - skipTransaction?: boolean; }; export type RelayStatus = { diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index df69597addd..c3ecd2b7a17 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -21,7 +21,10 @@ import type { TransactionControllerUpdateTransactionAction } from '@metamask/tra import type { TransactionPayControllerMessenger } from '..'; import type { BridgeStatusControllerSubmitTxAction } from '../../../bridge-status-controller/src/types'; -import type { TransactionPayControllerGetStrategyAction } from '../types'; +import type { + TransactionPayControllerGetDelegationTransactionAction, + TransactionPayControllerGetStrategyAction, +} from '../types'; import { type TransactionPayControllerGetStateAction } from '../types'; type AllActions = MessengerActions; @@ -104,6 +107,10 @@ export function getMessengerMock({ NetworkControllerGetNetworkClientByIdAction['handler'] > = jest.fn(); + const getDelegationTransactionMock: jest.MockedFn< + TransactionPayControllerGetDelegationTransactionAction['handler'] + > = jest.fn(); + const messenger: RootMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE, }); @@ -198,6 +205,11 @@ export function getMessengerMock({ 'NetworkController:getNetworkClientById', getNetworkClientByIdMock, ); + + messenger.registerActionHandler( + 'TransactionPayController:getDelegationTransaction', + getDelegationTransactionMock, + ); } const publish = messenger.publish.bind(messenger); @@ -211,6 +223,7 @@ export function getMessengerMock({ getBridgeStatusControllerStateMock, getControllerStateMock, getCurrencyRateControllerStateMock, + getDelegationTransactionMock, getGasFeeControllerStateMock, getNetworkClientByIdMock, getRemoteFeatureFlagControllerStateMock, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 37189085a16..bb7571dac92 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -17,6 +17,7 @@ import type { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metam import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/network-controller'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; import type { + AuthorizationList, TransactionControllerAddTransactionBatchAction, TransactionControllerUnapprovedTransactionAddedEvent, } from '@metamask/transaction-controller'; @@ -59,6 +60,11 @@ export type TransactionPayControllerGetStateAction = ControllerGetStateAction< TransactionPayControllerState >; +export type TransactionPayControllerGetDelegationTransactionAction = { + type: `${typeof CONTROLLER_NAME}:getDelegationTransaction`; + handler: GetDelegationTransactionCallback; +}; + /** Action to get the pay strategy type used for a transaction. */ export type TransactionPayControllerGetStrategyAction = { type: `${typeof CONTROLLER_NAME}:getStrategy`; @@ -78,6 +84,7 @@ export type TransactionPayControllerStateChangeEvent = >; export type TransactionPayControllerActions = + | TransactionPayControllerGetDelegationTransactionAction | TransactionPayControllerGetStateAction | TransactionPayControllerGetStrategyAction | TransactionPayControllerUpdatePaymentTokenAction; @@ -93,6 +100,9 @@ export type TransactionPayControllerMessenger = Messenger< /** Options for the TransactionPayController. */ export type TransactionPayControllerOptions = { + /** Callback to convert a transaction into a redeem delegation. */ + getDelegationTransaction: GetDelegationTransactionCallback; + /** Callback to select the PayStrategy for a transaction. */ getStrategy?: ( transaction: TransactionMeta, @@ -399,3 +409,15 @@ export type UpdatePaymentTokenRequest = { /** Chain ID of the new payment token. */ chainId: Hex; }; + +/** Callback to convert a transaction to a redeem delegation. */ +export type GetDelegationTransactionCallback = ({ + transaction, +}: { + transaction: TransactionMeta; +}) => Promise<{ + authorizationList?: AuthorizationList; + data: Hex; + to: Hex; + value: Hex; +}>; diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index 6102949df1e..9104e7e1706 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -63,11 +63,7 @@ export function calculateTotals({ ? sumProperty(quotes, (quote) => quote.fees.targetNetwork.usd) : transactionNetworkFee.usd; - const quoteTokens = tokens.filter( - (t) => - !t.skipIfBalance || new BigNumber(t.balanceRaw).isLessThan(t.amountRaw), - ); - + const quoteTokens = tokens.filter((t) => !t.skipIfBalance); const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd);