Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions oxlint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6746,17 +6746,23 @@
}
},
"packages/transaction-pay-controller/src/strategy/server/server-quotes.test.ts": {
"no-unused-vars": {
"count": 1
},
"typescript/no-unnecessary-type-assertion": {
"count": 1
},
"typescript/no-unsafe-assignment": {
"count": 39
"count": 47
},
"typescript/no-unsafe-call": {
"count": 8
"count": 9
},
"typescript/no-unsafe-member-access": {
"count": 8
"count": 9
},
"typescript/no-unsafe-return": {
"count": 1
"count": 2
}
},
"packages/transaction-pay-controller/src/strategy/server/server-quotes.ts": {
Expand All @@ -6768,14 +6774,17 @@
}
},
"packages/transaction-pay-controller/src/strategy/server/server-submit.test.ts": {
"typescript/no-unnecessary-type-assertion": {
"count": 4
},
"typescript/no-unsafe-assignment": {
"count": 42
"count": 44
},
"typescript/no-unsafe-call": {
"count": 20
"count": 21
},
"typescript/no-unsafe-member-access": {
"count": 23
"count": 29
},
"typescript/no-unsafe-return": {
"count": 3
Expand All @@ -6795,18 +6804,24 @@
"count": 108
}
},
"packages/transaction-pay-controller/src/utils/gas-station.test.ts": {
"typescript/no-unsafe-argument": {
"packages/transaction-pay-controller/src/utils/gas-payment.test.ts": {
"typescript/explicit-function-return-type": {
"count": 1
},
"typescript/no-unnecessary-type-assertion": {
"count": 2
},
"typescript/no-unsafe-assignment": {
"count": 6
"count": 7
},
"typescript/no-unsafe-call": {
"count": 5
"count": 16
},
"typescript/no-unsafe-member-access": {
"count": 5
"count": 16
},
"typescript/no-unsafe-return": {
"count": 3
}
},
"packages/transaction-pay-controller/src/utils/gas.test.ts": {
Expand Down
5 changes: 5 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Bump `@metamask/transaction-controller` from `^72.0.1` to `^72.1.0` ([#10652](https://github.com/MetaMask/core/pull/10652))

### Fixed

- Always set `excludeNativeTokenForFee` when origin gas is priced in the source token, so the fee shown in the quote is the fee the user is charged ([#10404](https://github.com/MetaMask/core/pull/10404))
- Previously only the across strategy opted in, so for the relay and server strategies the `TransactionController` re-checked the native balance at publish time and silently dropped the gas fee token, billing the user in native token instead.

## [30.0.1]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,11 @@ import {
getPayStrategiesConfig,
getSlippage,
} from '../../utils/feature-flags.js';
import {
getGasStationCostInSourceTokenRaw,
getGasStationEligibility,
} from '../../utils/gas-station.js';
import { resolveGasStationCost } from '../../utils/gas-payment.js';
import { calculateGasCost } from '../../utils/gas.js';
import { estimateQuoteGasLimits } from '../../utils/quote-gas.js';
import type { QuoteGasTransaction } from '../../utils/quote-gas.js';
import {
getNativeToken,
getTokenBalance,
getTokenFiatRate,
} from '../../utils/token.js';
import { getTokenFiatRate } from '../../utils/token.js';
import type { AcrossDestination } from './across-actions.js';
import { getAcrossDestination } from './across-actions.js';
import { hasUnsupportedTransactionAuthorizationList } from './authorization-list.js';
Expand Down Expand Up @@ -692,50 +685,23 @@ async function calculateSourceNetworkCost(
? combinePostQuoteGas(result, transaction, swapTx, messenger)
: result;

const nativeBalance = getTokenBalance(
messenger,
from,
sourceChainId,
getNativeToken(sourceChainId),
);
const hasNativeBalance = new BigNumber(nativeBalance).isGreaterThanOrEqualTo(
finalResult.sourceNetwork.max.raw,
);

if (isPredictWithdraw && !accountSupports7702) {
return finalResult;
}

if (hasNativeBalance && !isPredictWithdraw) {
return finalResult;
}

const gasStationEligibility = getGasStationEligibility(
messenger,
sourceChainId,
);

if (gasStationEligibility.isDisabledChain) {
log('Skipping Across gas station as disabled chain', { sourceChainId });
return finalResult;
}

if (!gasStationEligibility.chainSupportsGasStation) {
log('Skipping Across gas station as chain does not support EIP-7702', {
sourceChainId,
});
return finalResult;
}

const firstTransaction = orderedTransactions[0];

const gasFeeTokenCost = await getGasStationCostInSourceTokenRaw({
// Predict withdraws always consult the gas station: the user's source token
// lives in the Safe, so a healthy native balance does not mean they can
// actually pay gas. They do require an EIP-7702 capable account, since the
// withdraw is submitted as a 7702 batch.
const { amount: gasFeeTokenCost, isAvailable } = await resolveGasStationCost({
accountSupports7702: isPredictWithdraw ? accountSupports7702 : undefined,
firstStepData: {
data: firstTransaction.data,
to: firstTransaction.to,
value: firstTransaction.value,
},
messenger,
nativeGasCostRaw: isPredictWithdraw
? undefined
: finalResult.sourceNetwork.max.raw,
request: {
from,
sourceChainId,
Expand All @@ -757,7 +723,7 @@ async function calculateSourceNetworkCost(
estimate: gasFeeTokenCost,
max: gasFeeTokenCost,
};
} else if (isPredictWithdraw) {
} else if (isAvailable && isPredictWithdraw) {
gasFeeTokenNetwork = calculateSourceGasFeeTokenNetworkFallback({
messenger,
nativeSourceNetwork: finalResult.sourceNetwork,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import type {
import { accountSupports7702 } from '../../utils/7702.js';
import { getPayStrategiesConfig } from '../../utils/feature-flags.js';
import { getGasBuffer } from '../../utils/feature-flags.js';
import {
GasPaymentMode,
logGasPaymentOutcome,
resolveGasPayment,
} from '../../utils/gas-payment.js';
import { getNetworkClientId } from '../../utils/provider.js';
import {
collectTransactionIds,
Expand Down Expand Up @@ -127,8 +132,17 @@ async function submitTransactions(
): Promise<Hex | undefined> {
const { swapTx } = quote.original.quote;
const { gasLimits: quoteGasLimits, is7702 } = quote.original.metamask;
const { from } = quote.request;
const { from, sourceTokenAddress } = quote.request;
const chainId = toHex(swapTx.chainId);

// Across has no relayer-paid delegation flow and is never sponsored, so the
// only lift available is the gas station collecting gas in the source token.
const gasPayment = resolveGasPayment({
isSourceGasFeeToken: quote.fees.isSourceGasFeeToken,
sourceTokenAddress,
});
const isGasFeeTokenPayment = gasPayment.mode === GasPaymentMode.GasFeeToken;

const orderedTransactions = getAcrossOrderedTransactions({
quote: quote.original.quote,
swapType: acrossDepositType,
Expand Down Expand Up @@ -165,10 +179,11 @@ async function submitTransactions(
parentTransaction.txParams.authorizationList?.length,
);

const shouldUseGasFeeToken7702Submit = shouldEstimate7702SubmitBatch(
const shouldUseGasFeeToken7702Submit = shouldEstimate7702SubmitBatch({
isGasFeeTokenPayment,
parentTransaction,
quote,
)
})
? accountSupports7702(messenger, from)
: false;
const shouldUse7702Submit = [
Expand Down Expand Up @@ -259,10 +274,7 @@ async function submitTransactions(
);

let result: { result: Promise<string> } | undefined;
const gasFeeToken = quote.fees.isSourceGasFeeToken
? quote.request.sourceTokenAddress
: undefined;
const excludeNativeTokenForFee = gasFeeToken ? true : undefined;
const { excludeNativeTokenForFee, gasFeeToken } = gasPayment;

try {
if (transactions.length === 1) {
Expand Down Expand Up @@ -313,6 +325,11 @@ async function submitTransactions(
transactionIds.map((txId) => waitForTransactionConfirmed(txId, messenger)),
);

logGasPaymentOutcome(
gasPayment,
transactionIds.map((txId) => getTransaction(txId, messenger)),
);

const hash = transactionIds.length
? getTransaction(transactionIds.slice(-1)[0], messenger)?.hash
: undefined;
Expand Down Expand Up @@ -419,18 +436,25 @@ async function waitForAcrossCompletion(
* gas with the source token, because the final submit batch can differ from the
* batch shape that Across quoted.
*
* @param parentTransaction - Original transaction metadata.
* @param quote - Across quote selected for execution.
* @param args - Check arguments.
* @param args.isGasFeeTokenPayment - Whether source-chain gas is paid with a gas fee token.
* @param args.parentTransaction - Original transaction metadata.
* @param args.quote - Across quote selected for execution.
* @returns Whether submit should try to estimate the final 7702 batch gas.
*/
function shouldEstimate7702SubmitBatch(
parentTransaction: TransactionMeta,
quote: TransactionPayQuote<AcrossQuote>,
): boolean {
function shouldEstimate7702SubmitBatch({
isGasFeeTokenPayment,
parentTransaction,
quote,
}: {
isGasFeeTokenPayment: boolean;
parentTransaction: TransactionMeta;
quote: TransactionPayQuote<AcrossQuote>;
}): boolean {
return (
hasTransactionType(parentTransaction, [TransactionType.predictWithdraw]) &&
quote.request.isPostQuote === true &&
quote.fees.isSourceGasFeeToken === true
isGasFeeTokenPayment
);
}

Expand Down
Loading
Loading