1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Merge pull request #20588 from MetaMask/Version-v10.35.1

Version 10.35.1
This commit is contained in:
Mark Stacey 2023-08-31 15:11:34 -02:30 committed by GitHub
commit a7b4c20f32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1718 additions and 615 deletions

View File

@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [10.35.1]
### Changed
- Store default gas settings by network ([#20576](https://github.com/MetaMask/metamask-extension/pull/20576), [#20632](https://github.com/MetaMask/metamask-extension/pull/20632))
- Add more diagnostic information upon failure ([#20595](https://github.com/MetaMask/metamask-extension/pull/20595))
### Fixed
- Fix bug resulting in custom network configuration being lost upon restart ([#20586](https://github.com/MetaMask/metamask-extension/pull/20586))
- Fix UI crash when balances are missing ([#20385](https://github.com/MetaMask/metamask-extension/pull/20385))
- Fix infinite rerender on network change while signature request is pending ([#20473](https://github.com/MetaMask/metamask-extension/pull/20473))
- Fix Dapp link on NFT import screen ([#19799](https://github.com/MetaMask/metamask-extension/pull/19799))
- Fix 'View on Opensea' link for main and testnet NFTs ([#19797](https://github.com/MetaMask/metamask-extension/pull/19797))
- Ensure chainId comparison in switchEthereumChain handler is case insensitive ([#20149](https://github.com/MetaMask/metamask-extension/pull/20149))
- Enforce user preferences in incoming transactions controller ([#19982](https://github.com/MetaMask/metamask-extension/pull/19982))
## [10.35.0]
### Added
- Add the ability to customize tx nonce on ERC20 approval screens ([#17945](https://github.com/MetaMask/metamask-extension/pull/17945))
@ -3946,7 +3960,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized
- Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.35.0...HEAD
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.35.1...HEAD
[10.35.1]: https://github.com/MetaMask/metamask-extension/compare/v10.35.0...v10.35.1
[10.35.0]: https://github.com/MetaMask/metamask-extension/compare/v10.34.5...v10.35.0
[10.34.5]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...v10.34.5
[10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Erweiterte Einstellungen"
},
"advancedGasFeeDefaultOptIn": {
"message": "Speichern Sie diese $1 als Standard für \"Erweitert\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Immer diese Werte und erweiterte Einstellung als Standard verwenden."
},
"advancedGasFeeModalTitle": {
"message": "Erweiterte Gasgebühr"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Token importiert"
},
"newValues": {
"message": "neue Werte"
},
"next": {
"message": "Weiter"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Προηγμένη ρύθμιση παραμέτρων"
},
"advancedGasFeeDefaultOptIn": {
"message": "Αποθηκεύστε αυτά τα $1 ως προεπιλογή μου για το \"Προηγμένο\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Να χρησιμοποιούνται πάντα αυτές τις τιμές και η ρύθμιση για προχωρημένους."
},
"advancedGasFeeModalTitle": {
"message": "Προηγμένη χρέωση τελών συναλλαγής"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Τα token εισήχθησαν"
},
"newValues": {
"message": "νέες τιμές"
},
"next": {
"message": "Επόμενο"
},

View File

@ -306,10 +306,8 @@
"message": "Advanced configuration"
},
"advancedGasFeeDefaultOptIn": {
"message": "Save these $1 as my default for \"Advanced\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Always use these values and advanced setting as default."
"message": "Save these values as my default for the $1 network.",
"description": "$1 is the current network name."
},
"advancedGasFeeModalTitle": {
"message": "Advanced gas fee"
@ -2483,9 +2481,6 @@
"newTokensImportedTitle": {
"message": "Token imported"
},
"newValues": {
"message": "new values"
},
"next": {
"message": "Next"
},
@ -2752,6 +2747,15 @@
"notifications22Title": {
"message": "Looking for your account details or the block explorer URL?"
},
"notifications24ActionText": {
"message": "Got it"
},
"notifications24Description": {
"message": "Advanced gas fee settings are now remembered based on the network you're using. This means you can set specific advanced gas fees for each network and avoid overpaying for gas or stuck transactions."
},
"notifications24Title": {
"message": "Advanced gas fees by network"
},
"notifications3ActionText": {
"message": "Read more",
"description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a page about security on the metamask support website."

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Configuración avanzada"
},
"advancedGasFeeDefaultOptIn": {
"message": "Guarda estos 1$ como mi valor predeterminado para \"Avanzado\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Usar siempre estos valores y la configuración avanzada como valores predeterminados."
},
"advancedGasFeeModalTitle": {
"message": "Tarifa de gas avanzada"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Token importado"
},
"newValues": {
"message": "nuevos valores"
},
"next": {
"message": "Siguiente"
},

View File

@ -148,12 +148,6 @@
"advancedBaseGasFeeToolTip": {
"message": "Cuando su transacción se incluya en el bloque, se reembolsará cualquier diferencia entre su tarifa base máxima y la tarifa base real. El importe total se calcula como tarifa base máxima (en GWEI) * límite de gas."
},
"advancedGasFeeDefaultOptIn": {
"message": "Guarda estos 1$ como mi valor predeterminado para \"Avanzado\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Usar siempre estos valores y la configuración avanzada como valores predeterminados."
},
"advancedGasFeeModalTitle": {
"message": "Tarifa de gas avanzada"
},
@ -1431,9 +1425,6 @@
"newPassword": {
"message": "Contraseña nueva (mín. de 8 caracteres)"
},
"newValues": {
"message": "nuevos valores"
},
"next": {
"message": "Siguiente"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Configuration avancée"
},
"advancedGasFeeDefaultOptIn": {
"message": "Enregistrer ces $1 comme valeur par défaut pour «Avancé»"
},
"advancedGasFeeDefaultOptOut": {
"message": "Toujours utiliser par défaut ces valeurs et les paramètres avancés."
},
"advancedGasFeeModalTitle": {
"message": "Frais de carburant avancés"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Jeton importé"
},
"newValues": {
"message": "nouvelles valeurs"
},
"next": {
"message": "Suivant"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "उन्नत कंफिगुरेशन"
},
"advancedGasFeeDefaultOptIn": {
"message": "इन $1 को \"एडवांस\" के लिए मेरे डिफॉल्ट के रूप में सहेजें"
},
"advancedGasFeeDefaultOptOut": {
"message": "हमेशा इन मूल्यों और एडवांस सेटिंग को डिफॉल्ट के रूप में उपयोग करें।"
},
"advancedGasFeeModalTitle": {
"message": "एडवांस गैस शुल्क"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "टोकन इम्पोर्ट हो गया"
},
"newValues": {
"message": "नए मान"
},
"next": {
"message": "अगला"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Konfigurasi lanjutan"
},
"advancedGasFeeDefaultOptIn": {
"message": "Simpan $1 ini sebagai default saya untuk \"Lanjutan\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Selalu gunakan nilai ini dan pengaturan lanjutan sebagai default."
},
"advancedGasFeeModalTitle": {
"message": "Biaya gas lanjutan"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Token diimpor"
},
"newValues": {
"message": "nilai baru"
},
"next": {
"message": "Berikutnya"
},

View File

@ -213,12 +213,6 @@
"advancedBaseGasFeeToolTip": {
"message": "Quando la tua transazione viene inclusa nel blocco, ogni differenza tra la tua offerta massima di gas e il gas effettivamente utilizzato viene restituita a te. Il totale viene calcolato come offerta massima di gas (in GEWI) * limite di gas."
},
"advancedGasFeeDefaultOptIn": {
"message": "Salva queste $1 come mie preferite per \"Avanzate\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Utilizzare sempre questi valori e l'impostazione avanzata come predefiniti."
},
"advancedGasFeeModalTitle": {
"message": "Tariffa gas avanzata"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "詳細設定"
},
"advancedGasFeeDefaultOptIn": {
"message": "これらの$1を「高度な設定」のデフォルトとして保存"
},
"advancedGasFeeDefaultOptOut": {
"message": "常にこれらの値と高度な設定をデフォルトとして使用します。"
},
"advancedGasFeeModalTitle": {
"message": "高度なガス代"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "トークンがインポートされました"
},
"newValues": {
"message": "新しい値"
},
"next": {
"message": "次へ"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "고급 옵션"
},
"advancedGasFeeDefaultOptIn": {
"message": "이 $1 옵션을 \"고급\"의 기본값으로 저장합니다"
},
"advancedGasFeeDefaultOptOut": {
"message": "항상 이 값과 고급 설정을 기본값으로 사용합니다."
},
"advancedGasFeeModalTitle": {
"message": "고급 가스 요금"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "불러온 토큰"
},
"newValues": {
"message": "새로운 가치"
},
"next": {
"message": "다음"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Configurações avançadas"
},
"advancedGasFeeDefaultOptIn": {
"message": "Salvar estes $1 como meu padrão para \"Avançado\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Sempre utilizar esses valores e a configuração avançada por padrão."
},
"advancedGasFeeModalTitle": {
"message": "Taxa de gás avançada"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Token importado"
},
"newValues": {
"message": "novos valores"
},
"next": {
"message": "Próximo"
},

View File

@ -148,12 +148,6 @@
"advancedBaseGasFeeToolTip": {
"message": "Quando a sua transação for incluída no bloco, qualquer diferença entre a sua taxa de base máxima e a taxa de base real será reembolsada. O cálculo do valor total é feito da seguinte forma: taxa de base máxima (em GWEI) * limite de gás."
},
"advancedGasFeeDefaultOptIn": {
"message": "Salvar estes $1 como meu padrão para \"Avançado\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Sempre utilizar esses valores e a configuração avançada por padrão."
},
"advancedGasFeeModalTitle": {
"message": "Taxa de gás avançada"
},
@ -1431,9 +1425,6 @@
"newPassword": {
"message": "Nova senha (no mínimo 8 caracteres)"
},
"newValues": {
"message": "novos valores"
},
"next": {
"message": "Seguinte"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Расширенная конфигурация"
},
"advancedGasFeeDefaultOptIn": {
"message": "Сохранить этот $1 в качестве моего значения по умолчанию для «Дополнительной» настройки"
},
"advancedGasFeeDefaultOptOut": {
"message": "Всегда использовать эти значения и дополнительную настройку по умолчанию."
},
"advancedGasFeeModalTitle": {
"message": "Дополнительная плата за газ"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Токен импортирован"
},
"newValues": {
"message": "новые значения"
},
"next": {
"message": "Далее"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Advanced na pagsasaayos"
},
"advancedGasFeeDefaultOptIn": {
"message": "I-save itong mga $1bilang aking default para sa \"Advanced\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Laging gamitin ang mga value na ito at advanced setting bilang default."
},
"advancedGasFeeModalTitle": {
"message": "Advanced na gas fee"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Na-import ang token"
},
"newValues": {
"message": "bagong value"
},
"next": {
"message": "Susunod"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Gelişmiş yapılandırma"
},
"advancedGasFeeDefaultOptIn": {
"message": "\"Gelişmiş\" için şunları varsayılanım olarak kaydet: $1"
},
"advancedGasFeeDefaultOptOut": {
"message": "Varsayılan olarak her zaman bu değerleri ve gelişmiş ayarı kullan."
},
"advancedGasFeeModalTitle": {
"message": "Gelişmiş gaz ücreti"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Token içe aktarıldı"
},
"newValues": {
"message": "yeni değerler"
},
"next": {
"message": "Sonraki"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "Cấu hình nâng cao"
},
"advancedGasFeeDefaultOptIn": {
"message": "Lưu $1 này làm mặc định của tôi cho \"Nâng cao\""
},
"advancedGasFeeDefaultOptOut": {
"message": "Luôn sử dụng các giá trị và thiết lập nâng cao này làm mặc định."
},
"advancedGasFeeModalTitle": {
"message": "Phí gas nâng cao"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "Đã nhập token"
},
"newValues": {
"message": "giá trị mới"
},
"next": {
"message": "Tiếp theo"
},

View File

@ -275,12 +275,6 @@
"advancedConfiguration": {
"message": "高级配置"
},
"advancedGasFeeDefaultOptIn": {
"message": "将这些 $1 保存为“高级”默认值"
},
"advancedGasFeeDefaultOptOut": {
"message": "始终使用这些值和高级设置作为默认值。"
},
"advancedGasFeeModalTitle": {
"message": "高级燃料费"
},
@ -2101,9 +2095,6 @@
"newTokensImportedTitle": {
"message": "已导入代币"
},
"newValues": {
"message": "新的值"
},
"next": {
"message": "下一步"
},

View File

@ -360,7 +360,7 @@ async function loadPhishingWarningPage() {
} catch (error) {
if (error instanceof PhishingWarningPageTimeoutError) {
console.warn(
'Phishing warning page timeout; page not guaraneteed to work offline.',
'Phishing warning page timeout; page not guaranteed to work offline.',
);
} else {
console.error('Failed to initialize phishing warning page', error);

View File

@ -49,6 +49,10 @@ export default class AppStateController extends EventEmitter {
showProductTour: true,
trezorModel: null,
currentPopupId: undefined,
// This key is only used for checking if the user had set advancedGasFee
// prior to Migration 92.3 where we split out the setting to support
// multiple networks.
hadAdvancedGasFeesSetPriorToMigration92_3: false,
...initState,
qrHardware: {},
nftsDropdownState: {},

View File

@ -135,15 +135,12 @@ export default class IncomingTransactionsController {
}
start() {
const { featureFlags = {} } = this.preferencesController.store.getState();
const { showIncomingTransactions } = featureFlags;
const chainId = this.getCurrentChainId();
if (!showIncomingTransactions) {
return;
if (this._allowedToMakeFetchIncomingTx(chainId)) {
this.blockTracker.removeListener('latest', this._onLatestBlock);
this.blockTracker.addListener('latest', this._onLatestBlock);
}
this.blockTracker.removeListener('latest', this._onLatestBlock);
this.blockTracker.addListener('latest', this._onLatestBlock);
}
stop() {
@ -161,13 +158,9 @@ export default class IncomingTransactionsController {
* @param {number} [newBlockNumberDec] - block number to begin fetching from
*/
async _update(address, newBlockNumberDec) {
const { completedOnboarding } = this.onboardingController.store.getState();
const chainId = this.getCurrentChainId();
if (
!Object.hasOwnProperty.call(ETHERSCAN_SUPPORTED_NETWORKS, chainId) ||
!address ||
!completedOnboarding
) {
if (!address || !this._allowedToMakeFetchIncomingTx(chainId)) {
return;
}
try {
@ -302,4 +295,26 @@ export default class IncomingTransactionsController {
type: TransactionType.incoming,
};
}
/**
* @param chainId - {string} The chainId of the current network
* @returns {boolean} Whether or not the user has consented to show incoming transactions
*/
_allowedToMakeFetchIncomingTx(chainId) {
const { featureFlags = {} } = this.preferencesController.store.getState();
const { completedOnboarding } = this.onboardingController.store.getState();
const hasIncomingTransactionsFeatureEnabled = Boolean(
featureFlags.showIncomingTransactions,
);
const isEtherscanSupportedNetwork = Boolean(
ETHERSCAN_SUPPORTED_NETWORKS[chainId],
);
return (
completedOnboarding &&
isEtherscanSupportedNetwork &&
hasIncomingTransactionsFeatureEnabled
);
}
}

View File

@ -78,11 +78,11 @@ function getMockPreferencesController({
};
}
function getMockOnboardingController() {
function getMockOnboardingController({ completedOnboarding = true } = {}) {
return {
store: {
getState: sinon.stub().returns({
completedOnboarding: true,
completedOnboarding,
}),
subscribe: sinon.spy(),
},
@ -98,6 +98,16 @@ function getMockBlockTracker() {
};
}
function getDefaultControllerOpts() {
return {
blockTracker: getMockBlockTracker(),
...getMockNetworkControllerMethods(CHAIN_IDS.GOERLI),
preferencesController: getMockPreferencesController(),
onboardingController: getMockOnboardingController(),
initState: getEmptyInitState(),
};
}
/**
* @typedef {import(
* '../../../../app/scripts/controllers/incoming-transactions'
@ -226,6 +236,7 @@ describe('IncomingTransactionsController', function () {
preferencesController: getMockPreferencesController(),
onboardingController: getMockOnboardingController(),
initState: {},
getCurrentChainId: () => CHAIN_IDS.GOERLI,
},
);
@ -831,6 +842,97 @@ describe('IncomingTransactionsController', function () {
});
});
describe('block explorer lookup', function () {
let sandbox;
beforeEach(function () {
sandbox = sinon.createSandbox();
});
afterEach(function () {
sandbox.restore();
});
function stubFetch() {
return sandbox.stub(window, 'fetch');
}
function assertStubNotCalled(stub) {
assert(stub.callCount === 0);
}
async function triggerUpdate(incomingTransactionsController) {
const subscription =
incomingTransactionsController.preferencesController.store.subscribe.getCall(
1,
).args[0];
// Sets address causing a call to _update
await subscription({ selectedAddress: MOCK_SELECTED_ADDRESS });
}
it('should not happen when incoming transactions feature is disabled', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
...getDefaultControllerOpts(),
preferencesController: getMockPreferencesController({
showIncomingTransactions: false,
}),
},
);
const fetchStub = stubFetch();
await triggerUpdate(incomingTransactionsController);
assertStubNotCalled(fetchStub);
});
it('should not happen when onboarding is in progress', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
...getDefaultControllerOpts(),
onboardingController: getMockOnboardingController({
completedOnboarding: false,
}),
},
);
const fetchStub = stubFetch();
await triggerUpdate(incomingTransactionsController);
assertStubNotCalled(fetchStub);
});
it('should not happen when chain id is not supported', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
...getDefaultControllerOpts(),
getCurrentChainId: () => FAKE_CHAIN_ID,
},
);
const fetchStub = stubFetch();
await triggerUpdate(incomingTransactionsController);
assertStubNotCalled(fetchStub);
});
it('should make api call when chain id, incoming features, and onboarding status are ok', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
...getDefaultControllerOpts(),
getCurrentChainId: () => CHAIN_IDS.GOERLI,
onboardingController: getMockOnboardingController({
completedOnboarding: true,
}),
preferencesController: getMockPreferencesController({
showIncomingTransactions: true,
}),
},
);
const fetchStub = stubFetch();
await triggerUpdate(incomingTransactionsController);
assert(fetchStub.callCount === 1);
});
});
describe('_update', function () {
describe('when state is empty (initialized)', function () {
it('should use provided block number and update the latest block seen', async function () {

View File

@ -41,7 +41,7 @@ export default class PreferencesController {
useNftDetection: false,
useCurrencyRateCheck: true,
openSeaEnabled: false,
advancedGasFee: null,
advancedGasFee: {},
// WARNING: Do not use feature flags for security-sensitive things.
// Feature flag toggling is available in the global namespace
@ -188,10 +188,18 @@ export default class PreferencesController {
/**
* Setter for the `advancedGasFee` property
*
* @param {object} val - holds the maxBaseFee and PriorityFee that the user set as default advanced settings.
* @param {object} options
* @param {string} options.chainId - The chainId the advancedGasFees should be set on
* @param {object} options.gasFeePreferences - The advancedGasFee options to set
*/
setAdvancedGasFee(val) {
this.store.updateState({ advancedGasFee: val });
setAdvancedGasFee({ chainId, gasFeePreferences }) {
const { advancedGasFee } = this.store.getState();
this.store.updateState({
advancedGasFee: {
...advancedGasFee,
[chainId]: gasFeePreferences,
},
});
}
/**

View File

@ -2,6 +2,7 @@ import { strict as assert } from 'assert';
import sinon from 'sinon';
import { ControllerMessenger } from '@metamask/base-controller';
import { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences';
describe('preferences controller', function () {
@ -250,24 +251,31 @@ describe('preferences controller', function () {
});
describe('setAdvancedGasFee', function () {
it('should default to null', function () {
const state = preferencesController.store.getState();
assert.equal(state.advancedGasFee, null);
it('should default to an empty object', function () {
assert.deepEqual(
preferencesController.store.getState().advancedGasFee,
{},
);
});
it('should set the setAdvancedGasFee property in state', function () {
const state = preferencesController.store.getState();
assert.equal(state.advancedGasFee, null);
assert.deepEqual(state.advancedGasFee, {});
preferencesController.setAdvancedGasFee({
maxBaseFee: '1.5',
priorityFee: '2',
chainId: CHAIN_IDS.GOERLI,
gasFeePreferences: {
maxBaseFee: '1.5',
priorityFee: '2',
},
});
assert.equal(
preferencesController.store.getState().advancedGasFee.maxBaseFee,
preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI]
.maxBaseFee,
'1.5',
);
assert.equal(
preferencesController.store.getState().advancedGasFee.priorityFee,
preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI]
.priorityFee,
'2',
);
});

View File

@ -1962,9 +1962,13 @@ export default class TransactionController extends EventEmitter {
*/
this.getTransactions = (opts) => this.txStateManager.getTransactions(opts);
/** @returns {object} the saved default values for advancedGasFee */
/**
* @returns {object} the saved default values for advancedGasFee
*/
this.getAdvancedGasFee = () =>
this.preferencesStore.getState().advancedGasFee;
this.preferencesStore.getState().advancedGasFee[
this._getCurrentChainId()
];
}
// called once on startup

View File

@ -59,6 +59,7 @@ describe('Transaction Controller', function () {
fromAccount,
fragmentExists,
networkStatusStore,
preferencesStore,
getCurrentChainId,
messengerMock,
resultCallbacksMock,
@ -81,6 +82,7 @@ describe('Transaction Controller', function () {
}).provider;
networkStatusStore = new ObservableStore(currentNetworkStatus);
preferencesStore = new ObservableStore({ advancedGasFee: {} });
fromAccount = getTestAccounts()[0];
const blockTrackerStub = new EventEmitter();
@ -129,6 +131,7 @@ describe('Transaction Controller', function () {
getAccountType: () => 'MetaMask',
getDeviceModel: () => 'N/A',
securityProviderRequest: () => undefined,
preferencesStore,
messenger: messengerMock,
});

View File

@ -116,7 +116,7 @@ async function switchEthereumChainHandler(
if (
Object.values(BUILT_IN_INFURA_NETWORKS)
.map(({ chainId: id }) => id)
.includes(chainId)
.includes(_chainId)
) {
await setProviderType(approvedRequestData.type);
} else {

View File

@ -0,0 +1,128 @@
import {
CHAIN_IDS,
NETWORK_TYPES,
} from '../../../../../shared/constants/network';
import switchEthereumChain from './switch-ethereum-chain';
const NON_INFURA_CHAIN_ID = '0x123456789';
const mockRequestUserApproval = ({ requestData }) => {
return Promise.resolve(requestData);
};
const MOCK_MAINNET_CONFIGURATION = {
id: 123,
chainId: CHAIN_IDS.MAINNET,
type: NETWORK_TYPES.MAINNET,
};
const MOCK_LINEA_MAINNET_CONFIGURATION = {
id: 123,
chainId: CHAIN_IDS.LINEA_MAINNET,
type: NETWORK_TYPES.LINEA_MAINNET,
};
describe('switchEthereumChainHandler', () => {
it('should call setProviderType when switching to a built in infura network', async () => {
const mockSetProviderType = jest.fn();
const mockSetActiveNetwork = jest.fn();
const switchEthereumChainHandler = switchEthereumChain.implementation;
await switchEthereumChainHandler(
{
origin: 'example.com',
params: [{ chainId: CHAIN_IDS.MAINNET }],
},
{},
jest.fn(),
jest.fn(),
{
getCurrentChainId: () => NON_INFURA_CHAIN_ID,
findNetworkConfigurationBy: () => MOCK_MAINNET_CONFIGURATION,
setProviderType: mockSetProviderType,
setActiveNetwork: mockSetActiveNetwork,
requestUserApproval: mockRequestUserApproval,
},
);
expect(mockSetProviderType).toHaveBeenCalledTimes(1);
expect(mockSetProviderType).toHaveBeenCalledWith(
MOCK_MAINNET_CONFIGURATION.type,
);
});
it('should call setProviderType when switching to a built in infura network, when chainId from request is lower case', async () => {
const mockSetProviderType = jest.fn();
const mockSetActiveNetwork = jest.fn();
const switchEthereumChainHandler = switchEthereumChain.implementation;
await switchEthereumChainHandler(
{
origin: 'example.com',
params: [{ chainId: CHAIN_IDS.LINEA_MAINNET.toLowerCase() }],
},
{},
jest.fn(),
jest.fn(),
{
getCurrentChainId: () => NON_INFURA_CHAIN_ID,
findNetworkConfigurationBy: () => MOCK_LINEA_MAINNET_CONFIGURATION,
setProviderType: mockSetProviderType,
setActiveNetwork: mockSetActiveNetwork,
requestUserApproval: mockRequestUserApproval,
},
);
expect(mockSetProviderType).toHaveBeenCalledTimes(1);
expect(mockSetProviderType).toHaveBeenCalledWith(
MOCK_LINEA_MAINNET_CONFIGURATION.type,
);
});
it('should call setProviderType when switching to a built in infura network, when chainId from request is upper case', async () => {
const mockSetProviderType = jest.fn();
const mockSetActiveNetwork = jest.fn();
const switchEthereumChainHandler = switchEthereumChain.implementation;
await switchEthereumChainHandler(
{
origin: 'example.com',
params: [{ chainId: CHAIN_IDS.LINEA_MAINNET.toUpperCase() }],
},
{},
jest.fn(),
jest.fn(),
{
getCurrentChainId: () => NON_INFURA_CHAIN_ID,
findNetworkConfigurationBy: () => MOCK_LINEA_MAINNET_CONFIGURATION,
setProviderType: mockSetProviderType,
setActiveNetwork: mockSetActiveNetwork,
requestUserApproval: mockRequestUserApproval,
},
);
expect(mockSetProviderType).toHaveBeenCalledTimes(1);
expect(mockSetProviderType).toHaveBeenCalledWith(
MOCK_LINEA_MAINNET_CONFIGURATION.type,
);
});
it('should call setActiveNetwork when switching to a custom network', async () => {
const mockSetProviderType = jest.fn();
const mockSetActiveNetwork = jest.fn();
const switchEthereumChainHandler = switchEthereumChain.implementation;
await switchEthereumChainHandler(
{
origin: 'example.com',
params: [{ chainId: NON_INFURA_CHAIN_ID }],
},
{},
jest.fn(),
jest.fn(),
{
getCurrentChainId: () => CHAIN_IDS.MAINNET,
findNetworkConfigurationBy: () => MOCK_MAINNET_CONFIGURATION,
setProviderType: mockSetProviderType,
setActiveNetwork: mockSetActiveNetwork,
requestUserApproval: mockRequestUserApproval,
},
);
expect(mockSetActiveNetwork).toHaveBeenCalledTimes(1);
expect(mockSetActiveNetwork).toHaveBeenCalledWith(
MOCK_MAINNET_CONFIGURATION.id,
);
});
});

View File

@ -1,6 +1,7 @@
import * as Sentry from '@sentry/browser';
import { Dedupe, ExtraErrorData } from '@sentry/integrations';
import { AllProperties } from '../../../shared/modules/object.utils';
import { FilterEvents } from './sentry-filter-events';
import extractEthjsErrorMessage from './extractEthjsErrorMessage';
@ -28,75 +29,254 @@ export const ERROR_URL_ALLOWLIST = {
// debugging, and they do not contain any identifiable information.
export const SENTRY_BACKGROUND_STATE = {
AccountTracker: {
accounts: false,
currentBlockGasLimit: true,
},
AddressBookController: {
addressBook: false,
},
AlertController: {
alertEnabledness: true,
unconnectedAccountAlertShownOrigins: false,
web3ShimUsageOrigins: false,
},
AnnouncementController: {
announcements: false,
},
AppMetadataController: {
currentAppVersion: true,
currentMigrationVersion: true,
previousAppVersion: true,
previousMigrationVersion: true,
currentMigrationVersion: true,
},
ApprovalController: {
approvalFlows: false,
pendingApprovals: false,
pendingApprovalCount: false,
},
AppStateController: {
browserEnvironment: true,
connectedStatusPopoverHasBeenShown: true,
currentPopupId: false,
defaultHomeActiveTabName: true,
fullScreenGasPollTokens: true,
hadAdvancedGasFeesSetPriorToMigration92_3: true,
nftsDetectionNoticeDismissed: true,
nftsDropdownState: true,
notificationGasPollTokens: true,
outdatedBrowserWarningLastShown: true,
popupGasPollTokens: true,
qrHardware: true,
recoveryPhraseReminderHasBeenShown: true,
recoveryPhraseReminderLastShown: true,
serviceWorkerLastActiveTime: true,
showBetaHeader: true,
showProductTour: true,
showTestnetMessageInDropdown: true,
snapsInstallPrivacyWarningShown: true,
termsOfUseLastAgreed: true,
timeoutMinutes: true,
trezorModel: true,
usedNetworks: true,
},
CachedBalancesController: {
cachedBalances: false,
},
CurrencyController: {
conversionDate: true,
conversionRate: true,
currentCurrency: true,
nativeCurrency: true,
pendingCurrentCurrency: true,
pendingNativeCurrency: true,
usdConversionRate: true,
},
DecryptMessageController: {
unapprovedDecryptMsgs: false,
unapprovedDecryptMsgCount: true,
},
DesktopController: {
desktopEnabled: true,
},
EncryptionPublicKeyController: {
unapprovedEncryptionPublicKeyMsgs: false,
unapprovedEncryptionPublicKeyMsgCount: true,
},
EnsController: {
ensResolutionsByAddress: false,
},
GasFeeController: {
estimatedGasFeeTimeBounds: true,
gasEstimateType: true,
gasFeeEstimates: true,
},
IncomingTransactionsController: {
incomingTransactions: false,
incomingTxLastFetchedBlockByChainId: true,
},
KeyringController: {
encryptionKey: false,
isUnlocked: true,
keyrings: false,
keyringTypes: false,
},
MetaMetricsController: {
eventsBeforeMetricsOptIn: false,
fragments: false,
metaMetricsId: true,
participateInMetaMetrics: true,
previousUserTraits: false,
segmentApiCalls: false,
traits: false,
},
NetworkController: {
networkConfigurations: false,
networkDetails: false,
networkId: true,
networkStatus: true,
providerConfig: {
chainId: true,
id: true,
nickname: true,
rpcPrefs: false,
rpcUrl: false,
ticker: true,
type: true,
},
},
NftController: {
allNftContracts: false,
allNfts: false,
ignoredNfts: false,
},
OnboardingController: {
completedOnboarding: true,
firstTimeFlowType: true,
onboardingTabs: false,
seedPhraseBackedUp: true,
},
PermissionController: {
subjects: false,
},
PermissionLogController: {
permissionActivityLog: false,
permissionHistory: false,
},
PhishingController: {},
PreferencesController: {
advancedGasFee: true,
currentLocale: true,
disabledRpcMethodPreferences: true,
dismissSeedBackUpReminder: true,
featureFlags: true,
forgottenPassword: true,
ipfsGateway: true,
preferences: true,
identities: false,
infuraBlocked: true,
ipfsGateway: false,
isLineaMainnetReleased: true,
knownMethodData: false,
ledgerTransportType: true,
lostIdentities: false,
openSeaEnabled: true,
preferences: {
autoLockTimeLimit: true,
hideZeroBalanceTokens: true,
showFiatInTestnets: true,
showTestNetworks: true,
useNativeCurrencyAsPrimaryCurrency: true,
},
selectedAddress: false,
snapRegistryList: false,
theme: true,
transactionSecurityCheckEnabled: true,
useBlockie: true,
useCurrencyRateCheck: true,
useMultiAccountBalanceChecker: true,
useNftDetection: true,
useNonceField: true,
usePhishDetect: true,
useTokenDetection: true,
},
SignatureController: {
unapprovedMsgCount: true,
unapprovedMsgs: false,
unapprovedPersonalMsgCount: true,
unapprovedPersonalMsgs: false,
unapprovedTypedMessages: false,
unapprovedTypedMessagesCount: true,
},
SmartTransactionsController: {
smartTransactionsState: {
fees: {
approvalTxFees: true,
tradeTxFees: true,
},
liveness: true,
smartTransactions: false,
userOptIn: true,
},
},
SubjectMetadataController: {
subjectMetadata: false,
},
SwapsController: {
swapsState: {
approveTxId: false,
customApproveTxData: false,
customGasPrice: true,
customMaxFeePerGas: true,
customMaxGas: true,
customMaxPriorityFeePerGas: true,
errorKey: true,
fetchParams: true,
quotes: false,
quotesLastFetched: true,
quotesPollingLimitEnabled: true,
routeState: true,
saveFetchedQuotes: true,
selectedAggId: true,
swapsFeatureFlags: true,
swapsFeatureIsLive: true,
swapsQuotePrefetchingRefreshTime: true,
swapsQuoteRefreshTime: true,
swapsStxBatchStatusRefreshTime: true,
swapsStxGetTransactionsRefreshTime: true,
swapsStxMaxFeeMultiplier: true,
swapsUserFeeLevel: true,
tokens: false,
topAggId: false,
tradeTxId: false,
},
},
TokenListController: {
preventPollingOnNetworkRestart: true,
tokenList: false,
tokensChainsCache: {
[AllProperties]: false,
},
},
TokenRatesController: {
contractExchangeRates: false,
},
TokensController: {
allDetectedTokens: {
[AllProperties]: false,
},
allIgnoredTokens: {
[AllProperties]: false,
},
allTokens: {
[AllProperties]: false,
},
detectedTokens: false,
ignoredTokens: false,
tokens: false,
},
TransactionController: {
currentNetworkTxList: false,
lastFetchedBlockNumbers: false,
},
TxController: {
currentNetworkTxList: false,
unapprovedTxs: false,
},
};
const flattenedBackgroundStateMask = Object.values(
@ -121,7 +301,9 @@ export const SENTRY_UI_STATE = {
// These properties are in the `metamask` slice but not in the background state
customNonceValue: true,
isAccountMenuOpen: true,
isNetworkMenuOpen: true,
nextNonce: true,
pendingTokens: false,
welcomeScreenSeen: true,
},
unconnectedAccount: true,

View File

@ -1447,6 +1447,7 @@ export default class MetamaskController extends EventEmitter {
this.encryptionPublicKeyController.clearUnapproved();
this.decryptMessageController.clearUnapproved();
this.signatureController.clearUnapproved();
this.approvalController.clear();
},
);

View File

@ -0,0 +1,99 @@
import { NetworkType, toHex } from '@metamask/controller-utils';
import { NetworkStatus } from '@metamask/network-controller';
import { cloneDeep } from 'lodash';
import { version as currentStateVersion, migrate } from './092.2';
const TEST_NETWORK_CONTROLLER_STATE = {
networkId: 'network-id',
networkStatus: NetworkStatus.Available,
providerConfig: {
type: NetworkType.rpc,
chainId: toHex(42),
nickname: 'Funky Town Chain',
ticker: 'ETH',
id: 'test-network-client-id',
},
networkDetails: { EIPS: {} },
networkConfigurations: {
'network-configuration-id-1': {
chainId: toHex(42),
nickname: 'Localhost 8545',
rpcPrefs: {},
rpcUrl: 'http://localhost:8545',
ticker: 'ETH',
},
},
};
const anyPreviousStateVersion = 91;
describe('migration #96', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should update the state version number in the appropriate metadata field', async () => {
const originalVersionedState = {
meta: { version: anyPreviousStateVersion },
data: {},
};
const newStorage = await migrate(cloneDeep(originalVersionedState));
expect(newStorage.meta).toStrictEqual({ version: currentStateVersion });
});
it('should return state unaltered if there is no network controller state', async () => {
const originalMetaMaskState = {
anotherController: 'another-controller-state',
};
const originalVersionedState = {
meta: { version: anyPreviousStateVersion },
data: originalMetaMaskState,
};
const updatedVersionedState = await migrate(
cloneDeep(originalVersionedState),
);
expect(updatedVersionedState.data).toStrictEqual(originalMetaMaskState);
});
it('should return unaltered state if there are no obsolete network controller state properties', async () => {
const originalMetaMaskState = {
anotherController: 'another-controller-state',
NetworkController: TEST_NETWORK_CONTROLLER_STATE,
};
const originalVersionedState = {
meta: { version: anyPreviousStateVersion },
data: originalMetaMaskState,
};
const updatedVersionedState = await migrate(
cloneDeep(originalVersionedState),
);
expect(updatedVersionedState.data).toStrictEqual(originalMetaMaskState);
});
it('should return updated state without obsolete network controller state properties', async () => {
const originalMetaMaskState = {
anotherController: 'another-controller-state',
NetworkController: {
...TEST_NETWORK_CONTROLLER_STATE,
someSortOfRogueObsoleteStateProperty: 'exists',
},
};
const originalVersionedState = {
meta: { version: anyPreviousStateVersion },
data: originalMetaMaskState,
};
const updatedVersionedState = await migrate(
cloneDeep(originalVersionedState),
);
expect(updatedVersionedState.data).not.toStrictEqual(originalMetaMaskState);
expect(updatedVersionedState.data).toStrictEqual({
anotherController: 'another-controller-state',
NetworkController: TEST_NETWORK_CONTROLLER_STATE,
});
});
});

View File

@ -0,0 +1,75 @@
import { hasProperty } from '@metamask/utils';
import { captureException } from '@sentry/browser';
import { cloneDeep, isObject, pick } from 'lodash';
type MetaMaskState = Record<string, unknown>;
type VersionedState = {
meta: { version: number };
data: MetaMaskState;
};
export const version = 92.2;
/**
* This migration removes obsolete NetworkController state properties.
*
* @param originalVersionedState - Versioned MetaMask extension state, exactly what we persist to dist.
* @param originalVersionedState.meta - State metadata.
* @param originalVersionedState.meta.version - The current state version.
* @param originalVersionedState.data - The persisted MetaMask state, keyed by controller.
* @returns Updated versioned of MetaMask extension state.
*/
export async function migrate(
originalVersionedState: VersionedState,
): Promise<VersionedState> {
const updatedVersionedState = cloneDeep(originalVersionedState);
updatedVersionedState.meta.version = version;
updatedVersionedState.data = transformState(updatedVersionedState.data);
return updatedVersionedState;
}
function transformState(originalState: MetaMaskState): MetaMaskState {
const updatedState =
filterOutObsoleteNetworkControllerStateProperties(originalState);
return updatedState;
}
function filterOutObsoleteNetworkControllerStateProperties(
state: MetaMaskState,
): MetaMaskState {
// https://github.com/MetaMask/core/blob/%40metamask/network-controller%4010.3.1/packages/network-controller/src/NetworkController.ts#L336-L342
const CURRENT_NETWORK_CONTROLLER_STATE_PROPS = [
'networkId',
'networkStatus',
'providerConfig',
'networkDetails',
'networkConfigurations',
];
if (
!hasProperty(state, 'NetworkController') ||
!isObject(state.NetworkController)
) {
captureException(
`Migration ${version}: Invalid NetworkController state: ${typeof state.NetworkController}`,
);
return state;
}
const networkControllerState = state.NetworkController;
// delete network state properties that are not currently in use
const updatedNetworkController = pick(
networkControllerState,
CURRENT_NETWORK_CONTROLLER_STATE_PROPS,
);
return {
...state,
NetworkController: updatedNetworkController,
};
}

View File

@ -0,0 +1,184 @@
import { migrate } from './092.3';
const PREFERENCES_CONTROLLER_MOCK = {
useBlockie: false,
useNonceField: false,
usePhishDetect: true,
dismissSeedBackUpReminder: false,
disabledRpcMethodPreferences: {
eth_sign: false,
},
useMultiAccountBalanceChecker: true,
useTokenDetection: false,
useNftDetection: false,
use4ByteResolution: true,
useCurrencyRateCheck: true,
openSeaEnabled: false,
advancedGasFee: null,
featureFlags: {
showIncomingTransactions: true,
},
knownMethodData: {},
currentLocale: 'EN',
identities: {},
lostIdentities: {},
forgottenPassword: false,
preferences: {
autoLockTimeLimit: undefined,
showFiatInTestnets: false,
showTestNetworks: false,
useNativeCurrencyAsPrimaryCurrency: true,
hideZeroBalanceTokens: false,
},
// ENS decentralized website resolution
ipfsGateway: '',
useAddressBarEnsResolution: true,
infuraBlocked: null,
ledgerTransportType: 'U2F',
snapRegistryList: {},
transactionSecurityCheckEnabled: false,
theme: 'OS',
isLineaMainnetReleased: false,
};
describe('migration #92.3', () => {
it('updates the version metadata', async () => {
const oldStorage = {
meta: { version: 92.2 },
data: {},
};
const newStorage = await migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({ version: 92.3 });
});
it('does nothing if no PreferencesController state', async () => {
const oldData = {
some: 'data',
};
const oldStorage = {
meta: { version: 92.2 },
data: oldData,
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual(oldData);
});
it('does nothing if no AppStateController state', async () => {
const oldData = {
some: 'data',
};
const oldStorage = {
meta: { version: 92.2 },
data: oldData,
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual(oldData);
});
it('changes advancedGasFee from null to an empty object, and sets hadAdvancedGasFeesSetPriorToMigration92_3 to false', async () => {
const oldData = {
some: 'data',
PreferencesController: {
...PREFERENCES_CONTROLLER_MOCK,
},
AppStateController: {},
};
const oldStorage = {
meta: { version: 92.2 },
data: oldData,
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual({
some: oldData.some,
PreferencesController: {
...PREFERENCES_CONTROLLER_MOCK,
advancedGasFee: {},
},
AppStateController: {
hadAdvancedGasFeesSetPriorToMigration92_3: false,
},
});
});
it('changes advancedGasFee from an object of values to an empty object and sets hadAdvancedGasFeesSetPriorToMigration92_3 to true', async () => {
const oldData = {
some: 'data',
PreferencesController: {
...PREFERENCES_CONTROLLER_MOCK,
advancedGasFee: {
priorityFee: '0x1',
maxBaseFee: '0x1',
},
},
AppStateController: {},
};
const oldStorage = {
meta: { version: 92.2 },
data: oldData,
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual({
some: oldData.some,
PreferencesController: {
...PREFERENCES_CONTROLLER_MOCK,
advancedGasFee: {},
},
AppStateController: {
hadAdvancedGasFeesSetPriorToMigration92_3: true,
},
});
});
it('does not erase advancedGasFee if it does not contain the expected data prior to this migration', async () => {
const oldData = {
some: 'data',
PreferencesController: {
...PREFERENCES_CONTROLLER_MOCK,
advancedGasFee: {
'0x5': {
priorityFee: '0x1',
maxBaseFee: '0x1',
},
},
},
AppStateController: {},
};
const oldStorage = {
meta: { version: 92.2 },
data: oldData,
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual({
some: oldData.some,
PreferencesController: {
...PREFERENCES_CONTROLLER_MOCK,
advancedGasFee: {
'0x5': {
priorityFee: '0x1',
maxBaseFee: '0x1',
},
},
},
AppStateController: {
hadAdvancedGasFeesSetPriorToMigration92_3: false,
},
});
});
});

View File

@ -0,0 +1,98 @@
import { hasProperty, isNullOrUndefined, isObject } from '@metamask/utils';
import { cloneDeep } from 'lodash';
import log from 'loglevel';
type VersionedData = {
meta: { version: number };
data: Record<string, unknown>;
};
export const version = 92.3;
/**
* This migration does the following:
*
* - Deletes currently stored advancedGasFee in preferences controller,
* replacing the default with an empty object
* - Sets hadAdvancedGasFeesSetPriorToMigration92_3 flag on AppStateController
* to indicate if the user had previously had advancedGasFee set in their
* preferences. This will be used to display a whats new entry to inform users
* that we wiped these settings and made them apply per network.
*
* @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist.
* @param originalVersionedData.meta - State metadata.
* @param originalVersionedData.meta.version - The current state version.
* @param originalVersionedData.data - The persisted MetaMask state, keyed by controller.
* @returns Updated versioned MetaMask extension state.
*/
export async function migrate(
originalVersionedData: VersionedData,
): Promise<VersionedData> {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
migrateData(versionedData.data);
return versionedData;
}
function migrateData(state: Record<string, unknown>): void {
changeShapeAndRemoveOldAdvancedGasFeePreference(state);
}
function changeShapeAndRemoveOldAdvancedGasFeePreference(
state: Record<string, unknown>,
) {
if (isNullOrUndefined(state.PreferencesController)) {
log.warn(
`Migration #${version}: preferences controller null or undefined, skipping migration`,
);
return;
}
if (
hasProperty(state, 'AppStateController') &&
isObject(state.AppStateController) &&
hasProperty(state, 'PreferencesController') &&
isObject(state.PreferencesController)
) {
const possibleOriginalValue = state.PreferencesController?.advancedGasFee;
// Will be false if the keys set on the object are anything other than the
// maxBaseFee or priorityFee. Essentially if the object is already keyed
// by chainId it won't show as hadFeesSet.
const hadFeesSet =
isObject(possibleOriginalValue) &&
hasFeePreferenceKeys(possibleOriginalValue);
state.AppStateController.hadAdvancedGasFeesSetPriorToMigration92_3 =
hadFeesSet;
if (
state.PreferencesController.advancedGasFee === null ||
(isObject(state.PreferencesController.advancedGasFee) &&
hasFeePreferenceKeys(state.PreferencesController.advancedGasFee))
) {
state.PreferencesController.advancedGasFee = {};
}
} else if (isObject(state.AppStateController) === false) {
global.sentry?.captureException?.(
new Error(
`typeof state.AppStateController is ${typeof state.AppStateController}`,
),
);
} else if (isObject(state.PreferencesController) === false) {
global.sentry?.captureException?.(
new Error(
`typeof state.PreferencesController is ${typeof state.PreferencesController}`,
),
);
}
}
function hasFeePreferenceKeys(objectToCheck: Record<string, unknown>): boolean {
const keys = Object.keys(objectToCheck);
if (keys.includes('maxBaseFee') || keys.includes('priorityFee')) {
return true;
}
return false;
}

View File

@ -97,6 +97,8 @@ import * as m090 from './090';
import * as m091 from './091';
import * as m092 from './092';
import * as m092point1 from './092.1';
import * as m092point2 from './092.2';
import * as m092point3 from './092.3';
const migrations = [
m002,
@ -191,6 +193,8 @@ const migrations = [
m091,
m092,
m092point1,
m092point2,
m092point3,
];
export default migrations;

View File

@ -2205,7 +2205,9 @@
"packages": {
"@metamask/snaps-controllers-flask>concat-stream>readable-stream": true,
"browserify>buffer": true,
"pumpify>inherits": true
"browserify>concat-stream>typedarray": true,
"pumpify>inherits": true,
"terser>source-map-support>buffer-from": true
}
},
"@metamask/snaps-controllers-flask>concat-stream>readable-stream": {

View File

@ -2205,7 +2205,9 @@
"packages": {
"@metamask/snaps-controllers-flask>concat-stream>readable-stream": true,
"browserify>buffer": true,
"pumpify>inherits": true
"browserify>concat-stream>typedarray": true,
"pumpify>inherits": true,
"terser>source-map-support>buffer-from": true
}
},
"@metamask/snaps-controllers-flask>concat-stream>readable-stream": {

View File

@ -1752,20 +1752,7 @@
"process.platform": true
},
"packages": {
"browserify>browser-resolve>resolve": true
}
},
"browserify>browser-resolve>resolve": {
"builtin": {
"fs.readFile": true,
"fs.readFileSync": true,
"fs.stat": true,
"fs.statSync": true,
"path": true
},
"globals": {
"process.nextTick": true,
"process.platform": true
"brfs>resolve": true
}
},
"browserify>cached-path-relative": {
@ -1874,6 +1861,7 @@
},
"packages": {
"brfs>resolve": true,
"browserify>browser-resolve": true,
"browserify>cached-path-relative": true,
"browserify>concat-stream": true,
"browserify>duplexer2": true,
@ -1881,7 +1869,6 @@
"browserify>module-deps>stream-combiner2": true,
"browserify>module-deps>through2": true,
"browserify>parents": true,
"lavamoat-browserify>browser-resolve": true,
"loose-envify": true,
"pumpify>inherits": true,
"readable-stream": true,
@ -6149,8 +6136,8 @@
},
"packages": {
"@lavamoat/lavapack": true,
"browserify>browser-resolve": true,
"duplexify": true,
"lavamoat-browserify>browser-resolve": true,
"lavamoat-browserify>concat-stream": true,
"lavamoat-browserify>readable-stream": true,
"lavamoat-browserify>through2": true,
@ -6159,29 +6146,16 @@
"lavamoat>lavamoat-core": true
}
},
"lavamoat-browserify>browser-resolve": {
"builtin": {
"fs.readFile": true,
"fs.readFileSync": true,
"path": true
},
"globals": {
"__dirname": true,
"process.platform": true
},
"packages": {
"brfs>resolve": true
}
},
"lavamoat-browserify>concat-stream": {
"globals": {
"Buffer.concat": true,
"Buffer.from": true,
"Buffer.isBuffer": true
},
"packages": {
"browserify>concat-stream>typedarray": true,
"lavamoat-browserify>readable-stream": true,
"pumpify>inherits": true
"pumpify>inherits": true,
"terser>source-map-support>buffer-from": true
}
},
"lavamoat-browserify>readable-stream": {

View File

@ -1,6 +1,6 @@
{
"name": "metamask-crx",
"version": "10.35.0",
"version": "10.35.1",
"private": true,
"repository": {
"type": "git",

View File

@ -1,25 +1,43 @@
/**
* Return a "masked" copy of the given object.
* This symbol matches all object properties when used in a mask
*/
export const AllProperties = Symbol('*');
/**
* Return a "masked" copy of the given object. The returned object includes
* only the properties present in the mask.
*
* The returned object includes only the properties present in the mask. The
* mask is an object that mirrors the structure of the given object, except
* the only values are `true` or a sub-mask. `true` implies the property
* should be included, and a sub-mask implies the property should be further
* masked according to that sub-mask.
* The mask is an object that mirrors the structure of the given object, except
* the only values are `true`, `false, a sub-mask, or the 'AllProperties"
* symbol. `true` implies the property should be included, and `false` will
* exclude it. A sub-mask implies the property should be further masked
* according to that sub-mask. The "AllProperties" symbol is used for objects
* with dynamic keys, and applies a rule (either `true`, `false`, or a
* sub-mask`) to every property in that object.
*
* If a property is not found in the last, its type is included instead.
* If a property is excluded, its type is included instead.
*
* @param {object} object - The object to mask
* @param {Object<object | boolean>} mask - The mask to apply to the object
*/
export function maskObject(object, mask) {
let maskAllProperties = false;
if (Object.keys(mask).includes(AllProperties)) {
if (Object.keys(mask).length > 1) {
throw new Error('AllProperties mask key does not support sibling keys');
}
maskAllProperties = true;
}
return Object.keys(object).reduce((state, key) => {
if (mask[key] === true) {
const maskKey = maskAllProperties ? mask[AllProperties] : mask[key];
if (maskKey === true) {
state[key] = object[key];
} else if (mask[key]) {
state[key] = maskObject(object[key], mask[key]);
} else {
} else if (maskKey && typeof maskKey === 'object') {
state[key] = maskObject(object[key], maskKey);
} else if (maskKey === undefined || maskKey === false) {
state[key] = typeof object[key];
} else {
throw new Error(`Unsupported mask entry: ${maskKey}`);
}
return state;
}, {});

View File

@ -121,6 +121,10 @@ export const UI_NOTIFICATIONS = {
src: 'images/global-menu-block-explorer.svg',
},
},
24: {
id: 24,
date: null,
},
};
export const getTranslatedUINotifications = (t, locale) => {
@ -331,5 +335,16 @@ export const getTranslatedUINotifications = (t, locale) => {
)
: '',
},
24: {
...UI_NOTIFICATIONS[24],
title: t('notifications24Title'),
description: t('notifications24Description'),
actionText: t('notifications24ActionText'),
date: UI_NOTIFICATIONS[24].date
? new Intl.DateTimeFormat(formattedLocale).format(
new Date(UI_NOTIFICATIONS[24].date),
)
: '',
},
};
};

View File

@ -113,6 +113,7 @@
"networkStatus": "available",
"providerConfig": {
"type": "rpc",
"nickname": "goerli",
"chainId": "0x5",
"ticker": "ETH",
"id": "chain5"
@ -339,8 +340,10 @@
"useTokenDetection": true,
"useCurrencyRateCheck": true,
"advancedGasFee": {
"maxBaseFee": "75",
"priorityFee": "2"
"0x5": {
"maxBaseFee": "75",
"priorityFee": "2"
}
},
"nftsDropdownState": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {

View File

@ -4,15 +4,38 @@ const { strict: assert } = require('assert');
const { get, has, set, unset } = require('lodash');
const { Browser } = require('selenium-webdriver');
const { format } = require('prettier');
const { convertToHexValue, withFixtures } = require('../helpers');
const { isObject } = require('@metamask/utils');
const { SENTRY_UI_STATE } = require('../../../app/scripts/lib/setupSentry');
const FixtureBuilder = require('../fixture-builder');
const { convertToHexValue, withFixtures } = require('../helpers');
/**
* Derive a UI state field from a background state field.
*
* @param {string} backgroundField - The path of a background field.
* @returns {string} The path for the corresponding UI field.
*/
function backgroundToUiField(backgroundField) {
// The controller name is lost in the UI due to state flattening
const [, ...rest] = backgroundField.split('.');
const flattenedBackgroundField = rest.join('.');
// Controller state is under the 'metamask' slice in the UI
return `metamask.${flattenedBackgroundField}`;
}
const maskedBackgroundFields = [
'CurrencyController.conversionDate', // This is a timestamp that changes each run
// App metadata is masked so that we don't have to update the snapshot as
// part of the release process
'AppMetadataController.currentAppVersion',
'AppMetadataController.currentMigrationVersion',
'AppStateController.browserEnvironment.browser',
'AppStateController.browserEnvironment.os',
'AppStateController.outdatedBrowserWarningLastShown',
'AppStateController.recoveryPhraseReminderLastShown',
'AppStateController.termsOfUseLastAgreed',
];
const maskedUiFields = [
'metamask.conversionDate', // This is a timestamp that changes each run
];
const maskedUiFields = maskedBackgroundFields.map(backgroundToUiField);
const removedBackgroundFields = [
// This property is timing-dependent
@ -22,13 +45,7 @@ const removedBackgroundFields = [
'AppStateController.timeoutMinutes',
];
const removedUiFields = [
// This property is timing-dependent
'metamask.currentBlockGasLimit',
// These properties are set to undefined, causing inconsistencies between Chrome and Firefox
'metamask.currentPopupId',
'metamask.timeoutMinutes',
];
const removedUiFields = removedBackgroundFields.map(backgroundToUiField);
/**
* Transform background state to make it consistent between test runs.
@ -107,6 +124,38 @@ async function matchesSnapshot({
}
}
/**
* Get an object consisting of all properties in the complete
* object that are missing from the given object.
*
* @param {object} complete - The complete object to compare to.
* @param {object} object - The object to test for missing properties.
*/
function getMissingProperties(complete, object) {
const missing = {};
for (const [key, value] of Object.entries(complete)) {
if (key in object) {
if (isObject(value) && isObject(object[key])) {
const missingNestedProperties = getMissingProperties(
value,
object[key],
);
if (Object.keys(missingNestedProperties).length > 0) {
missing[key] = missingNestedProperties;
} else {
// no missing nested properties
}
} else {
// Skip non-object values, they are considered as present
// even if they represent masked data structures
}
} else {
missing[key] = value;
}
}
return missing;
}
describe('Sentry errors', function () {
const migrationError =
process.env.SELENIUM_BROWSER === Browser.CHROME
@ -312,7 +361,10 @@ describe('Sentry errors', function () {
'Invalid version state',
);
await matchesSnapshot({
data: transformBackgroundState(appState.persistedState),
data: {
...appState.persistedState,
data: transformBackgroundState(appState.persistedState.data),
},
snapshot: 'errors-before-init-opt-in-background-state',
});
},
@ -460,7 +512,10 @@ describe('Sentry errors', function () {
'Invalid version state',
);
await matchesSnapshot({
data: transformBackgroundState(appState.persistedState),
data: {
...appState.persistedState,
data: transformBackgroundState(appState.persistedState.data),
},
snapshot: 'errors-before-init-opt-in-ui-state',
});
},
@ -728,4 +783,80 @@ describe('Sentry errors', function () {
);
});
});
it('should have no policy gaps for UI controller state', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions,
title: this.test.title,
},
async ({ driver }) => {
await driver.navigate();
await driver.findElement('#password');
const fullUiState = await driver.executeScript(() =>
window.stateHooks?.getCleanAppState?.(),
);
const missingState = getMissingProperties(
fullUiState.metamask,
SENTRY_UI_STATE.metamask,
);
assert.deepEqual(missingState, {});
},
);
});
it('should not have extra properties in UI state mask', async function () {
const expectedMissingState = {
currentPopupId: false, // Initialized as undefined
// Part of transaction controller store, but missing from the initial
// state
lastFetchedBlockNumbers: false,
preferences: {
autoLockTimeLimit: true, // Initialized as undefined
},
smartTransactionsState: {
fees: {
approvalTxFees: true, // Initialized as undefined
tradeTxFees: true, // Initialized as undefined
},
userOptIn: true, // Initialized as undefined
},
swapsState: {
// This can get wiped out during initialization due to a bug in
// the "resetState" method
swapsFeatureFlags: true,
},
// This can get erased due to a bug in the app state controller's
// preferences state change handler
timeoutMinutes: true,
};
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions,
title: this.test.title,
},
async ({ driver }) => {
await driver.navigate();
await driver.findElement('#password');
const fullUiState = await driver.executeScript(() =>
window.stateHooks?.getCleanAppState?.(),
);
const extraMaskProperties = getMissingProperties(
SENTRY_UI_STATE.metamask,
fullUiState.metamask,
);
const unexpectedExtraMaskProperties = getMissingProperties(
extraMaskProperties,
expectedMissingState,
);
assert.deepEqual(unexpectedExtraMaskProperties, {});
},
);
});
});

View File

@ -1,51 +1,56 @@
{
"AccountTracker": { "accounts": "object" },
"AddressBookController": "object",
"AddressBookController": { "addressBook": "object" },
"AlertController": {
"alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true },
"unconnectedAccountAlertShownOrigins": "object",
"web3ShimUsageOrigins": "object"
},
"AnnouncementController": "object",
"AnnouncementController": { "announcements": "object" },
"AppMetadataController": {
"currentAppVersion": "10.35.0",
"currentAppVersion": "string",
"previousAppVersion": "",
"previousMigrationVersion": 0,
"currentMigrationVersion": 92.1
"currentMigrationVersion": "number"
},
"AppStateController": {
"connectedStatusPopoverHasBeenShown": true,
"defaultHomeActiveTabName": null,
"browserEnvironment": "object",
"popupGasPollTokens": "object",
"notificationGasPollTokens": "object",
"fullScreenGasPollTokens": "object",
"recoveryPhraseReminderHasBeenShown": "boolean",
"browserEnvironment": { "os": "string", "browser": "string" },
"popupGasPollTokens": [],
"notificationGasPollTokens": [],
"fullScreenGasPollTokens": [],
"recoveryPhraseReminderHasBeenShown": true,
"recoveryPhraseReminderLastShown": "number",
"outdatedBrowserWarningLastShown": "number",
"nftsDetectionNoticeDismissed": "boolean",
"showTestnetMessageInDropdown": "boolean",
"showBetaHeader": "boolean",
"showProductTour": "boolean",
"trezorModel": "object",
"nftsDropdownState": "object",
"nftsDetectionNoticeDismissed": false,
"showTestnetMessageInDropdown": true,
"showBetaHeader": false,
"showProductTour": true,
"trezorModel": null,
"hadAdvancedGasFeesSetPriorToMigration92_3": false,
"nftsDropdownState": {},
"termsOfUseLastAgreed": "number",
"qrHardware": "object",
"usedNetworks": "object",
"snapsInstallPrivacyWarningShown": "boolean",
"serviceWorkerLastActiveTime": "number"
"qrHardware": {},
"usedNetworks": { "0x1": true, "0x5": true, "0x539": true },
"snapsInstallPrivacyWarningShown": true,
"serviceWorkerLastActiveTime": 0
},
"ApprovalController": {
"pendingApprovals": "object",
"pendingApprovalCount": "number",
"approvalFlows": "object"
},
"ApprovalController": "object",
"BackupController": "undefined",
"CachedBalancesController": "object",
"CachedBalancesController": { "cachedBalances": "object" },
"CurrencyController": {
"conversionDate": "number",
"conversionRate": 1700,
"nativeCurrency": "ETH",
"currentCurrency": "usd",
"pendingCurrentCurrency": "object",
"pendingNativeCurrency": "object",
"usdConversionRate": "number"
"pendingCurrentCurrency": null,
"pendingNativeCurrency": null,
"usdConversionRate": 1700
},
"DecryptMessageController": {
"unapprovedDecryptMsgs": "object",
@ -55,8 +60,12 @@
"unapprovedEncryptionPublicKeyMsgs": "object",
"unapprovedEncryptionPublicKeyMsgCount": 0
},
"EnsController": "object",
"GasFeeController": "object",
"EnsController": { "ensResolutionsByAddress": "object" },
"GasFeeController": {
"gasFeeEstimates": {},
"estimatedGasFeeTimeBounds": {},
"gasEstimateType": "none"
},
"IncomingTransactionsController": {
"incomingTransactions": "object",
"incomingTxLastFetchedBlockByChainId": {
@ -86,38 +95,45 @@
"networkId": "1337",
"networkStatus": "available",
"providerConfig": {
"chainId": "string",
"chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": "object",
"rpcUrl": "string",
"ticker": "ETH",
"type": "rpc",
"id": "string"
"id": "networkConfigurationId"
},
"networkDetails": "object",
"networkConfigurations": "object"
},
"NftController": "object",
"NftController": {
"allNftContracts": "object",
"allNfts": "object",
"ignoredNfts": "object"
},
"OnboardingController": {
"seedPhraseBackedUp": true,
"firstTimeFlowType": "import",
"completedOnboarding": true,
"onboardingTabs": "object"
},
"PermissionController": "object",
"PermissionLogController": "object",
"PermissionController": { "subjects": "object" },
"PermissionLogController": {
"permissionHistory": "object",
"permissionActivityLog": "object"
},
"PreferencesController": {
"useBlockie": false,
"useNonceField": false,
"usePhishDetect": true,
"dismissSeedBackUpReminder": "boolean",
"disabledRpcMethodPreferences": "object",
"useMultiAccountBalanceChecker": "boolean",
"useTokenDetection": "boolean",
"useNftDetection": "boolean",
"useCurrencyRateCheck": "boolean",
"openSeaEnabled": "boolean",
"advancedGasFee": "object",
"dismissSeedBackUpReminder": true,
"disabledRpcMethodPreferences": { "eth_sign": false },
"useMultiAccountBalanceChecker": true,
"useTokenDetection": false,
"useNftDetection": false,
"useCurrencyRateCheck": true,
"openSeaEnabled": false,
"advancedGasFee": {},
"featureFlags": { "showIncomingTransactions": true },
"knownMethodData": "object",
"currentLocale": "en",
@ -130,13 +146,13 @@
"showTestNetworks": false,
"useNativeCurrencyAsPrimaryCurrency": true
},
"ipfsGateway": "dweb.link",
"infuraBlocked": "boolean",
"ledgerTransportType": "string",
"ipfsGateway": "string",
"infuraBlocked": false,
"ledgerTransportType": "webhid",
"snapRegistryList": "object",
"transactionSecurityCheckEnabled": "boolean",
"theme": "string",
"isLineaMainnetReleased": "boolean",
"transactionSecurityCheckEnabled": false,
"theme": "light",
"isLineaMainnetReleased": true,
"selectedAddress": "string"
},
"SignatureController": {
@ -147,11 +163,58 @@
"unapprovedPersonalMsgCount": 0,
"unapprovedTypedMessagesCount": 0
},
"SmartTransactionsController": "object",
"SubjectMetadataController": "object",
"SwapsController": "object",
"TokenListController": "object",
"TokenRatesController": "object",
"TokensController": "object",
"TxController": "object"
"SmartTransactionsController": {
"smartTransactionsState": {
"fees": {},
"liveness": true,
"smartTransactions": "object"
}
},
"SubjectMetadataController": { "subjectMetadata": "object" },
"SwapsController": {
"swapsState": {
"quotes": "object",
"quotesPollingLimitEnabled": false,
"fetchParams": null,
"tokens": "object",
"tradeTxId": "object",
"approveTxId": "object",
"quotesLastFetched": null,
"customMaxGas": "",
"customGasPrice": null,
"customMaxFeePerGas": null,
"customMaxPriorityFeePerGas": null,
"swapsUserFeeLevel": "",
"selectedAggId": null,
"customApproveTxData": "string",
"errorKey": "",
"topAggId": "object",
"routeState": "",
"swapsFeatureIsLive": true,
"saveFetchedQuotes": false,
"swapsQuoteRefreshTime": 60000,
"swapsQuotePrefetchingRefreshTime": 60000,
"swapsStxBatchStatusRefreshTime": 10000,
"swapsStxGetTransactionsRefreshTime": 10000,
"swapsStxMaxFeeMultiplier": 2
}
},
"TokenListController": {
"tokenList": "object",
"tokensChainsCache": {},
"preventPollingOnNetworkRestart": true
},
"TokenRatesController": { "contractExchangeRates": "object" },
"TokensController": {
"tokens": "object",
"ignoredTokens": "object",
"detectedTokens": "object",
"allTokens": {},
"allIgnoredTokens": {},
"allDetectedTokens": {}
},
"TxController": {
"unapprovedTxs": "object",
"currentNetworkTxList": "object"
}
}

View File

@ -11,7 +11,7 @@
"isInitialized": true,
"isUnlocked": false,
"isAccountMenuOpen": false,
"isNetworkMenuOpen": "boolean",
"isNetworkMenuOpen": false,
"identities": "object",
"unapprovedTxs": "object",
"networkConfigurations": "object",
@ -38,38 +38,39 @@
"nativeCurrency": "ETH",
"connectedStatusPopoverHasBeenShown": true,
"defaultHomeActiveTabName": null,
"browserEnvironment": "object",
"popupGasPollTokens": "object",
"notificationGasPollTokens": "object",
"fullScreenGasPollTokens": "object",
"recoveryPhraseReminderHasBeenShown": "boolean",
"browserEnvironment": { "os": "string", "browser": "string" },
"popupGasPollTokens": [],
"notificationGasPollTokens": [],
"fullScreenGasPollTokens": [],
"recoveryPhraseReminderHasBeenShown": true,
"recoveryPhraseReminderLastShown": "number",
"outdatedBrowserWarningLastShown": "number",
"nftsDetectionNoticeDismissed": "boolean",
"showTestnetMessageInDropdown": "boolean",
"showBetaHeader": "boolean",
"showProductTour": "boolean",
"trezorModel": "object",
"nftsDropdownState": "object",
"nftsDetectionNoticeDismissed": false,
"showTestnetMessageInDropdown": true,
"showBetaHeader": false,
"showProductTour": true,
"trezorModel": null,
"hadAdvancedGasFeesSetPriorToMigration92_3": false,
"nftsDropdownState": {},
"termsOfUseLastAgreed": "number",
"qrHardware": "object",
"usedNetworks": "object",
"snapsInstallPrivacyWarningShown": "boolean",
"serviceWorkerLastActiveTime": "number",
"currentAppVersion": "10.35.0",
"qrHardware": {},
"usedNetworks": { "0x1": true, "0x5": true, "0x539": true },
"snapsInstallPrivacyWarningShown": true,
"serviceWorkerLastActiveTime": 0,
"currentAppVersion": "string",
"previousAppVersion": "",
"previousMigrationVersion": 0,
"currentMigrationVersion": 92.1,
"currentMigrationVersion": "number",
"networkId": "1337",
"networkStatus": "available",
"providerConfig": {
"chainId": "string",
"chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": "object",
"rpcUrl": "string",
"ticker": "ETH",
"type": "rpc",
"id": "string"
"id": "networkConfigurationId"
},
"networkDetails": "object",
"cachedBalances": "object",
@ -78,23 +79,23 @@
"encryptionKey": "object",
"useNonceField": false,
"usePhishDetect": true,
"dismissSeedBackUpReminder": "boolean",
"disabledRpcMethodPreferences": "object",
"useMultiAccountBalanceChecker": "boolean",
"useTokenDetection": "boolean",
"useNftDetection": "boolean",
"useCurrencyRateCheck": "boolean",
"openSeaEnabled": "boolean",
"advancedGasFee": "object",
"dismissSeedBackUpReminder": true,
"disabledRpcMethodPreferences": { "eth_sign": false },
"useMultiAccountBalanceChecker": true,
"useTokenDetection": false,
"useNftDetection": false,
"useCurrencyRateCheck": true,
"openSeaEnabled": false,
"advancedGasFee": {},
"lostIdentities": "object",
"forgottenPassword": false,
"ipfsGateway": "dweb.link",
"infuraBlocked": "boolean",
"ledgerTransportType": "string",
"ipfsGateway": "string",
"infuraBlocked": false,
"ledgerTransportType": "webhid",
"snapRegistryList": "object",
"transactionSecurityCheckEnabled": "boolean",
"theme": "string",
"isLineaMainnetReleased": "boolean",
"transactionSecurityCheckEnabled": false,
"theme": "light",
"isLineaMainnetReleased": true,
"selectedAddress": "string",
"metaMetricsId": "fake-metrics-id",
"eventsBeforeMetricsOptIn": "object",
@ -104,9 +105,9 @@
"previousUserTraits": "object",
"conversionDate": "number",
"currentCurrency": "usd",
"pendingCurrentCurrency": "object",
"pendingNativeCurrency": "object",
"usdConversionRate": "number",
"pendingCurrentCurrency": null,
"pendingNativeCurrency": null,
"usdConversionRate": 1300,
"alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true },
"unconnectedAccountAlertShownOrigins": "object",
"web3ShimUsageOrigins": "object",
@ -125,19 +126,23 @@
"permissionActivityLog": "object",
"subjectMetadata": "object",
"announcements": "object",
"gasFeeEstimates": "object",
"estimatedGasFeeTimeBounds": "object",
"gasEstimateType": "string",
"gasFeeEstimates": {},
"estimatedGasFeeTimeBounds": {},
"gasEstimateType": "none",
"tokenList": "object",
"tokensChainsCache": "object",
"preventPollingOnNetworkRestart": "boolean",
"tokensChainsCache": {},
"preventPollingOnNetworkRestart": true,
"tokens": "object",
"ignoredTokens": "object",
"detectedTokens": "object",
"allTokens": "object",
"allIgnoredTokens": "object",
"allDetectedTokens": "object",
"smartTransactionsState": "object",
"allTokens": {},
"allIgnoredTokens": {},
"allDetectedTokens": {},
"smartTransactionsState": {
"fees": {},
"liveness": true,
"smartTransactions": "object"
},
"allNftContracts": "object",
"allNfts": "object",
"ignoredNfts": "object",
@ -153,7 +158,32 @@
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgCount": 0,
"unapprovedTypedMessagesCount": 0,
"swapsState": "object",
"swapsState": {
"quotes": "object",
"quotesPollingLimitEnabled": false,
"fetchParams": null,
"tokens": "object",
"tradeTxId": "object",
"approveTxId": "object",
"quotesLastFetched": null,
"customMaxGas": "",
"customGasPrice": null,
"customMaxFeePerGas": null,
"customMaxPriorityFeePerGas": null,
"swapsUserFeeLevel": "",
"selectedAggId": null,
"customApproveTxData": "string",
"errorKey": "",
"topAggId": "object",
"routeState": "",
"swapsFeatureIsLive": true,
"saveFetchedQuotes": false,
"swapsQuoteRefreshTime": 60000,
"swapsQuotePrefetchingRefreshTime": 60000,
"swapsStxBatchStatusRefreshTime": 10000,
"swapsStxGetTransactionsRefreshTime": 10000,
"swapsStxMaxFeeMultiplier": 2
},
"ensResolutionsByAddress": "object",
"pendingApprovals": "object",
"pendingApprovalCount": "number",

View File

@ -5,33 +5,42 @@
"unconnectedAccountAlertShownOrigins": "object",
"web3ShimUsageOrigins": "object"
},
"AnnouncementController": "object",
"AnnouncementController": { "announcements": "object" },
"AppStateController": {
"browserEnvironment": "object",
"nftsDropdownState": "object",
"browserEnvironment": {},
"nftsDropdownState": {},
"connectedStatusPopoverHasBeenShown": true,
"termsOfUseLastAgreed": "number",
"defaultHomeActiveTabName": null,
"fullScreenGasPollTokens": "object",
"notificationGasPollTokens": "object",
"popupGasPollTokens": "object",
"qrHardware": "object",
"recoveryPhraseReminderHasBeenShown": "boolean",
"fullScreenGasPollTokens": [],
"notificationGasPollTokens": [],
"popupGasPollTokens": [],
"qrHardware": {},
"recoveryPhraseReminderHasBeenShown": true,
"recoveryPhraseReminderLastShown": "number",
"showTestnetMessageInDropdown": "boolean",
"trezorModel": "object",
"usedNetworks": "object",
"snapsInstallPrivacyWarningShown": "boolean"
"showTestnetMessageInDropdown": true,
"trezorModel": null,
"usedNetworks": {
"0x1": true,
"0xe708": true,
"0x5": true,
"0x539": true
},
"snapsInstallPrivacyWarningShown": true
},
"CachedBalancesController": "object",
"CachedBalancesController": { "cachedBalances": "object" },
"CurrencyController": {
"conversionDate": 1665507600,
"conversionDate": "number",
"conversionRate": 1300,
"currentCurrency": "usd",
"nativeCurrency": "ETH",
"usdConversionRate": "number"
"usdConversionRate": 1300
},
"GasFeeController": {
"estimatedGasFeeTimeBounds": {},
"gasEstimateType": "none",
"gasFeeEstimates": {}
},
"GasFeeController": "object",
"IncomingTransactionsController": {
"incomingTransactions": "object",
"incomingTxLastFetchedBlockByChainId": {
@ -54,13 +63,13 @@
"networkId": "1337",
"networkStatus": "available",
"providerConfig": {
"chainId": "string",
"chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": "object",
"rpcUrl": "string",
"ticker": "ETH",
"type": "rpc",
"id": "string"
"id": "networkConfigurationId"
},
"networkConfigurations": "object"
},
@ -70,20 +79,20 @@
"onboardingTabs": "object",
"seedPhraseBackedUp": true
},
"PermissionController": "object",
"PermissionController": { "subjects": "object" },
"PreferencesController": {
"advancedGasFee": "object",
"advancedGasFee": null,
"currentLocale": "en",
"dismissSeedBackUpReminder": "boolean",
"dismissSeedBackUpReminder": true,
"featureFlags": { "showIncomingTransactions": true },
"forgottenPassword": false,
"identities": "object",
"infuraBlocked": "boolean",
"ipfsGateway": "dweb.link",
"infuraBlocked": false,
"ipfsGateway": "string",
"knownMethodData": "object",
"ledgerTransportType": "string",
"ledgerTransportType": "webhid",
"lostIdentities": "object",
"openSeaEnabled": "boolean",
"openSeaEnabled": false,
"preferences": {
"hideZeroBalanceTokens": false,
"showFiatInTestnets": false,
@ -91,19 +100,32 @@
"useNativeCurrencyAsPrimaryCurrency": true
},
"selectedAddress": "string",
"theme": "string",
"theme": "light",
"useBlockie": false,
"useNftDetection": "boolean",
"useNftDetection": false,
"useNonceField": false,
"usePhishDetect": true,
"useTokenDetection": "boolean",
"useCurrencyRateCheck": "boolean",
"useMultiAccountBalanceChecker": "boolean"
"useTokenDetection": false,
"useCurrencyRateCheck": true,
"useMultiAccountBalanceChecker": true
},
"SmartTransactionsController": "object",
"SubjectMetadataController": "object",
"TokensController": "object",
"TransactionController": "object",
"SmartTransactionsController": {
"smartTransactionsState": {
"fees": {},
"liveness": true,
"smartTransactions": "object"
}
},
"SubjectMetadataController": { "subjectMetadata": "object" },
"TokensController": {
"allDetectedTokens": {},
"allIgnoredTokens": {},
"allTokens": {},
"detectedTokens": "object",
"ignoredTokens": "object",
"tokens": "object"
},
"TransactionController": { "transactions": "object" },
"config": "object",
"firstTimeInfo": "object"
}

View File

@ -5,33 +5,42 @@
"unconnectedAccountAlertShownOrigins": "object",
"web3ShimUsageOrigins": "object"
},
"AnnouncementController": "object",
"AnnouncementController": { "announcements": "object" },
"AppStateController": {
"browserEnvironment": "object",
"nftsDropdownState": "object",
"browserEnvironment": {},
"nftsDropdownState": {},
"connectedStatusPopoverHasBeenShown": true,
"termsOfUseLastAgreed": "number",
"defaultHomeActiveTabName": null,
"fullScreenGasPollTokens": "object",
"notificationGasPollTokens": "object",
"popupGasPollTokens": "object",
"qrHardware": "object",
"recoveryPhraseReminderHasBeenShown": "boolean",
"fullScreenGasPollTokens": [],
"notificationGasPollTokens": [],
"popupGasPollTokens": [],
"qrHardware": {},
"recoveryPhraseReminderHasBeenShown": true,
"recoveryPhraseReminderLastShown": "number",
"showTestnetMessageInDropdown": "boolean",
"trezorModel": "object",
"usedNetworks": "object",
"snapsInstallPrivacyWarningShown": "boolean"
"showTestnetMessageInDropdown": true,
"trezorModel": null,
"usedNetworks": {
"0x1": true,
"0xe708": true,
"0x5": true,
"0x539": true
},
"snapsInstallPrivacyWarningShown": true
},
"CachedBalancesController": "object",
"CachedBalancesController": { "cachedBalances": "object" },
"CurrencyController": {
"conversionDate": 1665507600,
"conversionDate": "number",
"conversionRate": 1300,
"currentCurrency": "usd",
"nativeCurrency": "ETH",
"usdConversionRate": "number"
"usdConversionRate": 1300
},
"GasFeeController": {
"estimatedGasFeeTimeBounds": {},
"gasEstimateType": "none",
"gasFeeEstimates": {}
},
"GasFeeController": "object",
"IncomingTransactionsController": {
"incomingTransactions": "object",
"incomingTxLastFetchedBlockByChainId": {
@ -54,13 +63,13 @@
"networkId": "1337",
"networkStatus": "available",
"providerConfig": {
"chainId": "string",
"chainId": "0x539",
"nickname": "Localhost 8545",
"rpcPrefs": "object",
"rpcUrl": "string",
"ticker": "ETH",
"type": "rpc",
"id": "string"
"id": "networkConfigurationId"
},
"networkConfigurations": "object"
},
@ -70,20 +79,20 @@
"onboardingTabs": "object",
"seedPhraseBackedUp": true
},
"PermissionController": "object",
"PermissionController": { "subjects": "object" },
"PreferencesController": {
"advancedGasFee": "object",
"advancedGasFee": null,
"currentLocale": "en",
"dismissSeedBackUpReminder": "boolean",
"dismissSeedBackUpReminder": true,
"featureFlags": { "showIncomingTransactions": true },
"forgottenPassword": false,
"identities": "object",
"infuraBlocked": "boolean",
"ipfsGateway": "dweb.link",
"infuraBlocked": false,
"ipfsGateway": "string",
"knownMethodData": "object",
"ledgerTransportType": "string",
"ledgerTransportType": "webhid",
"lostIdentities": "object",
"openSeaEnabled": "boolean",
"openSeaEnabled": false,
"preferences": {
"hideZeroBalanceTokens": false,
"showFiatInTestnets": false,
@ -91,19 +100,32 @@
"useNativeCurrencyAsPrimaryCurrency": true
},
"selectedAddress": "string",
"theme": "string",
"theme": "light",
"useBlockie": false,
"useNftDetection": "boolean",
"useNftDetection": false,
"useNonceField": false,
"usePhishDetect": true,
"useTokenDetection": "boolean",
"useCurrencyRateCheck": "boolean",
"useMultiAccountBalanceChecker": "boolean"
"useTokenDetection": false,
"useCurrencyRateCheck": true,
"useMultiAccountBalanceChecker": true
},
"SmartTransactionsController": "object",
"SubjectMetadataController": "object",
"TokensController": "object",
"TransactionController": "object",
"SmartTransactionsController": {
"smartTransactionsState": {
"fees": {},
"liveness": true,
"smartTransactions": "object"
}
},
"SubjectMetadataController": { "subjectMetadata": "object" },
"TokensController": {
"allDetectedTokens": {},
"allIgnoredTokens": {},
"allTokens": {},
"detectedTokens": "object",
"ignoredTokens": "object",
"tokens": "object"
},
"TransactionController": { "transactions": "object" },
"config": "object",
"firstTimeInfo": "object"
},

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { capitalize } from 'lodash';
import { useTransactionEventFragment } from '../../../../hooks/useTransactionEventFragment';
import { EditGasModes } from '../../../../../shared/constants/gas';
import Box from '../../../ui/box';
@ -11,7 +12,11 @@ import {
TextColor,
TextVariant,
} from '../../../../helpers/constants/design-system';
import { getAdvancedGasFeeValues } from '../../../../selectors';
import {
getAdvancedGasFeeValues,
getCurrentChainId,
getNetworkIdentifier,
} from '../../../../selectors';
import { setAdvancedGasFee } from '../../../../store/actions';
import { useGasFeeContext } from '../../../../contexts/gasFee';
import { useAdvancedGasFeePopoverContext } from '../context';
@ -24,6 +29,9 @@ const AdvancedGasFeeDefaults = () => {
const { gasErrors, maxBaseFee, maxPriorityFeePerGas } =
useAdvancedGasFeePopoverContext();
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues);
// This will need to use a different chainId in multinetwork
const chainId = useSelector(getCurrentChainId);
const networkIdentifier = useSelector(getNetworkIdentifier);
const { updateTransactionEventFragment } = useTransactionEventFragment();
const { editGasMode } = useGasFeeContext();
const [isDefaultSettingsSelected, setDefaultSettingsSelected] = useState(
@ -42,7 +50,7 @@ const AdvancedGasFeeDefaults = () => {
const handleUpdateDefaultSettings = () => {
if (isDefaultSettingsSelected) {
dispatch(setAdvancedGasFee(null));
dispatch(setAdvancedGasFee({ chainId, gasFeePreferences: undefined }));
setDefaultSettingsSelected(false);
updateTransactionEventFragment({
properties: {
@ -53,8 +61,11 @@ const AdvancedGasFeeDefaults = () => {
} else {
dispatch(
setAdvancedGasFee({
maxBaseFee,
priorityFee: maxPriorityFeePerGas,
chainId,
gasFeePreferences: {
maxBaseFee,
priorityFee: maxPriorityFeePerGas,
},
}),
);
updateTransactionEventFragment({
@ -91,11 +102,7 @@ const AdvancedGasFeeDefaults = () => {
as="h6"
color={TextColor.textAlternative}
>
{isDefaultSettingsSelected
? t('advancedGasFeeDefaultOptOut')
: t('advancedGasFeeDefaultOptIn', [
<strong key="default-value-change">{t('newValues')}</strong>,
])}
{t('advancedGasFeeDefaultOptIn', [capitalize(networkIdentifier)])}
</Text>
</label>
</Box>

View File

@ -15,8 +15,11 @@ import { GasFeeContextProvider } from '../../../../contexts/gasFee';
import configureStore from '../../../../store/store';
import AdvancedGasFeeInputs from '../advanced-gas-fee-inputs';
import { CHAIN_IDS } from '../../../../../shared/constants/network';
import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults';
const TEXT_SELECTOR = 'Save these values as my default for the Goerli network.';
jest.mock('../../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
@ -62,68 +65,58 @@ const render = (defaultGasParams, contextParams) => {
};
describe('AdvancedGasFeeDefaults', () => {
it('should renders correct message when the default is not set', () => {
render({ advancedGasFee: null });
expect(screen.queryByText('new values')).toBeInTheDocument();
render({ advancedGasFee: {} });
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
});
it('should renders correct message when the default values are set', () => {
render({
advancedGasFee: { maxBaseFee: 50, priorityFee: 2 },
advancedGasFee: {
[CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 },
},
});
expect(
screen.queryByText(
'Always use these values and advanced setting as default.',
),
).toBeInTheDocument();
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
});
it('should renders correct message when the default values are set and the maxBaseFee values are updated', () => {
render({
advancedGasFee: { maxBaseFee: 50, priorityFee: 2 },
advancedGasFee: {
[CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 },
},
});
expect(document.getElementsByTagName('input')[2]).toBeChecked();
expect(
screen.queryByText(
'Always use these values and advanced setting as default.',
),
).toBeInTheDocument();
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
fireEvent.change(document.getElementsByTagName('input')[0], {
target: { value: 75 },
});
expect(document.getElementsByTagName('input')[0]).toHaveValue(75);
expect(screen.queryByText('new values')).toBeInTheDocument();
expect(
screen.queryByText('Save these as my default for "Advanced"'),
).toBeInTheDocument();
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
});
it('should renders correct message when the default values are set and the priorityFee values are updated', () => {
render({
advancedGasFee: { maxBaseFee: 50, priorityFee: 2 },
advancedGasFee: {
[CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 },
},
});
expect(document.getElementsByTagName('input')[2]).toBeChecked();
expect(
screen.queryByText(
'Always use these values and advanced setting as default.',
),
).toBeInTheDocument();
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
fireEvent.change(document.getElementsByTagName('input')[1], {
target: { value: 5 },
});
expect(document.getElementsByTagName('input')[1]).toHaveValue(5);
expect(screen.queryByText('new values')).toBeInTheDocument();
expect(
screen.queryByText('Save these as my default for "Advanced"'),
).toBeInTheDocument();
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument();
});
it('should call action setAdvancedGasFee when checkbox or label text is clicked', () => {
render({
advancedGasFee: { maxBaseFee: 50, priorityFee: 2 },
advancedGasFee: {
[CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 },
},
});
const mock = jest
.spyOn(Actions, 'setAdvancedGasFee')
.mockReturnValue({ type: 'test' });
const checkboxLabel = screen.queryByText(
'Always use these values and advanced setting as default.',
);
const checkboxLabel = screen.queryByText(TEXT_SELECTOR);
fireEvent.click(checkboxLabel);
expect(mock).toHaveBeenCalledTimes(1);
const checkbox = document.querySelector('input[type=checkbox]');

View File

@ -13,6 +13,7 @@ import configureStore from '../../../../../store/store';
import { AdvancedGasFeePopoverContextProvider } from '../../context';
import AdvancedGasFeeGasLimit from '../../advanced-gas-fee-gas-limit';
import { CHAIN_IDS } from '../../../../../../shared/constants/network';
import PriorityfeeInput from './priority-fee-input';
jest.mock('../../../../../store/actions', () => ({
@ -34,7 +35,7 @@ const render = (txProps, contextProps) => {
balance: '0x1F4',
},
},
advancedGasFee: { priorityFee: 100 },
advancedGasFee: { [CHAIN_IDS.GOERLI]: { priorityFee: 100 } },
featureFlags: { advancedInlineGas: true },
gasFeeEstimates:
mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates,

View File

@ -61,9 +61,13 @@ const render = ({ txProps, contextProps } = {}) => {
balance: '0x1F4',
},
},
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true },
gasFeeEstimates: MOCK_FEE_ESTIMATE,
advancedGasFee: {},
},
});

View File

@ -10,6 +10,7 @@ import { ETH } from '../../../../helpers/constants/common';
import configureStore from '../../../../store/store';
import { GasFeeContextProvider } from '../../../../contexts/gasFee';
import { CHAIN_IDS } from '../../../../../shared/constants/network';
import EditGasItem from './edit-gas-item';
jest.mock('../../../../store/actions', () => ({
@ -59,7 +60,9 @@ const renderComponent = ({
const store = configureStore({
metamask: {
nativeCurrency: ETH,
providerConfig: {},
providerConfig: {
chainId: CHAIN_IDS.GOERLI,
},
cachedBalances: {},
accounts: {
'0xAddress': {
@ -67,13 +70,18 @@ const renderComponent = ({
balance: '0x176e5b6f173ebe66',
},
},
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true },
gasEstimateType: 'fee-market',
gasFeeEstimates: MOCK_FEE_ESTIMATE,
advancedGasFee: {
maxBaseFee: '100',
priorityFee: '2',
[CHAIN_IDS.GOERLI]: {
maxBaseFee: '100',
priorityFee: '2',
},
},
},
});

View File

@ -41,6 +41,9 @@ const renderComponent = (componentProps) => {
balance: '0x176e5b6f173ebe66',
},
},
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true },
},

View File

@ -28,6 +28,9 @@ const render = () => {
balance: '0x176e5b6f173ebe66',
},
},
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress',
},
});

View File

@ -112,12 +112,13 @@ export default function NftDetails({ nft }) {
const getOpenSeaLink = () => {
switch (currentNetwork) {
case CHAIN_IDS.MAINNET:
return `https://opensea.io/assets/${address}/${tokenId}`;
return `https://opensea.io/assets/ethereum/${address}/${tokenId}`;
case CHAIN_IDS.POLYGON:
return `https://opensea.io/assets/matic/${address}/${tokenId}`;
case CHAIN_IDS.GOERLI:
return `https://testnets.opensea.io/assets/goerli/${address}/${tokenId}`;
case CHAIN_IDS.SEPOLIA:
return `https://testnets.opensea.io/assets/${address}/${tokenId}`;
return `https://testnets.opensea.io/assets/sepolia/${address}/${tokenId}`;
default:
return null;
}

View File

@ -165,7 +165,7 @@ describe('NFT Details', () => {
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://testnets.opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`,
url: `https://testnets.opensea.io/assets/goerli/${nfts[5].address}/${nfts[5].tokenId}`,
});
});
});
@ -200,7 +200,7 @@ describe('NFT Details', () => {
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`,
url: `https://opensea.io/assets/ethereum/${nfts[5].address}/${nfts[5].tokenId}`,
});
});
});
@ -272,7 +272,7 @@ describe('NFT Details', () => {
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://testnets.opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`,
url: `https://testnets.opensea.io/assets/sepolia/${nfts[5].address}/${nfts[5].tokenId}`,
});
});
});

View File

@ -61,7 +61,7 @@ exports[`SignatureRequestHeader should match snapshot 1`] = `
<span
class="icon-with-fallback__fallback"
>
U
G
</span>
</div>
</div>
@ -71,7 +71,7 @@ exports[`SignatureRequestHeader should match snapshot 1`] = `
<h6
class="box mm-text mm-text--body-sm box--flex-direction-row box--color-text-alternative"
>
Unknown private network
goerli
</h6>
<h6
class="box mm-text mm-text--body-sm mm-text--font-weight-bold box--flex-direction-row box--color-text-default"

View File

@ -137,7 +137,7 @@ exports[`SignatureRequestOriginal should match snapshot 1`] = `
<span
class="icon-with-fallback__fallback"
>
U
G
</span>
</div>
</div>
@ -147,7 +147,7 @@ exports[`SignatureRequestOriginal should match snapshot 1`] = `
<h6
class="box mm-text mm-text--body-sm box--flex-direction-row box--color-text-alternative"
>
Unknown private network
goerli
</h6>
<h6
class="box mm-text mm-text--body-sm mm-text--font-weight-bold box--flex-direction-row box--color-text-default"

View File

@ -134,7 +134,7 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`]
<span
class="icon-with-fallback__fallback"
>
U
G
</span>
</div>
</div>
@ -144,7 +144,7 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`]
<h6
class="box mm-text mm-text--body-sm box--flex-direction-row box--color-text-alternative"
>
Unknown private network
goerli
</h6>
<h6
class="box mm-text mm-text--body-sm mm-text--font-weight-bold box--flex-direction-row box--color-text-default"

View File

@ -89,7 +89,7 @@ exports[`SignatureRequestHeader renders correctly with fromAccount 1`] = `
<span
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography chip__label typography--h7 typography--weight-normal typography--style-normal typography--color-text-alternative"
>
Private network
goerli
</span>
</div>
</div>
@ -127,7 +127,7 @@ exports[`SignatureRequestHeader renders correctly without fromAccount 1`] = `
<span
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography chip__label typography--h7 typography--weight-normal typography--style-normal typography--color-text-alternative"
>
Private network
goerli
</span>
</div>
</div>

View File

@ -103,6 +103,9 @@ function getActionFunctionById(id, history) {
22: () => {
updateViewedNotifications({ 22: true });
},
24: () => {
updateViewedNotifications({ 24: true });
},
};
return actionFunctions[id];
@ -364,6 +367,7 @@ export default function WhatsNewPopup({
19: renderFirstNotification,
21: renderFirstNotification,
22: renderFirstNotification,
24: renderFirstNotification,
};
return (

View File

@ -11,7 +11,7 @@ import withModalProps from '../../../helpers/higher-order-components/with-modal-
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
import { setSelectedAddress } from '../../../store/actions';
import { getMetaMaskAccountsRaw } from '../../../selectors';
import { getMetaMaskIdentities } from '../../../selectors';
import {
getMMIAddressFromModalOrAddress,
getCustodyAccountDetails,
@ -42,7 +42,7 @@ const CustodyConfirmLink = ({ hideModal }) => {
const dispatch = useDispatch();
const mmiActions = mmiActionsFactory();
const trackEvent = useContext(MetaMetricsContext);
const mmiAccounts = useSelector(getMetaMaskAccountsRaw);
const mmiAccounts = useSelector(getMetaMaskIdentities);
const address = useSelector(getMMIAddressFromModalOrAddress);
const custodyAccountDetails = useSelector(getCustodyAccountDetails);
const { custodians } = useSelector(getMMIConfiguration);

View File

@ -77,6 +77,8 @@ import { draftTransactionInitialState, editExistingTransaction } from '.';
const mockStore = createMockStore([thunk]);
const mockAddress1 = '0xdafea492d9c6733ae3d56b7ed1adb60692c98123';
jest.mock('./send', () => {
const actual = jest.requireActual('./send');
return {
@ -1063,7 +1065,7 @@ describe('Send Slice', () => {
describe('QR Code Detected', () => {
const qrCodestate = getInitialSendStateWithExistingTxState({
recipient: {
address: '0xAddress',
address: mockAddress1,
},
});
@ -1102,7 +1104,7 @@ describe('Send Slice', () => {
const draftTransaction = getTestUUIDTx(result);
expect(draftTransaction.recipient.address).toStrictEqual('0xAddress');
expect(draftTransaction.recipient.address).toStrictEqual(mockAddress1);
expect(draftTransaction.recipient.error).toStrictEqual(
INVALID_RECIPIENT_ADDRESS_ERROR,
);
@ -1115,7 +1117,7 @@ describe('Send Slice', () => {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
selectedAccount: {
balance: '0x0',
address: '0xAddress',
address: mockAddress1,
},
};
@ -1144,7 +1146,7 @@ describe('Send Slice', () => {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
selectedAccount: {
balance: '0x0',
address: '0xAddress',
address: mockAddress1,
},
};
@ -1158,7 +1160,7 @@ describe('Send Slice', () => {
const result = sendReducer(olderState, action);
expect(result.selectedAccount.balance).toStrictEqual('0x0');
expect(result.selectedAccount.address).toStrictEqual('0xAddress');
expect(result.selectedAccount.address).toStrictEqual(mockAddress1);
});
});
@ -1167,13 +1169,13 @@ describe('Send Slice', () => {
const accountsChangedState = {
...getInitialSendStateWithExistingTxState({
fromAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
}),
stage: SEND_STAGES.EDIT,
selectedAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
};
@ -1182,7 +1184,7 @@ describe('Send Slice', () => {
type: 'ACCOUNT_CHANGED',
payload: {
account: {
address: '0xAddress',
address: mockAddress1,
balance: '0x1',
},
},
@ -1201,13 +1203,13 @@ describe('Send Slice', () => {
const accountsChangedState = {
...getInitialSendStateWithExistingTxState({
fromAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
}),
stage: SEND_STAGES.EDIT,
selectedAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
};
@ -1231,7 +1233,7 @@ describe('Send Slice', () => {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
stage: SEND_STAGES.EDIT,
selectedAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
};
@ -1274,23 +1276,23 @@ describe('Send Slice', () => {
1559: true,
},
},
selectedAddress: '0xAddress',
identities: { '0xAddress': { address: '0xAddress' } },
selectedAddress: mockAddress1,
identities: { [mockAddress1]: { address: mockAddress1 } },
keyrings: [
{
type: KeyringType.hdKeyTree,
accounts: ['0xAddress'],
accounts: [mockAddress1],
},
],
accounts: {
'0xAddress': {
address: '0xAddress',
[mockAddress1]: {
address: mockAddress1,
balance: '0x0',
},
},
cachedBalances: {
0x5: {
'0xAddress': '0x0',
[mockAddress1]: '0x0',
},
},
providerConfig: {
@ -1580,14 +1582,17 @@ describe('Send Slice', () => {
},
cachedBalances: {
[CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0',
[mockAddress1]: '0x0',
},
},
accounts: {
'0xAddress': {
address: '0xAddress',
[mockAddress1]: {
address: mockAddress1,
},
},
identities: {
[mockAddress1]: {},
},
},
send: {
...getInitialSendStateWithExistingTxState({
@ -1607,7 +1612,7 @@ describe('Send Slice', () => {
userInputHexData: '',
}),
selectedAccount: {
address: '0xAddress',
address: mockAddress1,
},
},
};
@ -2379,16 +2384,18 @@ describe('Send Slice', () => {
addressBook: {
[CHAIN_IDS.GOERLI]: {},
},
identities: {},
identities: {
[mockAddress1]: {},
},
accounts: {
'0xAddress': {
address: '0xAddress',
[mockAddress1]: {
address: mockAddress1,
balance: '0x0',
},
},
cachedBalances: {
[CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0',
[mockAddress1]: '0x0',
},
},
tokenList: {},
@ -2397,7 +2404,7 @@ describe('Send Slice', () => {
id: 1,
txParams: {
data: '',
from: '0xAddress',
from: mockAddress1,
to: '0xRecipientAddress',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00', // 1000000000
@ -2414,7 +2421,7 @@ describe('Send Slice', () => {
...getInitialSendStateWithExistingTxState({
id: 1,
fromAccount: {
address: '0xAddress',
address: mockAddress1,
},
}),
},
@ -2443,7 +2450,7 @@ describe('Send Slice', () => {
type: AssetType.native,
},
fromAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
gas: {
@ -2514,16 +2521,18 @@ describe('Send Slice', () => {
addressBook: {
[CHAIN_IDS.GOERLI]: {},
},
identities: {},
identities: {
[mockAddress1]: {},
},
accounts: {
'0xAddress': {
address: '0xAddress',
[mockAddress1]: {
address: mockAddress1,
balance: '0x0',
},
},
cachedBalances: {
[CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0',
[mockAddress1]: '0x0',
},
},
tokenList: {},
@ -2533,10 +2542,10 @@ describe('Send Slice', () => {
txParams: {
data: generateERC721TransferData({
toAddress: BURN_ADDRESS,
fromAddress: '0xAddress',
fromAddress: mockAddress1,
tokenId: BigNumber.from(15000).toString(),
}),
from: '0xAddress',
from: mockAddress1,
to: '0xNftAddress',
gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
gasPrice: '0x3b9aca00', // 1000000000
@ -2586,7 +2595,7 @@ describe('Send Slice', () => {
type: AssetType.native,
},
fromAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
gas: {
@ -2697,16 +2706,18 @@ describe('Send Slice', () => {
addressBook: {
[CHAIN_IDS.GOERLI]: {},
},
identities: {},
identities: {
[mockAddress1]: {},
},
accounts: {
'0xAddress': {
address: '0xAddress',
[mockAddress1]: {
address: mockAddress1,
balance: '0x0',
},
},
cachedBalances: {
[CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0',
[mockAddress1]: '0x0',
},
},
unapprovedTxs: {
@ -2722,7 +2733,7 @@ describe('Send Slice', () => {
decimals: 18,
},
}),
from: '0xAddress',
from: mockAddress1,
to: '0xTokenAddress',
gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
gasPrice: '0x3b9aca00', // 1000000000
@ -2740,7 +2751,7 @@ describe('Send Slice', () => {
},
}),
selectedAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
stage: SEND_STAGES.EDIT,
@ -2777,7 +2788,7 @@ describe('Send Slice', () => {
type: AssetType.native,
},
fromAccount: {
address: '0xAddress',
address: mockAddress1,
balance: '0x0',
},
gas: {

View File

@ -106,9 +106,12 @@ const ConfirmAddSuggestedNFT = () => {
}, [history, mostRecentOverviewPage, suggestedNfts]);
let origin;
let link;
if (suggestedNfts.length) {
try {
origin = new URL(suggestedNfts[0].origin)?.host;
const url = new URL(suggestedNfts[0].origin);
origin = url.host;
link = url.href;
} catch {
origin = 'dapp';
}
@ -138,7 +141,7 @@ const ConfirmAddSuggestedNFT = () => {
<ButtonLink
key={origin}
size={BUTTON_SIZES.INHERIT}
href={origin}
href={link}
target="_blank"
>
{origin}

View File

@ -281,28 +281,38 @@ export function deprecatedGetCurrentNetworkId(state) {
return state.metamask.networkId ?? 'loading';
}
/**
* Get MetaMask accounts, including account name and balance.
*/
export const getMetaMaskAccounts = createSelector(
getMetaMaskAccountsRaw,
getMetaMaskIdentities,
getMetaMaskAccountBalances,
getMetaMaskCachedBalances,
(currentAccounts, cachedBalances) =>
Object.entries(currentAccounts).reduce(
(selectedAccounts, [accountID, account]) => {
if (account.balance === null || account.balance === undefined) {
return {
...selectedAccounts,
[accountID]: {
...account,
balance: cachedBalances && cachedBalances[accountID],
},
};
}
return {
...selectedAccounts,
[accountID]: account,
(identities, balances, cachedBalances) =>
Object.keys(identities).reduce((accounts, address) => {
// TODO: mix in the identity state here as well, consolidating this
// selector with `accountsWithSendEtherInfoSelector`
let account = {};
if (balances[address]) {
account = {
...account,
...balances[address],
};
},
{},
),
}
if (account.balance === null || account.balance === undefined) {
account = {
...account,
balance: cachedBalances && cachedBalances[address],
};
}
return {
...accounts,
[address]: account,
};
}, {}),
);
export function getSelectedAddress(state) {
@ -325,11 +335,23 @@ export function getMetaMaskKeyrings(state) {
return state.metamask.keyrings;
}
/**
* Get identity state.
*
* @param {object} state - Redux state
* @returns {object} A map of account addresses to identities (which includes the account name)
*/
export function getMetaMaskIdentities(state) {
return state.metamask.identities;
}
export function getMetaMaskAccountsRaw(state) {
/**
* Get account balances state.
*
* @param {object} state - Redux state
* @returns {object} A map of account addresses to account objects (which includes the account balance)
*/
export function getMetaMaskAccountBalances(state) {
return state.metamask.accounts;
}
@ -368,7 +390,7 @@ export const getMetaMaskAccountsConnected = createSelector(
export function isBalanceCached(state) {
const selectedAccountBalance =
state.metamask.accounts[getSelectedAddress(state)].balance;
getMetaMaskAccountBalances(state)[getSelectedAddress(state)]?.balance;
const cachedBalance = getSelectedAccountCachedBalance(state);
return Boolean(!selectedAccountBalance && cachedBalance);
@ -1009,6 +1031,7 @@ function getAllowedAnnouncementIds(state) {
20: currentKeyringIsLedger && isFirefox,
21: isSwapsChain,
22: true,
24: state.metamask.hadAdvancedGasFeesSetPriorToMigration92_3 === true,
};
}
@ -1309,23 +1332,26 @@ export function getIsMultiLayerFeeNetwork(state) {
* To retrieve the maxBaseFee and priorityFee the user has set as default
*
* @param {*} state
* @returns Boolean
* @returns {{maxBaseFee: string, priorityFee: string} | undefined}
*/
export function getAdvancedGasFeeValues(state) {
return state.metamask.advancedGasFee;
}
/**
* To check if the user has set advanced gas fee settings as default with a non empty maxBaseFee and priotityFee.
*
* @param {*} state
* @returns Boolean
*/
export function getIsAdvancedGasFeeDefault(state) {
const { advancedGasFee } = state.metamask;
return (
Boolean(advancedGasFee?.maxBaseFee) && Boolean(advancedGasFee?.priorityFee)
);
// This will not work when we switch to supporting multi-chain.
// There are four non-test files that use this selector.
// advanced-gas-fee-defaults
// base-fee-input
// priority-fee-input
// useGasItemFeeDetails
// The first three are part of the AdvancedGasFeePopover
// The latter is used by the EditGasPopover
// Both of those are used in Confirmations as well as transaction-list-item
// All of the call sites have access to the GasFeeContext, which has a
// transaction object set on it, but there are currently no guarantees that
// the transaction has a chainId associated with it. To have this method
// support multichain we'll need a reliable way for the chainId of the
// transaction being modified to be available to all callsites and either
// pass it in to the selector as a second parameter, or access it at the
// callsite.
return state.metamask.advancedGasFee[getCurrentChainId(state)];
}
/**

View File

@ -600,11 +600,6 @@ describe('Selectors', () => {
priorityFee: '2',
});
});
it('#getIsAdvancedGasFeeDefault', () => {
const isAdvancedGasFeeDefault =
selectors.getIsAdvancedGasFeeDefault(mockState);
expect(isAdvancedGasFeeDefault).toStrictEqual(true);
});
it('#getAppIsLoading', () => {
const appIsLoading = selectors.getAppIsLoading(mockState);
expect(appIsLoading).toStrictEqual(false);

View File

@ -244,6 +244,9 @@ describe('Actions', () => {
'0xAnotherAddress': '0x0',
},
},
identities: {
'0xAnotherAddress': {},
},
}),
);
@ -1876,6 +1879,9 @@ describe('Actions', () => {
balance: '0x0',
},
},
identities: {
'0xFirstAddress': {},
},
cachedBalances: {
'0x1': {
'0xFirstAddress': '0x0',

View File

@ -3035,7 +3035,7 @@ export function detectNfts(): ThunkAction<
}
export function setAdvancedGasFee(
val: { maxBaseFee?: Hex; priorityFee?: Hex } | null,
val: { chainId: Hex; maxBaseFee?: Hex; priorityFee?: Hex } | null,
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return (dispatch: MetaMaskReduxDispatch) => {
dispatch(showLoadingIndication());

View File

@ -11249,15 +11249,6 @@ __metadata:
languageName: node
linkType: hard
"browser-resolve@npm:^1.11.0":
version: 1.11.3
resolution: "browser-resolve@npm:1.11.3"
dependencies:
resolve: 1.1.7
checksum: 431bfc1a17406362a3010a2c35503eb7d1253dbcb8081c1ce236ddb0b954a33d52dcaf0b07f64c0f20394d6eeec1be4f6551da3734ce9ed5dcc38e876c96d5d5
languageName: node
linkType: hard
"browser-resolve@npm:^2.0.0":
version: 2.0.0
resolution: "browser-resolve@npm:2.0.0"
@ -11361,13 +11352,13 @@ __metadata:
linkType: hard
"browserify@npm:^16.5.1":
version: 16.5.1
resolution: "browserify@npm:16.5.1"
version: 16.5.2
resolution: "browserify@npm:16.5.2"
dependencies:
JSONStream: ^1.0.3
assert: ^1.4.0
browser-pack: ^6.0.1
browser-resolve: ^1.11.0
browser-resolve: ^2.0.0
browserify-zlib: ~0.2.0
buffer: ~5.2.1
cached-path-relative: ^1.0.0
@ -11388,7 +11379,7 @@ __metadata:
insert-module-globals: ^7.0.0
labeled-stream-splicer: ^2.0.0
mkdirp-classic: ^0.5.2
module-deps: ^6.0.0
module-deps: ^6.2.3
os-browserify: ~0.3.0
parents: ^1.0.1
path-browserify: ~0.0.0
@ -11414,7 +11405,7 @@ __metadata:
xtend: ^4.0.0
bin:
browserify: bin/cmd.js
checksum: 71c02959ffbcedc6364fb77db75dbf9ac7d945747414774287202327b7be9ae390257acfbf788455a90a088daa18c5134ec7b46315b747ff51ccddb73ad2e3ea
checksum: 75dacf5c82355146b49a2febb3bf9f7898893931973cf901849791827e44782afcb562be7bc3a893d9022ae528fd6fccdf24fc8812cb5aa1b081bb7ce34c46b5
languageName: node
linkType: hard
@ -12907,11 +12898,13 @@ __metadata:
"concat-stream@npm:^2.0.0":
version: 2.0.0
resolution: "concat-stream@https://github.com/hugomrdias/concat-stream.git#commit=057bc7b5d6d8df26c8cf00a3f151b6721a0a8034"
resolution: "concat-stream@npm:2.0.0"
dependencies:
buffer-from: ^1.0.0
inherits: ^2.0.3
readable-stream: ^3.0.2
checksum: 1cef636e7061f310088706b34fe774e3960dff60a5039158b5e5c84795f6dd8a3411659324280405b8c5f1d7e8e3d4f68fa48e55963ed14953a44fef66423329
typedarray: ^0.0.6
checksum: d7f75d48f0ecd356c1545d87e22f57b488172811b1181d96021c7c4b14ab8855f5313280263dca44bb06e5222f274d047da3e290a38841ef87b59719bde967c7
languageName: node
linkType: hard
@ -14828,11 +14821,11 @@ __metadata:
linkType: hard
"dot-prop@npm:^4.1.0":
version: 5.2.0
resolution: "dot-prop@npm:5.2.0"
version: 4.2.1
resolution: "dot-prop@npm:4.2.1"
dependencies:
is-obj: ^2.0.0
checksum: 709a8208bff4fc4d5a11e357957a9e59ed625d7db909d14ea1e0dbeb30d26c25325a6e64ea27ed96fb17978cc13c7e38cf30bac17bb81eb9b5a740a4fe909a87
is-obj: ^1.0.0
checksum: 5f4f19aa440bc548670d87f2adcbd105fa6842cd1fba3165a8a2b1380568ae82862acf8ebafcc6093fa062505d7d08d7155c7ba9a88da212f7348e95ef2bdce6
languageName: node
linkType: hard
@ -16331,11 +16324,11 @@ __metadata:
"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git, ethereumjs-abi@npm:0.6.8, ethereumjs-abi@npm:^0.6.4, ethereumjs-abi@npm:^0.6.8":
version: 0.6.8
resolution: "ethereumjs-abi@https://github.com/ethereumjs/ethereumjs-abi.git#commit=ee3994657fa7a427238e6ba92a84d0b529bbcde0"
resolution: "ethereumjs-abi@npm:0.6.8"
dependencies:
bn.js: ^4.11.8
ethereumjs-util: ^6.0.0
checksum: ae074be0bb012857ab5d3ae644d1163b908a48dd724b7d2567cfde309dc72222d460438f2411936a70dc949dc604ce1ef7118f7273bd525815579143c907e336
checksum: cede2a8ae7c7e04eeaec079c2f925601a25b2ef75cf9230e7c5da63b4ea27883b35447365a47e35c1e831af520973a2252af89022c292c18a09a4607821a366b
languageName: node
linkType: hard
@ -20779,20 +20772,13 @@ __metadata:
languageName: node
linkType: hard
"is-obj@npm:^1.0.1":
"is-obj@npm:^1.0.0, is-obj@npm:^1.0.1":
version: 1.0.1
resolution: "is-obj@npm:1.0.1"
checksum: 3ccf0efdea12951e0b9c784e2b00e77e87b2f8bd30b42a498548a8afcc11b3287342a2030c308e473e93a7a19c9ea7854c99a8832a476591c727df2a9c79796c
languageName: node
linkType: hard
"is-obj@npm:^2.0.0":
version: 2.0.0
resolution: "is-obj@npm:2.0.0"
checksum: c9916ac8f4621962a42f5e80e7ffdb1d79a3fab7456ceaeea394cd9e0858d04f985a9ace45be44433bf605673c8be8810540fe4cc7f4266fc7526ced95af5a08
languageName: node
linkType: hard
"is-object@npm:^1.0.1, is-object@npm:~1.0.1":
version: 1.0.1
resolution: "is-object@npm:1.0.1"
@ -25831,7 +25817,7 @@ __metadata:
languageName: node
linkType: hard
"module-deps@npm:^6.0.0, module-deps@npm:^6.2.3":
"module-deps@npm:^6.2.3":
version: 6.2.3
resolution: "module-deps@npm:6.2.3"
dependencies:
@ -28723,7 +28709,7 @@ __metadata:
bin:
pbjs: bin/pbjs
pbts: bin/pbts
checksum: 6b7fd7540d74350d65c38f69f398c9995ae019da070e79d9cd464a458c6d19b40b07c9a026be4e10704c824a344b603307745863310c50026ebd661ce4da0663
checksum: b2fc6a01897b016c2a7e43a854ab4a3c57080f61be41e552235436e7a730711b8e89e47cb4ae52f0f065b5ab5d5989fc932f390337ce3a8ccf07203415700850
languageName: node
linkType: hard
@ -30618,13 +30604,6 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:1.1.7":
version: 1.1.7
resolution: "resolve@npm:1.1.7"
checksum: afd20873fbde7641c9125efe3f940c2a99f6b1f90f1b7b743e744bdaac1cb105b2e4e0317bcc052ed7e31d57afa86b394a4dc9a1b33a297977be134fdf0250ab
languageName: node
linkType: hard
"resolve@npm:^1.1.4, resolve@npm:^1.1.5, resolve@npm:^1.1.6, resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.10.1, resolve@npm:^1.11.1, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.17.0, resolve@npm:^1.18.1, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.21.0, resolve@npm:^1.22.0, resolve@npm:^1.22.1, resolve@npm:^1.22.3, resolve@npm:^1.4.0":
version: 1.22.3
resolution: "resolve@npm:1.22.3"
@ -30651,13 +30630,6 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@1.1.7#~builtin<compat/resolve>":
version: 1.1.7
resolution: "resolve@patch:resolve@npm%3A1.1.7#~builtin<compat/resolve>::version=1.1.7&hash=07638b"
checksum: e9dbca78600ae56835c43a09f1276876c883e4b4bbd43e2683fa140671519d2bdebeb1c1576ca87c8c508ae2987b3ec481645ac5d3054b0f23254cfc1ce49942
languageName: node
linkType: hard
"resolve@patch:resolve@^1.1.4#~builtin<compat/resolve>, resolve@patch:resolve@^1.1.5#~builtin<compat/resolve>, resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>, resolve@patch:resolve@^1.1.7#~builtin<compat/resolve>, resolve@patch:resolve@^1.10.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.10.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.11.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.15.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.17.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.18.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.21.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.3#~builtin<compat/resolve>, resolve@patch:resolve@^1.4.0#~builtin<compat/resolve>":
version: 1.22.3
resolution: "resolve@patch:resolve@npm%3A1.22.3#~builtin<compat/resolve>::version=1.22.3&hash=07638b"