Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -208,18 +208,22 @@ export const defaultEffectiveFeePayer = 'user' as IGasPayer;

// `effectiveFeePayerAtom` is the authoritative "who pays the fee" signal the
// UI renders from (sponsor badges, free copy, fee hiding). It mirrors the
// server's `payer` field with two narrow overrides to `'user'`:
// - when a custom RPC is active (all sponsors disabled), and
// - when the server indicates `'gasAccount'` while gas account is
// server's `payer` field with narrow overrides:
// - `'user'` when a custom RPC is active (all sponsors disabled),
// - `'user'` when the server indicates `'gasAccount'` while gas account is
// temporarily disabled after a fallback (the gas-account path only;
// a concurrent `'megafuel'` payer still surfaces).
// a concurrent `'megafuel'` payer still surfaces), and
// - when megafuel is suppressed for Private Send, a server `'megafuel'`
// payer falls through to `'gasAccount'` if an eligible quote exists,
// otherwise `'user'`.
//
// This is intentionally separate from `gasAccountUiState.selectedPayer` below:
// - `effectiveFeePayer` drives *display* (can be `'megafuel'` even when gas
// account quote exists — megafuel wins UI-wise).
// - `selectedPayer` drives *submit wiring* (whether to attach `quoteId` /
// `idempotencyKey` to the broadcast request).
// Keep them aligned in TxFeeInfo's estimate handler.
// Both derive from `resolveSponsorPayerState` in TxFeeInfo's estimate handler
// so they stay aligned by construction.
export const { atom: effectiveFeePayerAtom, use: useEffectiveFeePayerAtom } =
contextAtom<IGasPayer>(defaultEffectiveFeePayer);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import {
EGasAccountErrorStrategy,
getGasAccountErrorEntry,
} from '../../constants/gasAccountErrorCodes';
import { resolveSponsorPayerState } from '../../utils/gasAccountPayerSelection';

import { buildPresetMultiTxsFee } from './presetFeeInfoUtils';
import { TxFeeEditor } from './TxFeeEditor';
Expand Down Expand Up @@ -560,15 +561,28 @@ function TxFeeInfo(props: IProps) {
// Megafuel is an independent sponsor mechanism and should still be
// honored when the server indicates `payer='megafuel'`, even when a
// frontend scenario disables Gas Account.
//
// Display payer and submit wiring are derived together from the
// post-filtered sponsor state so they cannot drift: when megafuel is
// suppressed for Private Send, a server `payer='megafuel'` preference
// falls through to an eligible gas account quote instead of silently
// degrading to user-paid.
const serverPayer: IGasPayer = r.payer ?? 'user';
const nextEffectiveFeePayer: IGasPayer =
isCustomRpcEnabled ||
sponsorDisabledForBatch ||
(megafuelDisabledForPrivateSend && serverPayer === 'megafuel') ||
(gasAccountDisabledByScenario && serverPayer === 'gasAccount') ||
(gasAccountTemporarilyDisabled && serverPayer === 'gasAccount')
? 'user'
: serverPayer;
const {
effectiveFeePayer: nextEffectiveFeePayer,
selectedPayer: nextSelectedPayer,
} = resolveSponsorPayerState({
serverPayer,
megafuelSponsorable: !!r.megafuelEligible?.sponsorable,
gasAccountQuoteEligible: !!(
Comment thread
weatherstar marked this conversation as resolved.
Outdated
r.gasAccountEligible && r.gasAccountQuote
),
isCustomRpcEnabled,
sponsorDisabledForBatch,
megafuelDisabledForPrivateSend,
gasAccountDisabledByScenario,
gasAccountTemporarilyDisabled,
});
updateEffectiveFeePayer(nextEffectiveFeePayer);

if (
Expand Down Expand Up @@ -626,11 +640,6 @@ function TxFeeInfo(props: IProps) {
}
} else if (r.gasAccountEligible && r.gasAccountQuote) {
resetGasAccountTemporarilyDisabled();
const nextSelectedPayer =
r.megafuelEligible?.sponsorable || r.payer !== 'gasAccount'
? 'user'
: 'gasAccount';

updateGasAccountUiState({
payer: r.payer,
gasAccountEligible: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { resolveSponsorPayerState } from './gasAccountPayerSelection';

const baseParams = {
serverPayer: 'user' as const,
megafuelSponsorable: false,
gasAccountQuoteEligible: false,
isCustomRpcEnabled: false,
sponsorDisabledForBatch: false,
megafuelDisabledForPrivateSend: false,
gasAccountDisabledByScenario: false,
gasAccountTemporarilyDisabled: false,
};

describe('resolveSponsorPayerState', () => {
describe('default flow (no suppression)', () => {
it('keeps user payer when the server does not sponsor', () => {
expect(resolveSponsorPayerState(baseParams)).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('selects gas account when the server prefers it and a quote is eligible', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'gasAccount',
gasAccountQuoteEligible: true,
}),
).toEqual({
effectiveFeePayer: 'gasAccount',
selectedPayer: 'gasAccount',
});
});

it('keeps user submit when megafuel sponsors even if a gas account quote exists', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'megafuel',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
}),
).toEqual({
effectiveFeePayer: 'megafuel',
selectedPayer: 'user',
});
});

it('lets megafuel win submit wiring when sponsorable alongside a gasAccount payer', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'gasAccount',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
}),
).toEqual({
effectiveFeePayer: 'gasAccount',
selectedPayer: 'user',
});
});

it('does not select gas account without an eligible quote', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'gasAccount',
gasAccountQuoteEligible: false,
}),
).toEqual({
effectiveFeePayer: 'gasAccount',
selectedPayer: 'user',
});
});
});

describe('global suppressions', () => {
it('forces user for both payers when a custom RPC is enabled', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'gasAccount',
gasAccountQuoteEligible: true,
isCustomRpcEnabled: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('forces user for both payers on batch transactions', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'megafuel',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
sponsorDisabledForBatch: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('forces user when gas account is temporarily disabled after a fallback', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'gasAccount',
gasAccountQuoteEligible: true,
gasAccountTemporarilyDisabled: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('forces user when the frontend scenario disables gas account', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'gasAccount',
gasAccountQuoteEligible: true,
gasAccountDisabledByScenario: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('still surfaces megafuel display when only the gas account path is scenario-disabled', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'megafuel',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
gasAccountDisabledByScenario: true,
}),
).toEqual({
effectiveFeePayer: 'megafuel',
selectedPayer: 'user',
});
});
});

describe('private send (megafuel suppressed)', () => {
it('falls back to the eligible gas account quote when the server prefers megafuel', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'megafuel',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
megafuelDisabledForPrivateSend: true,
}),
).toEqual({
effectiveFeePayer: 'gasAccount',
selectedPayer: 'gasAccount',
});
});

it('falls back to user when the server prefers megafuel and no quote is eligible', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'megafuel',
megafuelSponsorable: true,
gasAccountQuoteEligible: false,
megafuelDisabledForPrivateSend: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('selects gas account when the server prefers it even with megafuel sponsorable', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'gasAccount',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
megafuelDisabledForPrivateSend: true,
}),
).toEqual({
effectiveFeePayer: 'gasAccount',
selectedPayer: 'gasAccount',
});
});

it('keeps user payer when the server does not sponsor', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'user',
megafuelDisabledForPrivateSend: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('does not fall back to gas account when a custom RPC is enabled', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'megafuel',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
megafuelDisabledForPrivateSend: true,
isCustomRpcEnabled: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});

it('does not fall back to gas account while it is temporarily disabled', () => {
expect(
resolveSponsorPayerState({
...baseParams,
serverPayer: 'megafuel',
megafuelSponsorable: true,
gasAccountQuoteEligible: true,
megafuelDisabledForPrivateSend: true,
gasAccountTemporarilyDisabled: true,
}),
).toEqual({
effectiveFeePayer: 'user',
selectedPayer: 'user',
});
});
});
});
Loading
Loading