1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02:00

[MMI] Move mmi actions to extension (#18057)

* MMI adds actions and background files to the institution/ folder

* MMI lint fix

* MMI lint fix

* MMI import path fixed to be relative

* MMI import path fixed

* MMI adds the relative path to isErrorWithMessage

* MMI adds the tests for mmi actions

* MMI lint fix

* adds tests to mmi actions

* prettier fix

* MMI prettier and adds test

* MMI prettier

* MMI lint fix

* MMI prettier fix

* MMI rename folder
This commit is contained in:
António Regadas 2023-03-14 10:57:05 +00:00 committed by GitHub
parent ada47802b3
commit c022d2eb9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 836 additions and 1 deletions

View File

@ -308,6 +308,10 @@ interface DappSuggestedGasFees {
* An object representing a transaction, in whatever state it is in.
*/
export interface TransactionMeta {
///: BEGIN:ONLY_INCLUDE_IN(mmi)
custodyStatus: string;
custodyId?: string;
///: END:ONLY_INCLUDE_IN
/**
* The block number this transaction was included in. Currently only present
* on incoming transactions!

View File

@ -86,6 +86,12 @@ import { TxParams } from '../../app/scripts/controllers/transactions/tx-state-ma
import { CustomGasSettings } from '../../app/scripts/controllers/transactions';
import { ThemeType } from '../../shared/constants/preferences';
import * as actionConstants from './actionConstants';
///: BEGIN:ONLY_INCLUDE_IN(mmi)
import {
checkForUnapprovedTypedMessages,
updateCustodyState,
} from './institutional/institution-actions';
///: END:ONLY_INCLUDE_IN
import {
generateActionId,
callBackgroundMethod,
@ -713,6 +719,11 @@ export function signPersonalMsg(
}
dispatch(updateMetamaskState(newState));
///: BEGIN:ONLY_INCLUDE_IN(mmi)
if (newState.unapprovedTypedMessages) {
return checkForUnapprovedTypedMessages(msgData, newState);
}
///: END:ONLY_INCLUDE_IN
dispatch(completedTx(msgData.metamaskId));
dispatch(closeCurrentNotificationWindow());
return msgData;
@ -840,6 +851,11 @@ export function signTypedMsg(
}
dispatch(updateMetamaskState(newState));
///: BEGIN:ONLY_INCLUDE_IN(mmi)
if (newState.unapprovedTypedMessages) {
return checkForUnapprovedTypedMessages(msgData, newState);
}
///: END:ONLY_INCLUDE_IN
dispatch(completedTx(msgData.metamaskId));
dispatch(closeCurrentNotificationWindow());
return msgData;
@ -1848,6 +1864,10 @@ export function updateMetamaskState(
dispatch(initializeSendState({ chainHasChanged: true }));
}
///: BEGIN:ONLY_INCLUDE_IN(mmi)
updateCustodyState(dispatch, newState, getState());
///: END:ONLY_INCLUDE_IN
};
}

View File

@ -0,0 +1,338 @@
import sinon from 'sinon';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import MetaMaskController from '../../../app/scripts/metamask-controller';
import { _setBackgroundConnection } from '../action-queue';
import {
showInteractiveReplacementTokenModal,
showCustodyConfirmLink,
checkForUnapprovedTypedMessages,
updateCustodyState,
} from './institution-actions';
const middleware = [thunk];
const defaultState = {
metamask: {
currentLocale: 'test',
selectedAddress: '0xFirstAddress',
provider: { chainId: '0x1' },
accounts: {
'0xFirstAddress': {
balance: '0x0',
},
},
identities: {
'0xFirstAddress': {},
},
cachedBalances: {
'0x1': {
'0xFirstAddress': '0x0',
},
},
custodyStatusMaps: {
saturn: {
signed: {
mmStatus: 'signed',
shortText: 'signed',
longText: 'signed',
finished: false,
},
},
},
currentNetworkTxList: [
{
id: 0,
time: 0,
txParams: {
from: '0xAddress',
to: '0xRecipient',
},
custodyId: '0',
custodyStatus: 'signed',
},
{
id: 1,
time: 1,
txParams: {
from: '0xAddress',
to: '0xRecipient',
},
custodyId: '1',
custodyStatus: 'signed',
},
],
custodyAccountDetails: {
'0xAddress': {
address: '0xc96348083d806DFfc546b36e05AF1f9452CDAe91',
details: 'details',
custodyType: 'testCustody - Saturn',
},
},
},
appState: {
modal: {
open: true,
modalState: {
name: 'CUSTODY_CONFIRM_LINK',
props: {
custodyId: '1',
},
},
},
},
};
const mockStore = (state = defaultState) => configureStore(middleware)(state);
const baseMockState = defaultState.metamask;
describe('#InstitutionActions', () => {
let background;
beforeEach(async () => {
background = sinon.createStubInstance(MetaMaskController, {
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
});
afterEach(() => {
sinon.restore();
});
it('calls showModal with the property name of showInteractiveReplacementTokenModal', async () => {
const store = mockStore();
background.getApi.returns({
setFeatureFlag: sinon
.stub()
.callsFake((_, __, cb) => cb(new Error('error'))),
});
_setBackgroundConnection(background.getApi());
const expectedActions = [
{
type: 'UI_MODAL_OPEN',
payload: { name: 'INTERACTIVE_REPLACEMENT_TOKEN_MODAL' },
},
];
await store.dispatch(showInteractiveReplacementTokenModal());
expect(store.getActions()).toStrictEqual(expectedActions);
});
it('calls showModal with the property name of showCustodyConfirmLink', async () => {
const store = mockStore();
background.getApi.returns({
setFeatureFlag: sinon
.stub()
.callsFake((_, __, cb) => cb(new Error('error'))),
});
_setBackgroundConnection(background.getApi());
const expectedActions = [
{
type: 'UI_MODAL_OPEN',
payload: {
name: 'CUSTODY_CONFIRM_LINK',
link: 'link',
address: '0x1',
closeNotification: false,
custodyId: 'custodyId',
},
},
];
await store.dispatch(
showCustodyConfirmLink('link', '0x1', false, 'custodyId'),
);
expect(store.getActions()).toStrictEqual(expectedActions);
});
});
describe('#checkForUnapprovedTypedMessages', () => {
it('calls checkForUnapprovedTypedMessages and returns the messageData', async () => {
const messageData = {
id: 1,
type: 'tx',
msgParams: {
metamaskId: 2,
data: '0x1',
},
custodyId: '123',
status: 'unapproved',
};
expect(
checkForUnapprovedTypedMessages(messageData, {
unapprovedTypedMessages: { msg: 'msg' },
}),
).toBe(messageData);
});
});
describe('#updateCustodyState', () => {
let background;
beforeEach(async () => {
background = sinon.createStubInstance(MetaMaskController, {
getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
});
afterEach(() => {
sinon.restore();
});
it('calls updateCustodyState but returns early undefined', async () => {
const store = mockStore();
background.getApi.returns({
setFeatureFlag: sinon
.stub()
.callsFake((_, __, cb) => cb(new Error('error'))),
});
_setBackgroundConnection(background.getApi());
const newState = {
provider: {
nickname: 'mainnet',
chainId: '0x1',
},
featureFlags: {
showIncomingTransactions: false,
},
selectedAddress: '0xAddress',
};
const custodyState = updateCustodyState(store.dispatch, newState, newState);
expect(custodyState).toBe(undefined);
});
it('calls updateCustodyState and returns the hideModal', async () => {
const store = mockStore();
background.getApi.returns({
setFeatureFlag: sinon
.stub()
.callsFake((_, __, cb) => cb(new Error('error'))),
});
_setBackgroundConnection(background.getApi());
const newState = {
provider: {
nickname: 'mainnet',
chainId: '0x1',
},
featureFlags: {
showIncomingTransactions: false,
},
selectedAddress: '0xAddress',
currentNetworkTxList: [
{
id: 0,
time: 0,
txParams: {
from: '0xAddress',
to: '0xRecipient',
},
custodyId: '0',
custodyStatus: 'approved',
},
{
id: 1,
time: 1,
txParams: {
from: '0xAddress',
to: '0xRecipient',
},
custodyId: '1',
custodyStatus: 'approved',
},
],
};
const expectedActions = [
{
type: 'UI_MODAL_CLOSE',
},
];
updateCustodyState(store.dispatch, newState, defaultState);
expect(store.getActions()).toStrictEqual(expectedActions);
});
it('calls updateCustodyState and closes INTERACTIVE_REPLACEMENT_TOKEN_MODAL', async () => {
const store = mockStore();
background.getApi.returns({
setFeatureFlag: sinon
.stub()
.callsFake((_, __, cb) => cb(new Error('error'))),
});
_setBackgroundConnection(background.getApi());
const newState = {
provider: {
nickname: 'mainnet',
chainId: '0x1',
},
featureFlags: {
showIncomingTransactions: false,
},
selectedAddress: '0xAddress',
currentNetworkTxList: [
{
id: 0,
time: 0,
txParams: {
from: '0xAddress',
to: '0xRecipient',
},
custodyId: '0',
custodyStatus: 'approved',
},
{
id: 1,
time: 1,
txParams: {
from: '0xAddress',
to: '0xRecipient',
},
custodyId: '1',
custodyStatus: 'approved',
},
],
};
const customState = {
...defaultState,
appState: {
modal: {
open: true,
modalState: {
name: 'INTERACTIVE_REPLACEMENT_TOKEN_MODAL',
props: {
custodyId: '1',
closeNotification: true,
},
},
},
},
};
const closedNotification = updateCustodyState(
store.dispatch,
newState,
customState,
);
expect(closedNotification).toBe(undefined);
});
});

View File

@ -0,0 +1,130 @@
import { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import {
closeCurrentNotificationWindow,
hideModal,
showModal,
} from '../actions';
import {
CombinedBackgroundAndReduxState,
MetaMaskReduxState,
TemporaryMessageDataType,
} from '../store';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
export function showInteractiveReplacementTokenModal(): ThunkAction<
void,
MetaMaskReduxState,
unknown,
AnyAction
> {
return (dispatch) => {
dispatch(
showModal({
name: 'INTERACTIVE_REPLACEMENT_TOKEN_MODAL',
}),
);
};
}
export function showCustodyConfirmLink(
link: string,
address: string,
closeNotification: boolean,
custodyId: string,
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return (dispatch) => {
dispatch(
showModal({
name: 'CUSTODY_CONFIRM_LINK',
link,
address,
closeNotification,
custodyId,
}),
);
};
}
export function updateCustodyState(
dispatch: ThunkDispatch<CombinedBackgroundAndReduxState, unknown, AnyAction>,
newState: MetaMaskReduxState['metamask'],
state: CombinedBackgroundAndReduxState & any,
) {
if (!newState.currentNetworkTxList || !state.metamask.currentNetworkTxList) {
return;
}
const differentTxs = newState.currentNetworkTxList.filter(
(item) =>
state.metamask.currentNetworkTxList.filter(
(tx: { [key: string]: any }) =>
tx.custodyId === item.custodyId &&
tx.custodyStatus !== item.custodyStatus,
).length > 0,
);
const txStateSaysDeepLinkShouldClose = Boolean(
differentTxs.find((tx) => {
const custodyAccountDetails =
state.metamask.custodyAccountDetails[
toChecksumHexAddress(tx.txParams.from)
];
const custody = custodyAccountDetails?.custodyType
.split(' - ')[1]
.toLowerCase();
if (!custody) {
return false;
}
return (
tx.custodyId === state.appState.modal.modalState.props?.custodyId &&
(state.metamask.custodyStatusMaps[custody][tx.custodyStatus]
?.mmStatus !== 'approved' ||
tx.custodyStatus === 'created')
);
}),
);
if (
state.appState.modal.open &&
state.appState.modal.modalState.name === 'CUSTODY_CONFIRM_LINK' &&
txStateSaysDeepLinkShouldClose
) {
if (state.appState.modal.modalState.props?.closeNotification) {
dispatch(closeCurrentNotificationWindow());
}
dispatch(hideModal());
}
if (
state.appState.modal.open &&
state.appState.modal.modalState.name ===
'INTERACTIVE_REPLACEMENT_TOKEN_MODAL'
) {
if (state.appState.modal.modalState.props?.closeNotification) {
dispatch(closeCurrentNotificationWindow());
}
}
}
export function checkForUnapprovedTypedMessages(
msgData: TemporaryMessageDataType['msgParams'],
newState: MetaMaskReduxState['metamask'],
) {
const custodianUnapprovedMessages = Object.keys(
newState.unapprovedTypedMessages,
)
.map((key) => newState.unapprovedTypedMessages[key])
.filter((message) => message.custodyId && message.status === 'unapproved');
if (custodianUnapprovedMessages && custodianUnapprovedMessages.length > 0) {
return {
...msgData,
custodyId:
newState.unapprovedTypedMessages[msgData.metamaskId]?.custodyId,
};
}
return msgData;
}

View File

@ -0,0 +1,125 @@
import { mmiActionsFactory } from './institution-background';
describe('Institution Actions', () => {
describe('#mmiActionsFactory', () => {
it('returns mmiActions object', async () => {
const actionsMock = {
connectCustodyAddresses: jest.fn(),
getCustodianAccounts: jest.fn(),
getCustodianAccountsByAddress: jest.fn(),
getCustodianTransactionDeepLink: jest.fn(),
getCustodianConfirmDeepLink: jest.fn(),
getCustodianSignMessageDeepLink: jest.fn(),
getCustodianToken: jest.fn(),
getCustodianJWTList: jest.fn(),
setComplianceAuthData: jest.fn(),
deleteComplianceAuthData: jest.fn(),
generateComplianceReport: jest.fn(),
getComplianceHistoricalReportsByAddress: jest.fn(),
syncReportsInProgress: jest.fn(),
removeConnectInstitutionalFeature: jest.fn(),
removeAddTokenConnectRequest: jest.fn(),
setCustodianConnectRequest: jest.fn(),
getCustodianConnectRequest: jest.fn(),
getMmiConfiguration: jest.fn(),
getAllCustodianAccountsWithToken: jest.fn(),
setWaitForConfirmDeepLinkDialog: jest.fn(),
setCustodianNewRefreshToken: jest.fn(),
};
const mmiActions = mmiActionsFactory({
log: { debug: jest.fn(), error: jest.fn() },
showLoadingIndication: jest.fn(),
submitRequestToBackground: jest.fn(() => actionsMock),
displayWarning: jest.fn(),
hideLoadingIndication: jest.fn(),
forceUpdateMetamaskState: jest.fn(),
showModal: jest.fn(),
callBackgroundMethod: jest.fn(() => actionsMock),
});
const connectCustodyAddresses = mmiActions.connectCustodyAddresses(
{},
'0xAddress',
);
mmiActions.getCustodianAccounts(
'token',
'apiUrl',
'custody',
'getNonImportedAccounts',
{},
);
mmiActions.getCustodianAccountsByAddress(
'jwt',
'apiUrl',
'address',
'custody',
{},
4,
);
mmiActions.getMmiConfiguration({
portfolio: {
enabled: true,
url: 'https://portfolio.io',
},
custodians: [],
});
mmiActions.getCustodianToken({});
mmiActions.getCustodianConnectRequest({
token: 'token',
custodianType: 'custodianType',
custodianName: 'custodianname',
apiUrl: undefined,
});
mmiActions.getCustodianTransactionDeepLink('0xAddress', 'txId');
mmiActions.getCustodianConfirmDeepLink('txId');
mmiActions.getCustodianSignMessageDeepLink('0xAddress', 'custodyTxId');
mmiActions.getCustodianJWTList({});
mmiActions.getAllCustodianAccountsWithToken({
custodianType: 'custodianType',
token: 'token',
});
mmiActions.setComplianceAuthData({
clientId: 'id',
projectId: 'projectId',
});
mmiActions.deleteComplianceAuthData();
mmiActions.generateComplianceReport('0xAddress');
mmiActions.getComplianceHistoricalReportsByAddress(
'0xAddress',
'projectId',
);
mmiActions.syncReportsInProgress({
address: '0xAddress',
historicalReports: [],
});
mmiActions.removeConnectInstitutionalFeature({
origin: 'origin',
projectId: 'projectId',
});
mmiActions.removeAddTokenConnectRequest({
origin: 'origin',
apiUrl: 'https://jupiter-custody.codefi.network',
token: 'token',
});
mmiActions.setCustodianConnectRequest({
token: 'token',
apiUrl: 'https://jupiter-custody.codefi.network',
custodianType: 'custodianType',
custodianName: 'custodianname',
});
const setWaitForConfirmDeepLinkDialog =
mmiActions.setWaitForConfirmDeepLinkDialog(true);
mmiActions.setCustodianNewRefreshToken(
'address',
'oldAuthDetails',
'oldApiUrl',
'newAuthDetails',
'newApiUrl',
);
connectCustodyAddresses(jest.fn());
setWaitForConfirmDeepLinkDialog(jest.fn());
expect(connectCustodyAddresses).toBeDefined();
expect(setWaitForConfirmDeepLinkDialog).toBeDefined();
});
});
});

View File

@ -0,0 +1,211 @@
import log from 'loglevel';
import { ThunkAction } from 'redux-thunk';
import { AnyAction } from 'redux';
import {
forceUpdateMetamaskState,
displayWarning,
hideLoadingIndication,
showLoadingIndication,
} from '../actions';
import {
callBackgroundMethod,
submitRequestToBackground,
} from '../action-queue';
import { MetaMaskReduxState } from '../store';
import { isErrorWithMessage } from '../../../shared/modules/error';
export function showInteractiveReplacementTokenBanner(
url: string,
oldRefreshToken: string,
) {
return () => {
callBackgroundMethod(
'showInteractiveReplacementTokenBanner',
[url, oldRefreshToken],
(err) => {
if (isErrorWithMessage(err)) {
throw new Error(err.message);
}
},
);
};
}
/**
* A factory that contains all MMI actions ready to use
* Example usage:
* const mmiActions = mmiActionsFactory();
* mmiActions.connectCustodyAddresses(...)
*/
export function mmiActionsFactory() {
function createAsyncAction(
name: string,
params: any,
useForceUpdateMetamaskState?: any,
loadingText?: string,
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
log.debug(`background.${name}`);
return async (dispatch) => {
if (loadingText) {
dispatch(showLoadingIndication(loadingText));
}
let result;
try {
result = await submitRequestToBackground(name, [...params]);
} catch (error) {
dispatch(displayWarning(error));
if (isErrorWithMessage(error)) {
throw new Error(error.message);
} else {
throw error;
}
}
if (loadingText) {
dispatch(hideLoadingIndication());
}
if (useForceUpdateMetamaskState) {
await forceUpdateMetamaskState(dispatch);
}
return result;
};
}
function createAction(name: string, payload: any) {
return () => {
callBackgroundMethod(name, [payload], (err) => {
if (isErrorWithMessage(err)) {
throw new Error(err.message);
}
});
};
}
return {
connectCustodyAddresses: (
custodianType: string,
custodianName: string,
newAccounts: string[],
) =>
createAsyncAction(
'connectCustodyAddresses',
[custodianType, custodianName, newAccounts],
forceUpdateMetamaskState,
'Looking for your custodian account...',
),
getCustodianAccounts: (
token: string,
apiUrl: string,
custody: string,
getNonImportedAccounts: boolean,
) =>
createAsyncAction(
'getCustodianAccounts',
[token, apiUrl, custody, getNonImportedAccounts],
forceUpdateMetamaskState,
'Getting custodian accounts...',
),
getCustodianAccountsByAddress: (
jwt: string,
apiUrl: string,
address: string,
custody: string,
) =>
createAsyncAction(
'getCustodianAccountsByAddress',
[jwt, apiUrl, address, custody],
forceUpdateMetamaskState,
'Getting custodian accounts...',
),
getCustodianTransactionDeepLink: (address: string, txId: string) =>
createAsyncAction(
'getCustodianTransactionDeepLink',
[address, txId],
forceUpdateMetamaskState,
),
getCustodianConfirmDeepLink: (txId: string) =>
createAsyncAction(
'getCustodianConfirmDeepLink',
[txId],
forceUpdateMetamaskState,
),
getCustodianSignMessageDeepLink: (from: string, custodyTxId: string) =>
createAsyncAction(
'getCustodianSignMessageDeepLink',
[from, custodyTxId],
forceUpdateMetamaskState,
),
getCustodianToken: (custody: string) =>
createAsyncAction(
'getCustodianToken',
[custody],
forceUpdateMetamaskState,
),
getCustodianJWTList: (custody: string) =>
createAsyncAction(
'getCustodianJWTList',
[custody],
forceUpdateMetamaskState,
),
setWaitForConfirmDeepLinkDialog: (waitForConfirmDeepLinkDialog: boolean) =>
createAction(
'setWaitForConfirmDeepLinkDialog',
waitForConfirmDeepLinkDialog,
),
setComplianceAuthData: (clientId: string, projectId: string) =>
createAsyncAction('setComplianceAuthData', [{ clientId, projectId }]),
deleteComplianceAuthData: () =>
createAsyncAction('deleteComplianceAuthData', []),
generateComplianceReport: (address: string) =>
createAction('generateComplianceReport', address),
getComplianceHistoricalReportsByAddress: (
address: string,
projectId: string,
) =>
createAsyncAction('getComplianceHistoricalReportsByAddress', [
address,
projectId,
]),
syncReportsInProgress: (address: string, historicalReports: []) =>
createAction('syncReportsInProgress', { address, historicalReports }),
removeConnectInstitutionalFeature: (origin: string, projectId: string) =>
createAction('removeConnectInstitutionalFeature', { origin, projectId }),
removeAddTokenConnectRequest: (
origin: string,
apiUrl: string,
token: string,
) =>
createAction('removeAddTokenConnectRequest', { origin, apiUrl, token }),
setCustodianConnectRequest: (
token: string,
apiUrl: string,
custodianType: string,
custodianName: string,
) =>
createAsyncAction('setCustodianConnectRequest', [
{ token, apiUrl, custodianType, custodianName },
]),
getCustodianConnectRequest: () =>
createAsyncAction('getCustodianConnectRequest', []),
getMmiConfiguration: () => createAsyncAction('getMmiConfiguration', []),
getAllCustodianAccountsWithToken: (custodyType: string, token: string) =>
createAsyncAction('getAllCustodianAccountsWithToken', [
custodyType,
token,
]),
setCustodianNewRefreshToken: (
address: string,
oldAuthDetails: string,
oldApiUrl: string,
newAuthDetails: string,
newApiUrl: string,
) =>
createAsyncAction('setCustodianNewRefreshToken', [
address,
oldAuthDetails,
oldApiUrl,
newAuthDetails,
newApiUrl,
]),
};
}

View File

@ -22,6 +22,10 @@ export interface TemporaryMessageDataType {
metamaskId: number;
data: string;
};
///: BEGIN:ONLY_INCLUDE_IN(mmi)
custodyId?: string;
status?: string;
///: END:ONLY_INCLUDE_IN
}
interface MessagesIndexedById {
@ -68,11 +72,14 @@ interface TemporaryBackgroundState {
};
gasFeeEstimates: GasFeeEstimates;
gasEstimateType: GasEstimateType;
///: BEGIN:ONLY_INCLUDE_IN(mmi)
custodyAccountDetails?: { [key: string]: any };
///: END:ONLY_INCLUDE_IN
}
type RootReducerReturnType = ReturnType<typeof rootReducer>;
type CombinedBackgroundAndReduxState = RootReducerReturnType & {
export type CombinedBackgroundAndReduxState = RootReducerReturnType & {
activeTab: {
origin: string;
};