Skip to content

Commit dbc74ea

Browse files
authored
fix: gas fees sponsored full swap native in transaction display (#28930)
## **Description** This pull request updates how the source token amount is displayed for gas-sponsored swap transactions, ensuring that the full amount sent by the user is shown instead of the post-fee amount. The changes affect both the transaction details UI and the transaction history decoding logic, and include new tests and mock data to cover this scenario. The fix mirror the extension's display logic for this use case. **Gas-sponsored transaction handling:** * Updated `BridgeTransactionDetails` to display the full user amount (`pricingData.amountSent`) when `quote.gasSponsored` is true, instead of the fee-deducted `srcTokenAmount`. (`app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.tsx`) * Modified `decodeSwapsTx` to use `pricingData.amountSent` for gas-sponsored transactions, ensuring transaction history reflects the correct sent amount. (`app/components/UI/Bridge/utils/transaction-history.ts`) **Testing and mock data:** * Added a new test to verify that the full amount is displayed for gas-sponsored transactions in the transaction details component. (`app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.test.tsx`) * Added a new test to ensure `decodeSwapsTx` uses `pricingData.amountSent` when gas is sponsored. (`app/components/UI/Bridge/utils/transaction-history.test.ts`) * Extended the mock bridge transaction state with a gas-sponsored transaction example, including all relevant fields for accurate testing. (`app/components/UI/Bridge/_mocks_/initialState.ts`) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` --> CHANGELOG entry: display correct amount in swap native in transaction display for gas fees sponsored trx ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: display the full amount swapped by the user Scenario: user swaps native token to any tokens Given the network is gas fees sponsored When user swaps 1 SEI for USDC Then the transaction history should display the full amount sent: 1 SEI ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> Swapping 1 SEI for USDC on SEI gas fees sponsored network ### **Before** <img width="250" height="550" alt="SEI native swap BEFORE" src="https://github.com/user-attachments/assets/e0fd55b1-f75d-4794-a6ea-e4aaf1b18ab0" /> ------------------------ ### **After** <img width="250" height="550" alt="SEI native swap Activity trx AFTER" src="https://github.com/user-attachments/assets/d5f67be1-da6e-47e0-856d-609b86060cf2" /> <img width="250" height="550" alt="SEI native swap details AFTER" src="https://github.com/user-attachments/assets/ea7386c6-7d9b-41a6-9f17-33593cd72f56" /> <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Display-only changes that switch the source amount calculation for a specific gas-sponsored swap case; covered by new unit tests and mock state. > > **Overview** > Fixes gas-sponsored swap/bridge UI and activity decoding to show the *full user sent amount* (`pricingData.amountSent`) instead of the fee-deducted `quote.srcTokenAmount`. > > Updates `BridgeTransactionDetails` and `decodeSwapsTx` to prefer `pricingData.amountSent` when `quote.gasSponsored` is true, and adds a dedicated gas-sponsored transaction fixture plus tests to prevent regressions in both the details screen and transaction history decoding. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3f57b98. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a0f9657 commit dbc74ea

5 files changed

Lines changed: 188 additions & 10 deletions

File tree

app/components/UI/Bridge/_mocks_/initialState.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,59 @@ export const initialState = {
673673
startTime: Date.now(),
674674
estimatedProcessingTimeInSeconds: 300,
675675
},
676+
'gas-sponsored-tx-id': {
677+
txMetaId: 'gas-sponsored-tx-id',
678+
account: evmAccountAddress,
679+
quote: {
680+
requestId: 'test-request-id',
681+
srcChainId: 1329,
682+
srcAsset: {
683+
chainId: 1329,
684+
address: '0x0000000000000000000000000000000000000000',
685+
decimals: 18,
686+
symbol: 'SEI',
687+
name: 'Sei',
688+
},
689+
destChainId: 1329,
690+
destAsset: {
691+
chainId: 1329,
692+
address: '0xe15fc38f6d8c56af07bbcbe3baf5708a2bf42392',
693+
decimals: 6,
694+
symbol: 'USDC',
695+
name: 'USDC',
696+
},
697+
// srcTokenAmount has metabridge fee deducted (0.99125 SEI)
698+
srcTokenAmount: '991250000000000000',
699+
destTokenAmount: '55320',
700+
feeData: {
701+
metabridge: {
702+
amount: '8750000000000000',
703+
asset: {
704+
address: '0x0000000000000000000000000000000000000000',
705+
chainId: 1329,
706+
symbol: 'SEI',
707+
decimals: 18,
708+
name: 'Sei',
709+
},
710+
},
711+
},
712+
gasSponsored: true,
713+
gasIncluded7702: true,
714+
},
715+
// pricingData.amountSent is the full user amount (1.0 SEI)
716+
pricingData: {
717+
amountSent: '1',
718+
amountSentInUsd: '0.055909',
719+
},
720+
status: {
721+
srcChain: {
722+
txHash: '0xgas123',
723+
},
724+
status: StatusTypes.COMPLETE,
725+
},
726+
startTime: Date.now(),
727+
estimatedProcessingTimeInSeconds: 0,
728+
},
676729
'solana-swap-tx': {
677730
quote: {
678731
srcChainId: 1151111081099710, // Solana Mainnet

app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,32 @@ describe('BridgeTransactionDetails', () => {
225225
expect(queryByTestId('paid-by-metamask')).not.toBeOnTheScreen();
226226
});
227227

228+
it('displays full amount from pricingData.amountSent when gas is sponsored', () => {
229+
mockIsHardwareAccount.mockReturnValue(false);
230+
231+
const gasSponsoredTx = {
232+
...mockEVMTx,
233+
id: 'gas-sponsored-tx-id',
234+
isGasFeeSponsored: true,
235+
} as TransactionMeta;
236+
237+
const { getByText } = renderScreen(
238+
() => (
239+
<BridgeTransactionDetails
240+
route={{ params: { evmTxMeta: gasSponsoredTx } }}
241+
/>
242+
),
243+
{
244+
name: Routes.BRIDGE.BRIDGE_TRANSACTION_DETAILS,
245+
},
246+
{ state: mockState },
247+
);
248+
249+
// Should display "1.00000 SEI" (from pricingData.amountSent),
250+
// not "0.99125 SEI" (from srcTokenAmount)
251+
expect(getByText(/1\.00000\s+SEI/)).toBeOnTheScreen();
252+
});
253+
228254
it('shows "Paid by MetaMask" when gas is sponsored and sender is not a hardware wallet', () => {
229255
mockIsHardwareAccount.mockReturnValue(false);
230256

app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,12 @@ export const BridgeTransactionDetails = (
234234
chainId: sourceChainId,
235235
};
236236

237-
const sourceTokenAmount = calcTokenAmount(
238-
quote.srcTokenAmount,
239-
quote.srcAsset.decimals,
240-
).toFixed(5);
237+
const sourceTokenAmount =
238+
quote.gasSponsored && bridgeTxHistoryItem.pricingData?.amountSent
239+
? parseFloat(bridgeTxHistoryItem.pricingData.amountSent).toFixed(5)
240+
: calcTokenAmount(quote.srcTokenAmount, quote.srcAsset.decimals).toFixed(
241+
5,
242+
);
241243

242244
const destinationChainId = isNonEvmChainId(quote.destChainId)
243245
? formatChainIdToCaip(quote.destChainId)

app/components/UI/Bridge/utils/transaction-history.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,100 @@ describe('decodeSwapsTx', () => {
411411
},
412412
]);
413413
});
414+
415+
it('uses pricingData.amountSent when quote.gasSponsored is true', () => {
416+
const args = {
417+
tx: {
418+
chainId: '0x531',
419+
id: 'gas-sponsored-tx-id',
420+
status: 'confirmed',
421+
txParams: {
422+
from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452',
423+
to: '0x881d40237659c251811cec9c364ef91dc08d300c',
424+
gas: '0x5208',
425+
value: '0x0',
426+
},
427+
type: 'swap',
428+
},
429+
txChainId: '0x531',
430+
ticker: 'SEI',
431+
currentCurrency: 'usd',
432+
contractExchangeRates: {},
433+
conversionRate: 0.055,
434+
primaryCurrency: 'ETH',
435+
bridgeTxHistoryData: {
436+
bridgeTxHistoryItem: {
437+
txMetaId: 'gas-sponsored-tx-id',
438+
quote: {
439+
requestId: 'test-request-id',
440+
bridgeId: 'openocean',
441+
srcChainId: 1329,
442+
destChainId: 1329,
443+
srcAsset: {
444+
address: '0x0000000000000000000000000000000000000000',
445+
chainId: 1329,
446+
assetId: 'eip155:1329/slip44:19000118',
447+
symbol: 'SEI',
448+
decimals: 18,
449+
name: 'Sei',
450+
},
451+
// srcTokenAmount has the metabridge fee deducted (0.99125 SEI)
452+
srcTokenAmount: '991250000000000000',
453+
destAsset: {
454+
address: '0xe15fc38f6d8c56af07bbcbe3baf5708a2bf42392',
455+
chainId: 1329,
456+
assetId:
457+
'eip155:1329/erc20:0xe15fc38f6d8c56af07bbcbe3baf5708a2bf42392',
458+
symbol: 'USDC',
459+
decimals: 6,
460+
name: 'USDC',
461+
},
462+
destTokenAmount: '55320',
463+
minDestTokenAmount: '54213',
464+
feeData: {
465+
metabridge: {
466+
amount: '8750000000000000',
467+
asset: {
468+
address: '0x0000000000000000000000000000000000000000',
469+
chainId: 1329,
470+
assetId: 'eip155:1329/slip44:19000118',
471+
symbol: 'SEI',
472+
decimals: 18,
473+
name: 'Sei',
474+
},
475+
},
476+
},
477+
bridges: ['1inch'],
478+
protocols: ['1inch'],
479+
steps: [],
480+
gasSponsored: true,
481+
gasIncluded7702: true,
482+
},
483+
startTime: Date.now(),
484+
estimatedProcessingTimeInSeconds: 0,
485+
slippagePercentage: 0,
486+
// amountSent is the full user amount (1.0 SEI)
487+
pricingData: {
488+
amountSent: '1',
489+
amountSentInUsd: '0.055909',
490+
},
491+
account: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452',
492+
status: {
493+
status: 'PENDING',
494+
srcChain: { chainId: 1329 },
495+
},
496+
hasApprovalTx: false,
497+
},
498+
isBridgeComplete: false,
499+
},
500+
};
501+
502+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
503+
const res = decodeSwapsTx(args as unknown as any);
504+
505+
// Should display "1 SEI" (from pricingData.amountSent), not "0.99125 SEI" (from srcTokenAmount)
506+
expect((res[0] as any).value).toBe('1 SEI');
507+
});
414508
});
415509

416510
describe('decodeBridgeTx', () => {

app/components/UI/Bridge/utils/transaction-history.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,15 @@ export const decodeSwapsTx = (args: {
140140

141141
const sourceTokenSymbol = quote.srcAsset?.symbol;
142142
const destTokenSymbol = quote.destAsset?.symbol;
143-
const rawSourceAmount = parseFloat(
144-
ethers.utils.formatUnits(
145-
bridgeTxHistoryItem.quote.srcTokenAmount,
146-
quote.srcAsset.decimals,
147-
),
148-
);
143+
const rawSourceAmount =
144+
quote.gasSponsored && bridgeTxHistoryItem.pricingData?.amountSent
145+
? parseFloat(bridgeTxHistoryItem.pricingData.amountSent)
146+
: parseFloat(
147+
ethers.utils.formatUnits(
148+
bridgeTxHistoryItem.quote.srcTokenAmount,
149+
quote.srcAsset.decimals,
150+
),
151+
);
149152
const sourceAmountSent = formatAmountWithThreshold(rawSourceAmount, 5);
150153

151154
const renderTo = tx.txParams.to;

0 commit comments

Comments
 (0)