Skip to content

Commit

Permalink
temp+fix: freeze during swap with approval (#11165)
Browse files Browse the repository at this point in the history
## **Description**

The freeze was ultimately caused by a render loop inside
`RootRPCMethodsUI` caused by the
`TransactionController:unapprovedTransactionAdded` listener.

More specifically:

1. When swaps are detected, the transaction is automatically approved
via the `autoSign` function.
2. This registers a listener to send a completed metric once the
transaction is confirmed.
3. This is triggered by adding the transaction ID to a queue via the
`addTransactionMetaIdForListening` function.
4. This function updates React state, but was also using that same state
as a dependency, meaning the callback was updated on every usage.
5. This function was a dependency of `autoSign` which was in turn a
dependency of `onUnapprovedTransaction` which was the only dependency
for a `useEffect` that re-created the
`TransactionController:unapprovedTransactionAdded` listener.
6. The `ControllerMessenger` processes listeners using an iterator
rather than a static list meaning each new listener was automatically
ran which in turn created another listener and resulted in a very high
CPU render loop.

The core fix was to update the `addTransactionMetaIdForListening` to
update the state using the callback override meaning it did not require
the state dependency.

However during testing it was discovered that this swaps completed
metric was not functioning since the `swapsTransaction` state was being
incorrectly populated with the `TransactionController` instance due to a
legacy function argument that was not fully removed.

This has been addressed, and all swaps transactions updates have been
abstracted via a new `swaps-transactions` utility file.

## **Related issues**

Fixes:
[#11085](#11085)

## **Manual testing steps**

Full swaps regression, including:

- Swaps with approvals.
- Smart swaps.
- Metric support.
- Multiple sequential swaps (with and without approvals).

## **Screenshots/Recordings**

### **Before**

### **After**

## **Pre-merge author checklist**

- [x] 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).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] 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.

## **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.

---------

Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com>
  • Loading branch information
runway-github[bot] and sethkfman authored Sep 13, 2024
2 parents ae9d84a + 059a391 commit 34730b6
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 61 deletions.
36 changes: 21 additions & 15 deletions app/components/Nav/Main/RootRPCMethodsUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { STX_NO_HASH_ERROR } from '../../../util/smart-transactions/smart-publis
import { getSmartTransactionMetricsProperties } from '../../../util/smart-transactions';
import { cloneDeep, isEqual } from 'lodash';
import { selectSwapsTransactions } from '../../../selectors/transactionController';
import { updateSwapsTransaction } from '../../../util/swaps/swaps-transactions';

///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import InstallSnapApproval from '../../Approvals/InstallSnapApproval';
Expand All @@ -88,12 +89,12 @@ export const useSwapConfirmedEvent = ({ trackSwaps }) => {

const addTransactionMetaIdForListening = useCallback(
(txMetaId) => {
setTransactionMetaIdsForListening([
setTransactionMetaIdsForListening((transactionMetaIdsForListening) => [
...transactionMetaIdsForListening,
txMetaId,
]);
},
[transactionMetaIdsForListening],
[],
);
const swapsTransactions = useSwapsTransactions();

Expand Down Expand Up @@ -144,8 +145,8 @@ const RootRPCMethodsUI = (props) => {
try {
const { TransactionController, SmartTransactionsController } =
Engine.context;
const newSwapsTransactions = swapsTransactions;
const swapTransaction = newSwapsTransactions[transactionMeta.id];
const swapTransaction = swapsTransactions[transactionMeta.id];

const {
sentAt,
gasEstimate,
Expand Down Expand Up @@ -187,15 +188,6 @@ const RootRPCMethodsUI = (props) => {
ethBalance,
);

newSwapsTransactions[transactionMeta.id].gasUsed = receipt.gasUsed;
if (tokensReceived) {
newSwapsTransactions[transactionMeta.id].receivedDestinationAmount =
new BigNumber(tokensReceived, 16).toString(10);
}
TransactionController.update((state) => {
state.swapsTransactions = newSwapsTransactions;
});

const timeToMine = currentBlock.timestamp - sentAt;
const estimatedVsUsedGasRatio = `${new BigNumber(receipt.gasUsed)
.div(gasEstimate)
Expand All @@ -218,8 +210,20 @@ const RootRPCMethodsUI = (props) => {
...swapTransaction.analytics,
account_type: getAddressAccountType(transactionMeta.txParams.from),
};
delete newSwapsTransactions[transactionMeta.id].analytics;
delete newSwapsTransactions[transactionMeta.id].paramsForAnalytics;

updateSwapsTransaction(transactionMeta.id, (swapsTransaction) => {
swapsTransaction.gasUsed = receipt.gasUsed;

if (tokensReceived) {
swapsTransaction.receivedDestinationAmount = new BigNumber(
tokensReceived,
16,
).toString(10);
}

delete swapsTransaction.analytics;
delete swapsTransaction.paramsForAnalytics;
});

const smartTransactionMetricsProperties =
getSmartTransactionMetricsProperties(
Expand All @@ -237,6 +241,8 @@ const RootRPCMethodsUI = (props) => {
...smartTransactionMetricsProperties,
};

Logger.log('Swaps', 'Sending metrics event', event);

trackEvent(event, { sensitiveProperties: { ...parameters } });
} catch (e) {
Logger.error(e, MetaMetricsEvents.SWAP_TRACKING_FAILED);
Expand Down
67 changes: 31 additions & 36 deletions app/components/UI/Swaps/QuotesView.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAs
import { selectGasFeeEstimates } from '../../../selectors/confirmTransaction';
import { selectShouldUseSmartTransaction } from '../../../selectors/smartTransactionsController';
import { selectGasFeeControllerEstimateType } from '../../../selectors/gasFeeController';
import { addSwapsTransaction } from '../../../util/swaps/swaps-transactions';

const LOG_PREFIX = 'Swaps';
const POLLING_INTERVAL = 30000;
const SLIPPAGE_BUCKETS = {
MEDIUM: AppConstants.GAS_OPTIONS.MEDIUM,
Expand Down Expand Up @@ -795,16 +797,15 @@ function SwapsQuotesView({
async (
transactionMeta,
approvalTransactionMetaId,
newSwapsTransactions,
) => {
const { TransactionController } = Engine.context;
const ethQuery = Engine.getGlobalEthQuery();
const blockNumber = await query(ethQuery, 'blockNumber', []);
const currentBlock = await query(ethQuery, 'getBlockByNumber', [
blockNumber,
false,
]);
newSwapsTransactions[transactionMeta.id] = {

addSwapsTransaction(transactionMeta.id, {
action: 'swap',
sourceToken: {
address: sourceToken.address,
Expand Down Expand Up @@ -851,9 +852,6 @@ function SwapsQuotesView({
ethAccountBalance: accounts[selectedAddress].balance,
approvalTransactionMetaId,
},
};
TransactionController.update((state) => {
state.swapsTransactions = newSwapsTransactions;
});
},
[
Expand Down Expand Up @@ -922,7 +920,7 @@ function SwapsQuotesView({
);

const handleSwapTransaction = useCallback(
async (newSwapsTransactions, approvalTransactionMetaId) => {
async (approvalTransactionMetaId) => {
if (!selectedQuote) {
return;
}
Expand All @@ -944,19 +942,22 @@ function SwapsQuotesView({
},
);

Logger.log(LOG_PREFIX, 'Added trade transaction', transactionMeta.id);

await result;

Logger.log(LOG_PREFIX, 'Submitted trade transaction', transactionMeta.id);

updateSwapsTransactions(
transactionMeta,
approvalTransactionMetaId,
newSwapsTransactions,
approvalTransactionMetaId
);

setRecipient(selectedAddress);
await addTokenToAssetsController(destinationToken);
await addTokenToAssetsController(sourceToken);
} catch (e) {
// send analytics
Logger.log(LOG_PREFIX, 'Failed to submit trade transaction', e);
}
},
[
Expand All @@ -973,11 +974,8 @@ function SwapsQuotesView({
],
);

const handleApprovaltransaction = useCallback(
const handleApprovalTransaction = useCallback(
async (
TransactionController,
newSwapsTransactions,
approvalTransactionMetaId,
isHardwareAddress,
) => {
try {
Expand All @@ -996,11 +994,17 @@ function SwapsQuotesView({
},
);

Logger.log(LOG_PREFIX, 'Added approval transaction', transactionMeta.id);

await result;

Logger.log(LOG_PREFIX, 'Submitted approval transaction', transactionMeta.id);

setRecipient(selectedAddress);

approvalTransactionMetaId = transactionMeta.id;
newSwapsTransactions[transactionMeta.id] = {
const approvalTransactionMetaId = transactionMeta.id;

addSwapsTransaction(transactionMeta.id, {
action: 'approval',
sourceToken: {
address: sourceToken.address,
Expand All @@ -1011,7 +1015,8 @@ function SwapsQuotesView({
decodeApproveData(approvalTransaction.data).encodedAmount,
16,
).toString(10),
};
});

if (isHardwareAddress || shouldUseSmartTransaction) {
const { id: transactionId } = transactionMeta;

Expand All @@ -1020,18 +1025,17 @@ function SwapsQuotesView({
(transactionMeta) => {
if (transactionMeta.status === TransactionStatus.confirmed) {
handleSwapTransaction(
TransactionController,
newSwapsTransactions,
approvalTransactionMetaId,
isHardwareAddress,
approvalTransactionMetaId
);
}
},
(transactionMeta) => transactionMeta.id === transactionId,
);
}

return approvalTransactionMetaId;
} catch (e) {
// send analytics
Logger.log(LOG_PREFIX, 'Failed to submit approval transaction', e);
}
},
[
Expand All @@ -1044,7 +1048,7 @@ function SwapsQuotesView({
selectedAddress,
setRecipient,
resetTransaction,
shouldUseSmartTransaction,
shouldUseSmartTransaction
],
);

Expand All @@ -1057,16 +1061,10 @@ function SwapsQuotesView({

startSwapAnalytics(selectedQuote, selectedAddress);

const { TransactionController } = Engine.context;

const newSwapsTransactions =
TransactionController.state.swapsTransactions || {};
let approvalTransactionMetaId;

if (approvalTransaction) {
await handleApprovaltransaction(
TransactionController,
newSwapsTransactions,
approvalTransactionMetaId,
approvalTransactionMetaId = await handleApprovalTransaction(
isHardwareAddress,
);

Expand All @@ -1081,10 +1079,7 @@ function SwapsQuotesView({
(shouldUseSmartTransaction && !approvalTransaction)
) {
await handleSwapTransaction(
TransactionController,
newSwapsTransactions,
approvalTransactionMetaId,
isHardwareAddress,
approvalTransactionMetaId
);
}

Expand All @@ -1094,7 +1089,7 @@ function SwapsQuotesView({
selectedAddress,
approvalTransaction,
startSwapAnalytics,
handleApprovaltransaction,
handleApprovalTransaction,
handleSwapTransaction,
navigation,
shouldUseSmartTransaction,
Expand Down
1 change: 1 addition & 0 deletions app/core/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,7 @@ class Engine {
transactions: [],
lastFetchedBlockNumbers: {},
submitHistory: [],
swapsTransactions: {}
}));

LoggingController.clear();
Expand Down
2 changes: 1 addition & 1 deletion app/selectors/smartTransactionsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const selectPendingSmartTransactionsBySender = (state: RootState) => {
// stx.uuid is one from sentinel API, not the same as tx.id which is generated client side
// Doesn't matter too much because we only care about the pending stx, confirmed txs are handled like normal
// However, this does make it impossible to read Swap data from TxController.swapsTransactions as that relies on client side tx.id
// To fix that we do transactionController.update({ swapsTransactions: newSwapsTransactions }) in app/util/smart-transactions/smart-tx.ts
// To fix that we create a duplicate swaps transaction for the stx.uuid in the smart publish hook.
id: stx.uuid,
status: stx.status?.startsWith(SmartTransactionStatuses.CANCELLED)
? SmartTransactionStatuses.CANCELLED
Expand Down
10 changes: 8 additions & 2 deletions app/util/smart-transactions/smart-publish-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ jest.mock('uuid', () => ({
v1: jest.fn(() => 'approvalId'),
}));

jest.mock('../../core/Engine', () => ({
context: {
TransactionController: {
update: jest.fn(),
},
},
}));

const addressFrom = '0xabce7847fd3661a9b7c86aaf1daea08d9da5750e';
const transactionHash =
'0x0302b75dfb9fd9eb34056af031efcaee2a8cbd799ea054a85966165cd82a7356';
Expand Down Expand Up @@ -505,8 +513,6 @@ describe('submitSmartTransactionHook', () => {
isSwapTransaction: true,
},
});
//@ts-expect-error - We are calling a protected method for testing purposes
expect(request.transactionController.update).toHaveBeenCalledTimes(1);
});
});
});
13 changes: 6 additions & 7 deletions app/util/smart-transactions/smart-publish-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { v1 as random } from 'uuid';
import { decimalToHex } from '../conversions';
import { ApprovalTypes } from '../../core/RPCMethods/RPCMethodMiddleware';
import { RAMPS_SEND } from '../../components/UI/Ramp/constants';
import { addSwapsTransaction } from '../swaps/swaps-transactions';

export declare type Hex = `0x${string}`;

Expand Down Expand Up @@ -427,17 +428,15 @@ class SmartTransactionHook {
this.#approvalEnded = true;
};

#updateSwapsTransactions = (id: string) => {
#updateSwapsTransactions = (uuid: string) => {
// We do this so we can show the Swap data (e.g. ETH to USDC, fiat values) in the app/components/Views/TransactionsView/index.js
const newSwapsTransactions =
const swapsTransactions =
// @ts-expect-error This is not defined on the type, but is a field added in app/components/UI/Swaps/QuotesView.js
this.#transactionController.state.swapsTransactions || {};

newSwapsTransactions[id] = newSwapsTransactions[this.#transactionMeta.id];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.#transactionController as any).update((state: any) => {
state.swapsTransactions = newSwapsTransactions;
});
const originalSwapsTransaction = swapsTransactions[this.#transactionMeta.id];

addSwapsTransaction(uuid, originalSwapsTransaction);
};
}

Expand Down
Loading

0 comments on commit 34730b6

Please sign in to comment.