Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **BREAKING:** Add `AppMetadataControllerGetStateAction` and `AssetsControllerGetStateForTransactionPayAction` to the `AllowedActions` messenger type ([#8163](https://github.com/MetaMask/core/pull/8163))
- Support gasless Relay deposits via `execute` endpoint ([#8133](https://github.com/MetaMask/core/pull/8133))
- Build Across post-swap transfer actions for `predictDeposit` quotes so Predict deposits can bridge swapped output into the destination proxy wallet ([#8159](https://github.com/MetaMask/core/pull/8159))

### Changed

- `getTokenBalance`, `getTokenInfo`, and `getTokenFiatRate` now source token metadata, balances, and pricing from `AssetsController:getStateForTransactionPay` when the `assetsUnifyState` remote feature flag is enabled, falling back to individual controller state calls otherwise ([#8163](https://github.com/MetaMask/core/pull/8163))
- Bump `@metamask/assets-controllers` from `^100.2.0` to `^100.2.1` ([#8162](https://github.com/MetaMask/core/pull/8162))
- Bump `@metamask/bridge-controller` from `^69.0.0` to `^69.1.0` ([#8162](https://github.com/MetaMask/core/pull/8162), [#8168](https://github.com/MetaMask/core/pull/8168))
- Bump `@metamask/bridge-status-controller` from `^68.0.1` to `^68.1.0` ([#8162](https://github.com/MetaMask/core/pull/8162), [#8168](https://github.com/MetaMask/core/pull/8168))
Expand Down
2 changes: 2 additions & 0 deletions packages/transaction-pay-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
"@ethersproject/abi": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@metamask/app-metadata-controller": "^2.0.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dependency is needed for feature flags that have a minimum version requirement, as this package exposes the current app version to do the comparison.

"@metamask/assets-controller": "^2.3.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assets-controller is the new controller that we are migrating towards and that will hold the state.

Once the flag is fully turned on, we will come back here and delete all the old code and the old dependency.

"@metamask/assets-controllers": "^100.2.1",
"@metamask/base-controller": "^9.0.0",
"@metamask/bridge-controller": "^69.1.0",
Expand Down
19 changes: 19 additions & 0 deletions packages/transaction-pay-controller/src/tests/messenger-mock.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AppMetadataControllerGetStateAction } from '@metamask/app-metadata-controller';
import type { TokensControllerGetStateAction } from '@metamask/assets-controllers';
import type { TokenBalancesControllerGetStateAction } from '@metamask/assets-controllers';
import type { TokenRatesControllerGetStateAction } from '@metamask/assets-controllers';
Expand Down Expand Up @@ -45,6 +46,10 @@ type RootMessenger = Messenger<MockAnyNamespace, AllActions, AllEvents>;
export function getMessengerMock({
skipRegister,
}: { skipRegister?: boolean } = {}) {
const getAppMetadataControllerStateMock: jest.MockedFn<
AppMetadataControllerGetStateAction['handler']
> = jest.fn();

const getControllerStateMock: jest.MockedFn<
TransactionPayControllerGetStateAction['handler']
> = jest.fn();
Expand Down Expand Up @@ -127,6 +132,8 @@ export function getMessengerMock({
TransactionControllerEstimateGasBatchAction['handler']
> = jest.fn();

const getAssetsControllerStateMock = jest.fn();

const messenger: RootMessenger = new Messenger({
namespace: MOCK_ANY_NAMESPACE,
});
Expand Down Expand Up @@ -241,12 +248,24 @@ export function getMessengerMock({
'TransactionController:estimateGasBatch',
estimateGasBatchMock,
);

messenger.registerActionHandler(
'AppMetadataController:getState',
getAppMetadataControllerStateMock,
);

messenger.registerActionHandler(
'AssetsController:getStateForTransactionPay',
getAssetsControllerStateMock,
);
}

const publish = messenger.publish.bind(messenger);

return {
addTransactionMock,
getAppMetadataControllerStateMock,
getAssetsControllerStateMock,
addTransactionBatchMock,
estimateGasMock,
estimateGasBatchMock,
Expand Down
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AppMetadataControllerGetStateAction } from '@metamask/app-metadata-controller';
import type { AssetsControllerGetStateForTransactionPayAction } from '@metamask/assets-controller';
import type {
CurrencyRateControllerActions,
TokenBalancesControllerGetStateAction,
Expand Down Expand Up @@ -38,6 +40,8 @@ import type { CONTROLLER_NAME, TransactionPayStrategy } from './constants';

export type AllowedActions =
| AccountTrackerControllerGetStateAction
| AppMetadataControllerGetStateAction
| AssetsControllerGetStateForTransactionPayAction
| BridgeControllerActions
| BridgeStatusControllerActions
| CurrencyRateControllerActions
Expand Down
113 changes: 111 additions & 2 deletions packages/transaction-pay-controller/src/utils/feature-flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DEFAULT_RELAY_QUOTE_URL,
DEFAULT_SLIPPAGE,
DEFAULT_STRATEGY_ORDER,
getAssetsUnifyStateFeature,
getFallbackGas,
DEFAULT_RELAY_EXECUTE_URL,
getRelayOriginGasOverhead,
Expand Down Expand Up @@ -38,8 +39,11 @@ const TOKEN_ADDRESS_DIFFERENT_MOCK = '0xdef789abc012' as Hex;
const TOKEN_SPECIFIC_SLIPPAGE_MOCK = 0.02;

describe('Feature Flags Utils', () => {
const { messenger, getRemoteFeatureFlagControllerStateMock } =
getMessengerMock();
const {
messenger,
getAppMetadataControllerStateMock,
getRemoteFeatureFlagControllerStateMock,
} = getMessengerMock();

beforeEach(() => {
jest.resetAllMocks();
Expand Down Expand Up @@ -562,6 +566,111 @@ describe('Feature Flags Utils', () => {
});
});

describe('getAssetsUnifyStateFeature', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is prime to convert into a test table/it.each 🤞🏾

Example Test Table / `it.each`
  describe('getAssetsUnifyStateFeature', () => {
    const defaultAppMetadata = {
      currentAppVersion: '',
      previousAppVersion: '',
      previousMigrationVersion: 0,
      currentMigrationVersion: 0,
    };

    const failureCases = [
      {
        description: 'returns false when assetsUnifyState is not set',
        remoteFeatureFlags: undefined,
        appMetadata: undefined,
      },
      {
        description: 'returns false when assetsUnifyState.enabled is false',
        remoteFeatureFlags: {
          assetsUnifyState: {
            enabled: false,
            featureVersion: '1',
            minimumVersion: null,
          },
        },
        appMetadata: undefined,
      },
      {
        description:
          'returns false when featureVersion does not match expected version',
        remoteFeatureFlags: {
          assetsUnifyState: {
            enabled: true,
            featureVersion: '2',
            minimumVersion: null,
          },
        },
        appMetadata: undefined,
      },
      {
        description:
          'returns false when app version does not satisfy minimumVersion',
        remoteFeatureFlags: {
          assetsUnifyState: {
            enabled: true,
            featureVersion: '1',
            minimumVersion: '2.0.0',
          },
        },
        appMetadata: {
          ...defaultAppMetadata,
          currentAppVersion: '1.0.0',
        },
      },
    ];

    const successCases = [
      {
        description: 'returns true when minimumVersion is null',
        remoteFeatureFlags: {
          assetsUnifyState: {
            enabled: true,
            featureVersion: '1',
            minimumVersion: null,
          },
        },
        appMetadata: undefined,
      },
      {
        description: 'returns true when app version satisfies minimumVersion',
        remoteFeatureFlags: {
          assetsUnifyState: {
            enabled: true,
            featureVersion: '1',
            minimumVersion: '1.0.0',
          },
        },
        appMetadata: {
          ...defaultAppMetadata,
          currentAppVersion: '2.0.0',
        },
      },
    ];

    const arrangeMocks = (
      remoteFeatureFlags: FeatureFlags | undefined,
      appMetadata: AppMetadataControllerState | undefined,
    ): void => {
      if (remoteFeatureFlags !== undefined) {
        getRemoteFeatureFlagControllerStateMock.mockReturnValue({
          ...getDefaultRemoteFeatureFlagControllerState(),
          remoteFeatureFlags,
        });
      }
      if (appMetadata !== undefined) {
        getAppMetadataControllerStateMock.mockReturnValue(appMetadata);
      }
    };

    it.each(failureCases)(
      '$description',
      ({ remoteFeatureFlags, appMetadata }: (typeof failureCases)[number]) => {
        arrangeMocks(remoteFeatureFlags, appMetadata);

        const result = getAssetsUnifyStateFeature(messenger);

        expect(result).toBe(false);
      },
    );

    it.each(successCases)(
      '$description',
      ({ remoteFeatureFlags, appMetadata }: (typeof successCases)[number]) => {
        arrangeMocks(remoteFeatureFlags, appMetadata);

        const result = getAssetsUnifyStateFeature(messenger);

        expect(result).toBe(true);
      },
    );
  });

it('returns false when assetsUnifyState is not set', () => {
const result = getAssetsUnifyStateFeature(messenger);

expect(result).toBe(false);
});

it('returns false when assetsUnifyState.enabled is false', () => {
getRemoteFeatureFlagControllerStateMock.mockReturnValue({
...getDefaultRemoteFeatureFlagControllerState(),
remoteFeatureFlags: {
assetsUnifyState: {
enabled: false,
featureVersion: '1',
minimumVersion: null,
},
},
});

const result = getAssetsUnifyStateFeature(messenger);

expect(result).toBe(false);
});

it('returns false when featureVersion does not match expected version', () => {
getRemoteFeatureFlagControllerStateMock.mockReturnValue({
...getDefaultRemoteFeatureFlagControllerState(),
remoteFeatureFlags: {
assetsUnifyState: {
enabled: true,
featureVersion: '2',
minimumVersion: null,
},
},
});

const result = getAssetsUnifyStateFeature(messenger);

expect(result).toBe(false);
});

it('returns true when minimumVersion is null', () => {
getRemoteFeatureFlagControllerStateMock.mockReturnValue({
...getDefaultRemoteFeatureFlagControllerState(),
remoteFeatureFlags: {
assetsUnifyState: {
enabled: true,
featureVersion: '1',
minimumVersion: null,
},
},
});

const result = getAssetsUnifyStateFeature(messenger);

expect(result).toBe(true);
});

it('returns false when app version does not satisfy minimumVersion', () => {
getRemoteFeatureFlagControllerStateMock.mockReturnValue({
...getDefaultRemoteFeatureFlagControllerState(),
remoteFeatureFlags: {
assetsUnifyState: {
enabled: true,
featureVersion: '1',
minimumVersion: '2.0.0',
},
},
});
getAppMetadataControllerStateMock.mockReturnValue({
currentAppVersion: '1.0.0',
previousAppVersion: '',
previousMigrationVersion: 0,
currentMigrationVersion: 0,
});

const result = getAssetsUnifyStateFeature(messenger);

expect(result).toBe(false);
});

it('returns true when app version satisfies minimumVersion', () => {
getRemoteFeatureFlagControllerStateMock.mockReturnValue({
...getDefaultRemoteFeatureFlagControllerState(),
remoteFeatureFlags: {
assetsUnifyState: {
enabled: true,
featureVersion: '1',
minimumVersion: '1.0.0',
},
},
});
getAppMetadataControllerStateMock.mockReturnValue({
currentAppVersion: '2.0.0',
previousAppVersion: '',
previousMigrationVersion: 0,
currentMigrationVersion: 0,
});

const result = getAssetsUnifyStateFeature(messenger);

expect(result).toBe(true);
});
});

describe('getStrategyOrder', () => {
it('returns default strategy order when none is set', () => {
const strategyOrder = getStrategyOrder(messenger);
Expand Down
65 changes: 64 additions & 1 deletion packages/transaction-pay-controller/src/utils/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Hex } from '@metamask/utils';
import { createModuleLogger } from '@metamask/utils';
import {
createModuleLogger,
isValidSemVerRange,
isValidSemVerVersion,
satisfiesVersionRange,
} from '@metamask/utils';
import { uniq } from 'lodash';

import type { TransactionPayControllerMessenger } from '..';
Expand Down Expand Up @@ -297,6 +302,64 @@ export function getSlippage(
return slippage;
}

/**
* Get the AssetsUnifyState feature flag state.
*
* @param messenger - Controller messenger.
* @returns True if the assets unify state feature is enabled, false otherwise.
*/
export function getAssetsUnifyStateFeature(
messenger: TransactionPayControllerMessenger,
): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ if we want to test this , what is the way to turn this to true ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently only controlled by remote feature flags, unsure how much work would be required if we want to force to true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By doing black magic cjs updates in the client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With that said, if that cjs change is done with a patch and directly at the feature flag controller, it can be used to test selector changes too.

const state = messenger.call('RemoteFeatureFlagController:getState');
const assetsUnifyState = state.remoteFeatureFlags.assetsUnifyState as
| {
enabled: boolean;
featureVersion: string | null;
minimumVersion: string | null;
}
| undefined;

if (!assetsUnifyState?.enabled) {
return false;
}

const AssetsUnifyStateFeatureVersion = '1';

return (
assetsUnifyState.featureVersion === AssetsUnifyStateFeatureVersion &&
hasMinimumRequiredVersion(messenger, assetsUnifyState.minimumVersion)
);
}

/**
* Check if the app version satisfies the minimum required version.
*
* @param messenger - Controller messenger.
* @param minRequiredVersion - The minimum required version.
* @returns True if the app version satisfies the minimum required version, false otherwise.
*/
function hasMinimumRequiredVersion(
messenger: TransactionPayControllerMessenger,
minRequiredVersion: string | null,
): boolean {
if (!minRequiredVersion) {
return true;
}

const appVersion = messenger.call(
'AppMetadataController:getState',
)?.currentAppVersion;

const semverRange = `>=${minRequiredVersion}`;

return (
isValidSemVerVersion(appVersion) &&
isValidSemVerRange(semverRange) &&
satisfiesVersionRange(appVersion, semverRange)
);
}

/**
* Get a value from a record using a case-insensitive key lookup.
*
Expand Down
Loading
Loading