Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Crowdin translations by Github Action #28996

Open
wants to merge 926 commits into
base: develop
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
926 commits
Select commit Hold shift + click to select a range
7727de3
fix: add mode logo (#29870)
salimtb Jan 23, 2025
17928df
feat: SOL-46 Adds tx history via multichain transactions controller (…
zone-live Jan 23, 2025
294512e
feat: adds fiat value to the swap totoken display (#29788)
ghgoodreau Jan 23, 2025
07b1bb4
chore: rm bridge dest token list (#29683)
micaelae Jan 23, 2025
c0349ad
fix: Fix bug where testnets do not show up correctly in the increment…
jiexi Jan 23, 2025
009ad97
chore: update accounts deps (#29867)
ccharly Jan 23, 2025
1946d07
test: remove MMI tests (#29748)
shane-t Jan 23, 2025
305ea7e
fix: fix permissions not correctly being updated when all network cli…
jiexi Jan 23, 2025
d3d81ad
fix: add sonic mainnet logo (#29700)
salimtb Jan 23, 2025
7c048f5
test: migrate more token tests and update related page objects (#29651)
cmd-ob Jan 24, 2025
2263ce4
fix: updated permissions header to be consistent (#29880)
NidhiKJha Jan 24, 2025
b810b5b
feat: Remove 'confirmation redesign' developer settings toggle (#29873)
pedronfigueiredo Jan 24, 2025
c4e4801
chore: replace `web3-stream-provider` with `StreamProvider` from `@me…
cryptodev-2s Jan 24, 2025
bdcd207
feat: Conditionally disable nonce editing when smart transactions are…
pedronfigueiredo Jan 24, 2025
00e2bae
fix: fix network filter on edit network (#29898)
salimtb Jan 24, 2025
2290ce0
fix: Fix bundle size diffs (#29862)
Gudahtt Jan 24, 2025
75be75c
refactor: modular controller init (#28948)
matthewwalsh0 Jan 24, 2025
fad926a
build: update node to v22.13.1 (#28368)
HowardBraham Jan 24, 2025
75b04c9
fix: Use latest `DecryptMessageManager` `EncryptMessageManager` to ex…
OGPoyraz Jan 27, 2025
f49a39f
fix: missing smart transaction messenger actions (#29913)
matthewwalsh0 Jan 27, 2025
b57ed09
fix: fix undici audit (5.28.4 -> 5.28.5) (#29914)
ccharly Jan 27, 2025
3db2064
feat: Update RPC URL check for smart transactions on BNB chain (#29922)
dan437 Jan 27, 2025
735e43f
fix: don't reject valid bridge statuses of failed with empty token ob…
infiniteflower Jan 27, 2025
56ec93d
chore: Bump Snaps packages (#29920)
FrederikBolding Jan 27, 2025
e2fea0f
fix: bump `@metamask/eth-ledger-bridge-keyring` to `^8.0.3` to fix Le…
dawnseeker8 Jan 28, 2025
39e4fbe
test: smart transaction e2e (#29935)
matthewwalsh0 Jan 28, 2025
a7049a6
fix: `decoding_in_progress` metric and flaky `decoding_latency` metri…
digiwand Jan 28, 2025
250b595
chore: Remove useExternalServices check for isDecodeSignatureRequestE…
pnarayanaswamy Jan 28, 2025
fcfd96f
fix: added some improvements to fix some flakiness (#29798)
javiergarciavera Jan 28, 2025
564c825
fix: Update STX Banner Alert, include `chainSupportsSmartTransactions…
httpJunkie Jan 28, 2025
d69b73e
feat: add OneKey on device selection screen (#29610)
Akaryatrh Jan 28, 2025
9782ff4
fix: Apply proper border radius to `SnapUICard` image (#29941)
FrederikBolding Jan 28, 2025
b6bf8fc
feat: xchain post submit metrics (#29291)
infiniteflower Jan 28, 2025
13c5066
test: Enhance Snap home page (#29765)
hjetpoluru Jan 28, 2025
2cf7e67
fix: reapply the fix for seeing you received when bridge tx not finis…
infiniteflower Jan 28, 2025
d9a2a12
fix: add explicit data to paramsForGasEstimate (#29946)
bfullam Jan 29, 2025
a9d90e5
feat: migrate storybook to gh actions (#29929)
itsyoboieltr Jan 29, 2025
89f8021
fix: replicate network change actions in rpc modal (#29943)
bergarces Jan 29, 2025
08365f0
chore: remove MMI UI code (#29884)
shane-t Jan 29, 2025
31e2acc
fix: Solana missing mock to `api.simplehash.com/api/v0/fungibles/asse…
seaona Jan 29, 2025
2053533
fix(action): the next semver version can not be a patch version (#29951)
gauthierpetetin Jan 29, 2025
e041dc8
refactor: connection Flow to use CAIP25 Permission format (#29824)
ffmcgee725 Jan 29, 2025
c00289d
ci: Take DOM snapshots of all windows on failure (#29983)
Gudahtt Jan 29, 2025
180641b
fix: Fix send flow max value issue (#29960)
OGPoyraz Jan 29, 2025
642fb08
chore(lavamoat/lavadome): bump to v0.0.20 (#29691)
weizman Jan 30, 2025
6ca9116
fix: storybook-deployment (#29984)
itsyoboieltr Jan 30, 2025
0be8d4a
fix: Display mailto links properly in Snaps link warning (#30000)
FrederikBolding Jan 30, 2025
5bbe82c
fix: handle null STX status as pre-enabled state (#29968)
httpJunkie Jan 30, 2025
33ffbc5
fix: :bug: fix insufficient funds displays incorrect native token (#2…
matteoscurati Jan 30, 2025
63b8dab
fix: decouple listNotifications from useProfileSyncing (#30004)
mathieuartu Jan 30, 2025
e6eec06
style: update the focus outline in the menu item component (#29753)
matteoscurati Jan 30, 2025
8534e11
refactor: Refactor state classes to prepare for state corruption back…
danjm Jan 30, 2025
267be65
fix: remove duplicate sign-in calls (#30003)
mathieuartu Jan 30, 2025
4e1b0dc
fix: update segmented `tab` styling (#29652)
matteoscurati Jan 30, 2025
51bd58e
feat: bump notifications package (#29921)
Prithpal-Sooriya Jan 30, 2025
0b202e3
fix: update alt text on NFT images (#29744)
matteoscurati Jan 30, 2025
5f76d25
chore: refactor and unify low return warning (#29918)
bfullam Jan 30, 2025
538bbc9
fix: bug when requested accounts/chainIds do not match wallet state (…
adonesky1 Jan 30, 2025
014c9dc
fix: hide network picker back button when network is unselected (#29711)
micaelae Jan 31, 2025
1f6f110
fix: flaky test `Queued Confirmations Queued Requests Banner Alert Ba…
seaona Jan 31, 2025
94d7958
feat: Delete legacy transaction confirmation code (#29926)
pedronfigueiredo Jan 31, 2025
9dae762
fix: fix granted permissions when adding an existing network via `wal…
jiexi Jan 31, 2025
7fdff0a
chore: update accounts deps (#29912)
ccharly Jan 31, 2025
c3acb6f
fix: Makes NFT list properly wrap within the send modal (#30036)
dbrans Jan 31, 2025
fc20a80
fix: Misdefined types related to `Bridge{,Status}Controller` state (#…
MajorLift Jan 31, 2025
df3931e
test: automatically install `anvil` during `yarn install` via `foundr…
davidmurdoch Jan 31, 2025
c75493d
fix: new flakiness on queued in FF for unordered events (#30031)
seaona Jan 31, 2025
bf07901
fix: flaky test timeout `Account syncing - User already has balances …
seaona Feb 3, 2025
7eae754
fix: error when pending transaction alert is trigger (#29825)
vinistevam Feb 3, 2025
74fd73c
fix: corrupted tokens state (#30046)
salimtb Feb 3, 2025
b9d63a4
feat: rls workflow (#29646)
jake-perkins Feb 3, 2025
c89f440
build: track circular dependency in the codebase (#29811)
dbrans Feb 3, 2025
2af746e
chore: Rename ControllerMessenger to Messenger (#30041)
cryptodev-2s Feb 3, 2025
2a81420
fix: remove segmented tab on confirm import modal (#29720)
matteoscurati Feb 3, 2025
600e4b1
feat: update release hash for create rls pr (#30072)
jake-perkins Feb 3, 2025
9bd0243
feat(snaps): Enable destructive footer buttons in Snap UI (#29966)
david0xd Feb 4, 2025
86334eb
feat: SOL-80 transaction details (#29323)
zone-live Feb 4, 2025
7f746e9
chore: Bump Snaps packages (#30062)
FrederikBolding Feb 4, 2025
11addf5
fix: use same logic in details page to show IPFS images (#30091)
Prithpal-Sooriya Feb 4, 2025
775eddc
fix: Readd cancel and speed up transaction modal (#30093)
pedronfigueiredo Feb 4, 2025
5e2ebff
fix: Updated snap header in review permissions screen (#30092)
NidhiKJha Feb 4, 2025
29e6a5d
chore: Bump `@metamask/snaps-controllers` to `^9.19.1` and `@metamask…
Mrtenz Feb 4, 2025
889594e
feat: update celo chain logo svg (#29106)
sodofi Feb 4, 2025
a381650
fix: cp-12.10.4 transaction resubmit on multiple endpoints (#30079)
matthewwalsh0 Feb 4, 2025
e48abe6
chore: Use network-specific smart transaction feature flags (#30094)
dan437 Feb 4, 2025
6b05509
chore: Temporarily store MMI policy validation/update (#30106)
Gudahtt Feb 4, 2025
200be79
feat(4098): customize fetchInterval for remoteFeatureFlagController t…
DDDDDanica Feb 4, 2025
af80c7b
fix: cp-12.10.4 hotfix network version / unresponsive network inpage …
jiexi Feb 4, 2025
a259fb3
fix: use intl locale for compatibility with Intl.NumberFormat (#30113)
micaelae Feb 4, 2025
6efe3a5
fix: Fix MMI LavaMoat policy update (#30117)
Gudahtt Feb 4, 2025
0474f2c
fix: fix swaps modal render (#30118)
sahar-fehri Feb 4, 2025
8835cf5
test: pin Firefox version temporarily to 134 until artifacts `tar.bz2…
seaona Feb 5, 2025
d62b731
fix: fix snaps permission flow ui (#30116)
ffmcgee725 Feb 5, 2025
b4fc88e
feat: Delete legacy signature confirmation code (#30038)
pedronfigueiredo Feb 5, 2025
efb0303
test: [POM] Migrate connections e2e tests to TS and Page Object Model…
chloeYue Feb 5, 2025
8f78340
test: Collect coverage from Snaps components (#30129)
FrederikBolding Feb 5, 2025
7fc280d
chore: Remove Snaps insight dead code (#30130)
FrederikBolding Feb 5, 2025
c09f80b
feat: mmassets-543 (#30133)
jpsains Feb 5, 2025
e7e8488
fix: default network selection logic in connection flow (#30139)
adonesky1 Feb 5, 2025
ba5f5b1
fix: cp-12.12.0 30060 lattice account import issue (#30128)
dawnseeker8 Feb 6, 2025
c4aa6a9
fix: Ensure has_marketing_consent user trait are correctly set and se…
danjm Feb 6, 2025
870b3ff
test: implement user-storage encrypted data generation for mocks (#30…
cmd-ob Feb 6, 2025
a96bd2e
fix: runway bot cherry pick (#30160)
itsyoboieltr Feb 6, 2025
6d5b3dd
feat: Add transaction alert when sending data to EOA (#30141)
pedronfigueiredo Feb 6, 2025
1359304
refactor: Refactor initialisation of Snaps controllers (#30034)
Mrtenz Feb 6, 2025
a7b562d
feat(ramps): Adds a close button to the buy banner (#28980)
georgeweiler Feb 6, 2025
f16d6e1
fix: bump assets-controllers to v48.0.0 (#30161)
sahar-fehri Feb 6, 2025
63afa20
build(webpack): add `--sentry` status to webpack's `--dry-run` output…
davidmurdoch Feb 6, 2025
6332ad8
ci: skip type imports when checking circular dependencies (#30080)
davidmurdoch Feb 6, 2025
335b3bb
fix: fix noisy sentry logs (#30163)
salimtb Feb 6, 2025
ecc7bd0
chore: update selenium (#30124)
pnarayanaswamy Feb 7, 2025
07cc430
fix: fix migration (#30189)
salimtb Feb 7, 2025
80134a1
fix: add funkichain logo (#29949)
salimtb Feb 7, 2025
0c697c8
test: Cleanup extension e2e tests code (#30121)
chloeYue Feb 7, 2025
67edd4b
test: Add basic tests for SnapUIRenderer (#30099)
FrederikBolding Feb 7, 2025
c9c2bf8
fix: integrate multichainAssetsController (#30059)
sahar-fehri Feb 7, 2025
e63ca9a
fix: fix e2e decimal (#30203)
sahar-fehri Feb 7, 2025
271c125
chore: tab component refactor (#30143)
georgewrmarshall Feb 7, 2025
36b58cd
fix: Ignore empty data field on hex data alert (#30192)
pedronfigueiredo Feb 7, 2025
43efd3f
refactor: `asset-list` and `token-list` (#29886)
gambinish Feb 7, 2025
46b456f
fix: show Bridge and Swap page header based on url query param (#30169)
micaelae Feb 7, 2025
2bd6abe
test: rename `ganacheOptions` to generic `localNodeOptions` and remov…
seaona Feb 7, 2025
5b8c780
chore: removing unused css (#30214)
georgewrmarshall Feb 7, 2025
436d69c
chore: bump controller version (#30209)
zone-live Feb 11, 2025
c5a6560
chore: update accounts deps + some related peer deps (#30205)
ccharly Feb 11, 2025
245d96f
feat: Add advanced details tooltip (#30197)
pedronfigueiredo Feb 11, 2025
2101ad0
feat: auth & profile sync logic improvements (#30174)
mathieuartu Feb 11, 2025
ca34a55
fix: `test-e2e-chrome-vault-decryption` spec with correct balance (#3…
seaona Feb 11, 2025
727b489
fix: Clicking on report link does not add external_link_clicked to si…
pnarayanaswamy Feb 11, 2025
3a17851
chore: non-evm UI updates (#30166)
zone-live Feb 11, 2025
de29767
chore: validate page object usage in new spec files on every PR (#29915)
seaona Feb 11, 2025
74d8431
fix: cp-12.12.0 bring back exception if invalid address passed in eth…
ffmcgee725 Feb 11, 2025
103cfb5
chore: upgrading design tokens from `v4.2.0` to `5.0.0` and removing …
georgewrmarshall Feb 11, 2025
d1e00a8
chore: removing more unused css (#30217)
georgewrmarshall Feb 11, 2025
15f3b81
refactor: remove circular dependencies in `ui/components/app/snaps` f…
davidmurdoch Feb 11, 2025
0eb7d57
chore: show max network fee tooltip (#30208)
infiniteflower Feb 11, 2025
93ec593
fix: cp-12.12.0 CAIP-25 Permission Migration when eth_accounts has no…
jiexi Feb 11, 2025
8005275
feat: Add e2e tests of speeding up and cancelling transactions (#30212)
pedronfigueiredo Feb 12, 2025
f750ec5
fix: Hide network fee fiat conversion on test nets (#30196)
pedronfigueiredo Feb 12, 2025
f7690e3
chore: add integration test to check nonce editing is disabled when s…
pnarayanaswamy Feb 12, 2025
584c13d
chore: bump `@metamask/base-controller` to `^8.0.0` (#30251)
cryptodev-2s Feb 12, 2025
e5a8ff9
feat: Add origin throttling modal (#29656)
OGPoyraz Feb 12, 2025
042d7cb
feat: Remove tooltip and connection badge from Connection Menu (#30232)
NidhiKJha Feb 12, 2025
103d92c
refactor: remove metametrics circular dependency (#30037)
davidmurdoch Feb 12, 2025
0ab294f
chore: adding storybook story for snap install warning component (#30…
georgewrmarshall Feb 12, 2025
fd64795
chore: adding storybook story for snap ui checkbox component (#30236)
georgewrmarshall Feb 12, 2025
297e327
test: remove unnecessary delays and wait for condition with `waitFor…
seaona Feb 12, 2025
832eeaf
fix: cp-12.12.0 improve token list rendering (#30266)
Prithpal-Sooriya Feb 12, 2025
c1c3aec
refactor: remove circular dependencies in `app/scripts/controllers/br…
davidmurdoch Feb 12, 2025
fa87b9b
refactor: update network punycode warning, isValidASCIIURL, and toPun…
digiwand Feb 12, 2025
41b7fc9
chore: show multichain network icons (#30276)
micaelae Feb 12, 2025
d98ac20
chore: adding storybook story for unconnected account alert component…
georgewrmarshall Feb 12, 2025
f38a93f
refactor: remove circular dependencies in `ui/components/component-li…
davidmurdoch Feb 12, 2025
28f8d74
chore: adding storybook story for asset picker modal network componen…
georgewrmarshall Feb 13, 2025
baf9b08
test: snap-bip-32 test scenario migration to pom (#30175)
hjetpoluru Feb 13, 2025
9104b8e
fix: Avoid nonce flicker when transaction is submitted (#30193)
pedronfigueiredo Feb 13, 2025
9c720d3
chore: Refactor the way multichain balances and transactions controll…
zone-live Feb 13, 2025
901ce6a
feat: disable metametrics when disabling basic functionality (#30210)
mathieuartu Feb 13, 2025
9f07d45
test: E2E test for Notifications Settings Syncing (#30243)
cmd-ob Feb 13, 2025
66a6c55
test: Fix e2e test spec file name (#30292)
chloeYue Feb 13, 2025
188d7d6
fix: Update multichain controllers init (#30294)
GuillaumeRx Feb 13, 2025
e259b81
chore: Update `@metamask/logo` to v4 (#30298)
Gudahtt Feb 13, 2025
9ec8eaf
test: [POM] Migrate dapp interaction e2e tests to TS and Page Object …
chloeYue Feb 13, 2025
1eacada
chore: UI improvements (#30272)
vinnyhoward Feb 13, 2025
3e73363
fix: Lint `en_GB` locale (#29967)
Gudahtt Feb 13, 2025
f7f2769
fix: add Runway to CLA Allowlist (#30302)
itsyoboieltr Feb 13, 2025
d338a5e
chore: adding appear animations to modal dialog and overlay (#30258)
georgewrmarshall Feb 13, 2025
776ef89
build: add linting TS React files (#30280)
Prithpal-Sooriya Feb 13, 2025
5899e37
refactor: `token-cell` (#30238)
gambinish Feb 13, 2025
99f5b19
fix: update fox positioning (#30310)
vinnyhoward Feb 13, 2025
1c4ae15
fix: fix network switch on dapp (#30211)
salimtb Feb 14, 2025
213037b
fix: flaky test `Settings Should show crypto value when price checker…
seaona Feb 14, 2025
1880f16
chore: bump `@metamask/api-specs` to `^0.10.15` (#30273)
jiexi Feb 14, 2025
8c81e3a
fix: bump assets-controllers to v49 (#30250)
sahar-fehri Feb 14, 2025
e7bbf08
chore(deps): update `express` (#29708)
mikesposito Feb 14, 2025
d6e21aa
fix: Add BNB Smart Chain to Smart Transactions description in setting…
httpJunkie Feb 14, 2025
2936a3c
fix: fetch quotes when src amount's decimals are greater than token's…
micaelae Feb 14, 2025
8be74f3
feat: MMS-1872 destination account picker for sol-evm (standalone com…
ghgoodreau Feb 14, 2025
346e3b8
fix(snaps): Add missing controller names to `ControllersToInitialize`…
GuillaumeRx Feb 17, 2025
cafa47a
chore: fix edge case for uploading git diff artifacts needed for POM …
seaona Feb 17, 2025
739fac8
fix(30190): fix flaky tests for user traits (#30346)
DDDDDanica Feb 17, 2025
422201c
feat: add unichain logo (#30361)
salimtb Feb 17, 2025
1f4afda
feat: integrate multichain assets rates controller to extension UI (#…
salimtb Feb 17, 2025
9f6f506
fix: cp-12.13.0 dependency version (#30375)
itsyoboieltr Feb 17, 2025
2f30c2e
fix: revisit list of currencies (#30324)
salimtb Feb 18, 2025
599a4b5
fix: Revert "fix: Avoid nonce flicker when transaction is submitted" …
pedronfigueiredo Feb 18, 2025
de15960
chore: adds call to the multichain transactions controller (#30369)
zone-live Feb 18, 2025
b5f4f0f
fix: remove supported chains check (#29773)
vinistevam Feb 18, 2025
e478643
fix: Perf: Prevent AddressCopyButton rerenders (#30289)
darkwing Feb 18, 2025
8ae2876
feat: bump notification services controller (#30339)
Prithpal-Sooriya Feb 18, 2025
6d08a65
refactor(snap-keyring): refactor Snap keyring implementation (#30244)
ccharly Feb 18, 2025
7d050e1
build: remove some unncessary autofix warnings (#30342)
Prithpal-Sooriya Feb 18, 2025
8746cf3
feat: update slides descriptions (#30270)
jonybur Feb 18, 2025
0b66f5d
fix: cp-12.13.0 fix modal scroll bar flash (#30355)
georgewrmarshall Feb 18, 2025
e758c07
feat: Bump `@metamask/providers` to `^20.0.0` (#29936)
jiexi Feb 18, 2025
2b39e3b
feat: GitHub-hosted runners for benchmarks (#29955)
HowardBraham Feb 18, 2025
fad21cd
refactor: remove circular dependency in `token-buttons.tsx` (#30299)
davidmurdoch Feb 19, 2025
159a416
docs: add code comments to better describe date formatting util (#29242)
Prithpal-Sooriya Feb 19, 2025
af66eb6
feat(action): Improve bug report creation (#30176)
gauthierpetetin Feb 19, 2025
c7e077e
feat: add `@metamask/multichain-network-controller` (#30426)
ccharly Feb 19, 2025
f73fd30
chore: bump `@metamask/keyring-controller` to `^19.1.0` (#30367)
mikesposito Feb 19, 2025
0832bdd
fix: flaky tests `Test Snap bip-44 can pop up bip-44 snap and get pri…
seaona Feb 19, 2025
2abb96a
chore: Bump Snaps dependencies (#30396)
FrederikBolding Feb 19, 2025
a19a398
fix: fix spinner display in NFT tab (#30427)
sahar-fehri Feb 19, 2025
03c5f58
feat: MMS-1868 new quote card and story (#30303)
ghgoodreau Feb 19, 2025
67aa6aa
chore: set swap input parameters (#30284)
micaelae Feb 19, 2025
c1f1f6b
fix: text visibility issues in error page in dark mode (#30408)
georgewrmarshall Feb 19, 2025
f28216f
fix: cp-12.13.0 fixes drag and drop in network list menu modal (#30437)
georgewrmarshall Feb 19, 2025
e04e2f7
refactor: remove circular dependency between `actions.ts` and `swaps.…
davidmurdoch Feb 19, 2025
468ab8f
fix: flaky test `Speed Up and Cancel Transaction Tests Cancel transac…
seaona Feb 19, 2025
42bd11d
fix: cp-12.13.0 Disable origin throttling middleware if cause is "rej…
OGPoyraz Feb 19, 2025
7b03791
refactor: remove circular dependencies in `ui/pages/snap-account-redi…
davidmurdoch Feb 19, 2025
0e3f87d
chore: Remove unnecessary resolutions (#30446)
Gudahtt Feb 19, 2025
9eabbc6
feat: re-enable account syncing (#30464)
mathieuartu Feb 20, 2025
eeeb584
chore: bump @metamask/utils to 11.1.0 (#30467)
sahar-fehri Feb 20, 2025
bab4d50
fix: flaky test `Full-size View Setting opens the extension in popup …
seaona Feb 20, 2025
94e764b
refactor: Expose E2E Chrome driver extension ID (#30444)
Gudahtt Feb 20, 2025
94643bf
refactor: remove circular dependency in `name-details.tsx` (#30414)
davidmurdoch Feb 20, 2025
68f20ec
refactor: remove circular dependency from `notifications.tsx` and `no…
davidmurdoch Feb 20, 2025
c86416d
refactor: remove circular dependencies from `account-picker.tsx` (#30…
davidmurdoch Feb 20, 2025
f2d52fb
fix(25182): reloading page during SRP creation breaks flow (#30178)
vinnyhoward Feb 20, 2025
e96b5db
refactor: remove circular dependencies from `ui/components/*` (#30457)
davidmurdoch Feb 20, 2025
eaa9b51
refactor: remove circular dependency from `confirm-alert-model.tsx` (…
davidmurdoch Feb 20, 2025
5a84737
feat(27255): allow local modification for remote feature flags (#29696)
DDDDDanica Feb 21, 2025
dc2249c
test: adds Anvil classes + Viem and migrates first specs from Ganache…
seaona Feb 21, 2025
ff041ab
fix: Add transaction simulation supported networks global mock (#30507)
Gudahtt Feb 21, 2025
3e51501
fix: Fix attribution generation (#30498)
Gudahtt Feb 21, 2025
c7a1c2f
fix: flaky tests `Confirmation Redesign Token Send ERC1155 Wallet ini…
seaona Feb 21, 2025
ee40f2f
feat: make Snap account creation flow async (#30406)
ccharly Feb 21, 2025
472b5d9
chore(bitcoin): add bitcoin build feature + disable it temporarily (#…
ccharly Feb 21, 2025
2c72002
fix: Handle nullish value in `alphanumeric sort` (#30500)
gambinish Feb 22, 2025
a245dc3
feat: add multi-srp support to the background script (#29942)
montelaidev Feb 24, 2025
aa2cd60
chore: multichain tx package update (#30499)
zone-live Feb 24, 2025
f6a628b
chore: assets controller package update (#30526)
zone-live Feb 24, 2025
d03d6f9
feat(multichain): add block explorer format URLs (#30085)
ccharly Feb 24, 2025
16c2173
chore: adds the multichain and solana fence to the needed places (#30…
zone-live Feb 24, 2025
4ec2d26
feat(3765): Add modal to include metric id before redirecting to supp…
DDDDDanica Feb 24, 2025
f4c3889
fix: Hide non-zero hex data alert for contract deployment confirmatio…
pedronfigueiredo Feb 24, 2025
a8804d3
refactor: bridge support for CAIP chainIds and non-hex addresses (#30…
micaelae Feb 24, 2025
ffe7ec5
feat: Integrate SPL tokens and rates from multichainAssetsRates (#30389)
sahar-fehri Feb 24, 2025
5a44781
feat: beta support link (#30482)
aganglada Feb 24, 2025
cfa0467
feat: account sync - primary SRP filtering, bulk accounts creation an…
mathieuartu Feb 24, 2025
da8838f
refactor: Use browser.runtime.onInstalled instead of a check for empt…
danjm Feb 24, 2025
32af047
feat(multichain-connect-ui): Add UI preparation changes for multichai…
david0xd Feb 24, 2025
78c5d2c
fix: Nft chainId and current global chainId arent always the same (#3…
gambinish Feb 25, 2025
32c2077
feat(snap-keyring): handle `displayAccountNameSuggestion` flag (#30531)
danroc Feb 25, 2025
0ad093f
feat: replace experimental add solana account with remote flag (#30487)
aganglada Feb 25, 2025
2fffd78
fix: Add `snap_experimentalProviderRequest` to unrestricted methods (…
Mrtenz Feb 25, 2025
5ddd7e9
fix: clear transaction data after submission or cancellation (#30546)
pedronfigueiredo Feb 25, 2025
0d4ff87
New Crowdin translations by Github Action
metamaskbot Feb 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: Refactor state classes to prepare for state corruption back…
…up mitigation (#29745)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

This PR replaces
#24016

This PR does the following:

Refactors `local-store.js` and `network-store.js` to `ExtensionStore.ts`
and `ReadOnlyNetworkStore.ts`. These two classes are extensions of the
new `BaseStore` class.

The new `BaseStore` class: "is an abstract class designed to be extended
by other classes that implement the abstract methods `set` and `get`.
This class provides the foundation for different storage
implementations, enabling them to adhere to a consistent interface for
retrieving and setting application state." In future PRs, new classes
that use the indexeddb api, and the localStorage api in the offscreen
document, will be added that also extend the BaseStore class.

The BaseStore class has a simple interface with just a `get` and `set`
method. Accordingly, `ExtensionSotre` and `ReadOnlyNetworkStore` are
significantly simplified compared to their predecessors. The
functionality for managing data (e.g. handling error states, handling
metadata, etc) that used to be in `local-store.js` and
`network-store.js` is now in a new `PersistanceManager` class.

The new `PersistanceManager` class: "The PersistanceManager class serves
as a high-level manager for handling storage-related operations using a
local storage system. It provides methods to read and write state,
manage metadata, and handle errors or corruption in the underlying
storage system."

This is a step towards providing a fail safe for state corruption. The
new classes will be utilized for this additional functionality.

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29745?quickstart=1)

## **Related issues**

Fixes:

## **Manual testing steps**

No functional changes should have occurred. Manual regression cases to
test include:

- make some changes such as a adding a network or changing settings.
Close the browser. Re-open the browser and open metamask. The data
should be unchanged.
- Install an old version. Make some changes. Upgrade to the build
generated from this branch. Verify that the state of the wallet, and its
data, is as expected.

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: Brad Decker <[email protected]>
Co-authored-by: Mark Stacey <[email protected]>
3 people authored Jan 30, 2025
commit 8534e11e37ecacc3d8e9398ffbc6195da94bfe5c
64 changes: 30 additions & 34 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
@@ -49,11 +49,12 @@ import {
import { getCurrentChainId } from '../../shared/modules/selectors/networks';
import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp';
import { checkURLForProviderInjection } from '../../shared/modules/provider-injection';
import { PersistenceManager } from './lib/stores/persistence-manager';
import ExtensionStore from './lib/stores/extension-store';
import ReadOnlyNetworkStore from './lib/stores/read-only-network-store';
import migrations from './migrations';
import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension';
import LocalStore from './lib/local-store';
import ReadOnlyNetworkStore from './lib/network-store';
import { SENTRY_BACKGROUND_STATE } from './constants/sentry-state';

import createStreamSink from './lib/createStreamSink';
@@ -63,7 +64,6 @@ import NotificationManager, {
import MetamaskController, {
METAMASK_CONTROLLER_EVENTS,
} from './metamask-controller';
import rawFirstTimeState from './first-time-state';
import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code';
import getObjStructure from './lib/getObjStructure';
import setupEnsIpfsResolver from './lib/ens-ipfs/setup';
@@ -72,8 +72,9 @@ import {
getPlatform,
shouldEmitDappViewedEvent,
} from './lib/util';
import { generateWalletState } from './fixtures/generate-wallet-state';
import { createOffscreen } from './offscreen';
import { generateWalletState } from './fixtures/generate-wallet-state';
import rawFirstTimeState from './first-time-state';

/* eslint-enable import/first */

@@ -87,9 +88,17 @@ const BADGE_MAX_COUNT = 9;

// Setup global hook for improved Sentry state snapshots during initialization
const inTest = process.env.IN_TEST;
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
const migrator = new Migrator({
migrations,
defaultVersion: process.env.WITH_STATE
? FIXTURE_STATE_METADATA_VERSION
: null,
});

const localStore = inTest ? new ReadOnlyNetworkStore() : new ExtensionStore();
const persistenceManager = new PersistenceManager({ localStore });
global.stateHooks.getMostRecentPersistedState = () =>
localStore.mostRecentRetrievedState;
persistenceManager.mostRecentRetrievedState;

const { sentry } = global;
let firstTimeState = { ...rawFirstTimeState };
@@ -113,11 +122,11 @@ let uiIsTriggering = false;
const openMetamaskTabsIDs = {};
const requestAccountTabIds = {};
let controller;
let versionedData;
const tabOriginMapping = {};

if (inTest || process.env.METAMASK_DEBUG) {
global.stateHooks.metamaskGetState = localStore.get.bind(localStore);
global.stateHooks.metamaskGetState =
persistenceManager.get.bind(persistenceManager);
}

const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
@@ -599,12 +608,6 @@ async function loadPhishingWarningPage() {
*/
export async function loadStateFromPersistence() {
// migrations
const migrator = new Migrator({
migrations,
defaultVersion: process.env.WITH_STATE
? FIXTURE_STATE_METADATA_VERSION
: null,
});
migrator.on('error', console.warn);

if (process.env.WITH_STATE) {
@@ -614,31 +617,22 @@ export async function loadStateFromPersistence() {

// read from disk
// first from preferred, async API:
versionedData =
(await localStore.get()) || migrator.generateInitialState(firstTimeState);

// check if somehow state is empty
// this should never happen but new error reporting suggests that it has
// for a small number of users
// https://github.com/metamask/metamask-extension/issues/3919
if (versionedData && !versionedData.data) {
// unable to recover, clear state
versionedData = migrator.generateInitialState(firstTimeState);
sentry.captureMessage('MetaMask - Empty vault found - unable to recover');
}
const preMigrationVersionedData =
(await persistenceManager.get()) ||
migrator.generateInitialState(firstTimeState);

// report migration errors to sentry
migrator.on('error', (err) => {
// get vault structure without secrets
const vaultStructure = getObjStructure(versionedData);
const vaultStructure = getObjStructure(preMigrationVersionedData);
sentry.captureException(err, {
// "extra" key is required by Sentry
extra: { vaultStructure },
});
});

// migrate data
versionedData = await migrator.migrateData(versionedData);
const versionedData = await migrator.migrateData(preMigrationVersionedData);
if (!versionedData) {
throw new Error('MetaMask - migrator returned undefined');
} else if (!isObject(versionedData.meta)) {
@@ -656,10 +650,10 @@ export async function loadStateFromPersistence() {
);
}
// this initializes the meta/version data as a class variable to be used for future writes
localStore.setMetadata(versionedData.meta);
persistenceManager.setMetadata(versionedData.meta);

// write to disk
localStore.set(versionedData.data);
persistenceManager.set(versionedData.data);

// return just the data
return versionedData;
@@ -821,7 +815,7 @@ export function setupController(
getOpenMetamaskTabsIds: () => {
return openMetamaskTabsIDs;
},
localStore,
persistenceManager,
overrides,
isFirstMetaMaskControllerSetup,
currentMigrationVersion: stateMetadata.version,
@@ -845,7 +839,7 @@ export function setupController(
storeAsStream(controller.store),
debounce(1000),
createStreamSink(async (state) => {
await localStore.set(state);
await persistenceManager.set(state);
statePersistenceEvents.emit('state-persisted', state);
}),
(error) => {
@@ -1304,12 +1298,14 @@ const addAppInstalledEvent = () => {

// On first install, open a new tab with MetaMask
async function onInstall() {
const storeAlreadyExisted = Boolean(await localStore.get());
const storeAlreadyExisted = Boolean(await persistenceManager.get());
// If the store doesn't exist, then this is the first time running this script,
// and is therefore an install
if (process.env.IN_TEST) {
addAppInstalledEvent();
} else if (!storeAlreadyExisted && !process.env.METAMASK_DEBUG) {
// If storeAlreadyExisted is true then this is a fresh installation
// and an app installed event should be tracked.
addAppInstalledEvent();
platform.openExtensionInBrowser();
}
@@ -1358,7 +1354,7 @@ async function initBackground() {
window.document?.documentElement?.classList.add('controller-loaded');
}
}
localStore.cleanUpMostRecentRetrievedState();
persistenceManager.cleanUpMostRecentRetrievedState();
} catch (error) {
log.error(error);
}
139 changes: 0 additions & 139 deletions app/scripts/lib/local-store.js

This file was deleted.

166 changes: 0 additions & 166 deletions app/scripts/lib/local-store.test.js

This file was deleted.

98 changes: 0 additions & 98 deletions app/scripts/lib/network-store.js

This file was deleted.

13 changes: 8 additions & 5 deletions app/scripts/lib/setup-initial-state-hooks.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { maskObject } from '../../../shared/modules/object.utils';
import ExtensionPlatform from '../platforms/extension';
import { SENTRY_BACKGROUND_STATE } from '../constants/sentry-state';
import LocalStore from './local-store';
import ReadOnlyNetworkStore from './network-store';
import ReadOnlyNetworkStore from './stores/read-only-network-store';
import ExtensionStore from './stores/extension-store';
import { PersistenceManager } from './stores/persistence-manager';

const platform = new ExtensionPlatform();

// This instance of `localStore` is used by Sentry to get the persisted state
const sentryLocalStore = process.env.IN_TEST
? new ReadOnlyNetworkStore()
: new LocalStore();
const sentryLocalStore = new PersistenceManager({
localStore: process.env.IN_TEST
? new ReadOnlyNetworkStore()
: new ExtensionStore(),
});

/**
* Get the persisted wallet state.
48 changes: 48 additions & 0 deletions app/scripts/lib/stores/base-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* This type is used to represent the state tree of MetaMask.
*/
export type MetaMaskStateType = Record<string, unknown>;

/**
* This type represents the 'meta' key on the state object. This key is used to
* store the current version of the state tree as set in the various migrations
* ran by the migrator. This key is used to determine if the state tree should
* be updated when the extension is loaded, by comparing the version to the
* target versions of the migrations.
*/
export type MetaData = { version: number };

/**
* This type represents the structure of the storage object that is saved in
* extension storage. This object has two keys, 'data' and 'meta'. The 'data'
* key is the entire state tree of MetaMask and the meta key contains an object
* with a single key 'version' that is the current version of the state tree.
*/
export type MetaMaskStorageStructure = {
data?: MetaMaskStateType;
meta?: MetaData;
};

/**
* The BaseStore class is an abstract class designed to be extended by other
* classes that implement the abstract methods `set` and `get`. This class
* provides the foundation for different storage implementations, enabling
* them to adhere to a consistent interface for retrieving and setting
* application state.
*
* Responsibilities of extending classes:
* 1. **Retrieve State:**
* - Implement a `get` method that retrieves the current state from the
* underlying storage system. This method should return `null` when the
* state is unavailable.
*
* 2. **Set State:**
* - Implement a `set` method that updates the state in the underlying
* storage system. This method should handle necessary validation or
* error handling to ensure the state is persisted correctly.
*/
export abstract class BaseStore {
abstract set(state: MetaMaskStateType): Promise<void>;

abstract get(): Promise<MetaMaskStorageStructure | null>;
}
95 changes: 95 additions & 0 deletions app/scripts/lib/stores/extension-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import browser from 'webextension-polyfill';
import ExtensionStore from './extension-store';

const MOCK_STATE = { data: {}, meta: { version: 1 } };

global.sentry = global.sentry || {};

jest.mock('webextension-polyfill', () => ({
runtime: { lastError: null },
storage: { local: true },
}));

const setup = (
options: { localMock?: { get?: unknown; set?: unknown } | false } = {},
) => {
if (typeof options.localMock === 'undefined') {
// @ts-expect-error Mock used just to spy on calls, doesn't implement API
jest.replaceProperty(browser.storage, 'local', jest.fn());
} else if (options.localMock === false) {
const storageApi: Partial<typeof browser.storage> = { ...browser.storage };
delete storageApi.local;
// @ts-expect-error Intentionally incomplete to test behavior when API is missing
jest.replaceProperty(browser, 'storage', storageApi);
} else {
// @ts-expect-error Incomplete mock, it just has the properties we call
jest.replaceProperty(browser.storage, 'local', options.localMock);
}
return new ExtensionStore();
};
describe('ExtensionStore', () => {
beforeEach(() => {
jest.replaceProperty(global, 'sentry', { captureException: jest.fn() });
});

afterEach(() => {
jest.resetModules();
});
describe('constructor', () => {
it('sets isSupported property to false when browser does not support local storage', () => {
const localStore = setup({ localMock: false });

expect(localStore.isSupported).toBe(false);
});

it('sets isSupported property to true when browser supports local storage', () => {
const localStore = setup();
expect(localStore.isSupported).toBe(true);
});
});

describe('set', () => {
it('throws an error if called in a browser that does not support local storage', async () => {
const localStore = setup({ localMock: false });
await expect(() => localStore.set(MOCK_STATE)).rejects.toThrow(
'Metamask- cannot persist state to local store as this browser does not support this action',
);
});

it('throws an error if passed a valid argument and metadata has been set', async () => {
const setMock = jest.fn();

const localStore = setup({ localMock: { set: setMock } });
await expect(async function () {
localStore.set(MOCK_STATE);
}).not.toThrow();
});

it('calls the browser storage.local.set method', async () => {
const setMock = jest.fn();
const localStore = setup({ localMock: { set: setMock } });

await localStore.set(MOCK_STATE);

expect(setMock).toHaveBeenCalledWith(MOCK_STATE);
});
});

describe('get', () => {
it('returns null if called in a browser that does not support local storage', async () => {
const localStore = setup({ localMock: false });
const result = await localStore.get();
expect(result).toBe(null);
});

it('returns state returned by the browser storage.local.get method', async () => {
const getMock = jest.fn().mockResolvedValue(MOCK_STATE);
const localStore = setup({ localMock: { get: getMock } });

const result = await localStore.get();

expect(result).toBe(MOCK_STATE);
expect(getMock).toHaveBeenCalledWith(null);
});
});
});
59 changes: 59 additions & 0 deletions app/scripts/lib/stores/extension-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import browser from 'webextension-polyfill';
import log from 'loglevel';
import {
type MetaMaskStateType,
type MetaMaskStorageStructure,
BaseStore,
} from './base-store';

/**
* An implementation of the MetaMask Extension BaseStore system that uses the
* browser.storage.local API to persist and retrieve state.
*/
export default class ExtensionStore extends BaseStore {
isSupported: boolean;

constructor() {
super();
this.isSupported = Boolean(browser.storage.local);
if (!this.isSupported) {
log.error('Storage local API not available.');
}
}

/**
* Return all data in `local` extension storage area.
*
* @returns All data stored`local` extension storage area.
*/
async get(): Promise<MetaMaskStorageStructure | null> {
if (!this.isSupported) {
log.error('Storage local API not available.');
return null;
}
const { local } = browser.storage;
return await local.get(null);
}

/**
* Overwrite data in `local` extension storage area
*
* @param obj - The data to set
* @param obj.data - The MetaMask State tree
* @param obj.meta - The metadata object
* @param obj.meta.version - The version of the state tree determined by the
* migration
*/
async set(obj: {
data: MetaMaskStateType;
meta: { version: number };
}): Promise<void> {
if (!this.isSupported) {
throw new Error(
'Metamask- cannot persist state to local store as this browser does not support this action',
);
}
const { local } = browser.storage;
return await local.set(obj);
}
}
166 changes: 166 additions & 0 deletions app/scripts/lib/stores/persistence-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// PersistanceManager.test.ts
import { captureException } from '@sentry/browser';
import log from 'loglevel';

import { PersistenceManager } from './persistence-manager';
import ExtensionStore from './extension-store';
import { MetaMaskStateType } from './base-store';

const MOCK_DATA = { config: { foo: 'bar' } };

const mockStoreSet = jest.fn();
const mockStoreGet = jest.fn();

jest.mock('./extension-store', () => {
return jest.fn().mockImplementation(() => {
return { set: mockStoreSet, get: mockStoreGet };
});
});
jest.mock('./read-only-network-store');
jest.mock('@sentry/browser', () => ({
captureException: jest.fn(),
}));
jest.mock('loglevel', () => ({
error: jest.fn(),
}));

describe('PersistenceManager', () => {
let manager: PersistenceManager;

beforeEach(() => {
jest.clearAllMocks();
manager = new PersistenceManager({ localStore: new ExtensionStore() });
});

describe('set', () => {
it('throws if state is missing', async () => {
await expect(
manager.set(undefined as unknown as MetaMaskStateType),
).rejects.toThrow('MetaMask - updated state is missing');
});

it('throws if metadata has not been set', async () => {
await expect(manager.set({ appState: { test: true } })).rejects.toThrow(
'MetaMask - metadata must be set before calling "set"',
);
});

it('calls localStore.set with the correct arguments once metadata is set', async () => {
manager.setMetadata({ version: 10 });

await manager.set({ appState: { test: true } });

expect(mockStoreSet).toHaveBeenCalledTimes(1);
expect(mockStoreSet).toHaveBeenCalledWith({
data: { appState: { test: true } },
meta: { version: 10 },
});
});

it('logs error and captures exception if store.set throws', async () => {
manager.setMetadata({ version: 10 });

const error = new Error('store.set error');
mockStoreSet.mockRejectedValueOnce(error);

await manager.set({ appState: { broken: true } });
expect(captureException).toHaveBeenCalledWith(error);
expect(log.error).toHaveBeenCalledWith(
'error setting state in local store:',
error,
);
});

it('captures exception only once if store.set is called and throws multiple times', async () => {
manager.setMetadata({ version: 10 });

const error = new Error('store.set error');
mockStoreSet.mockRejectedValue(error);

await manager.set({ appState: { broken: true } });
await manager.set({ appState: { broken: true } });

expect(captureException).toHaveBeenCalledTimes(1);
});

it('captures exception twice if store.set fails, then succeeds and then fails again', async () => {
manager.setMetadata({ version: 17 });

const error = new Error('store.set error');
mockStoreSet.mockRejectedValueOnce(error);

await manager.set({ appState: { broken: true } });

mockStoreSet.mockReturnValueOnce({
data: { appState: { broken: true } },
});
await manager.set({ appState: { broken: true } });

mockStoreSet.mockRejectedValueOnce(error);

await manager.set({ appState: { broken: true } });

expect(captureException).toHaveBeenCalledTimes(2);
});
});

describe('get', () => {
it('returns undefined and clears mostRecentRetrievedState if store returns empty', async () => {
mockStoreGet.mockReturnValueOnce({});
const result = await manager.get();
expect(result).toBeUndefined();
expect(manager.mostRecentRetrievedState).toBeNull();
});

it('returns undefined if store returns null', async () => {
mockStoreGet.mockReturnValueOnce(null);
const result = await manager.get();
expect(result).toBeUndefined();
expect(manager.mostRecentRetrievedState).toBeNull();
});

it('updates mostRecentRetrievedState if extension has not been initialized', async () => {
mockStoreGet.mockResolvedValueOnce({ data: MOCK_DATA });

const result = await manager.get();
expect(result).toStrictEqual({ data: MOCK_DATA });
expect(manager.mostRecentRetrievedState).toStrictEqual({
data: MOCK_DATA,
});
});

it('does not overwrite mostRecentRetrievedState if already initialized', async () => {
mockStoreGet.mockResolvedValueOnce({ data: MOCK_DATA });
// First call to get -> sets isExtensionInitialized = false -> sets mostRecentRetrievedState
await manager.get();
// The act of calling set will set isExtensionInitialized to true
manager.setMetadata({ version: 10 });
await manager.set({ appState: { test: true } });

// Now call get() again; it should not change mostRecentRetrievedState
mockStoreGet.mockResolvedValueOnce({
data: { config: { newData: true } },
});
await manager.get();
expect(manager.mostRecentRetrievedState).toStrictEqual({
data: MOCK_DATA,
});
});
});

describe('cleanUpMostRecentRetrievedState', () => {
it('sets mostRecentRetrievedState to null if previously set', async () => {
mockStoreGet.mockResolvedValueOnce({ data: MOCK_DATA });

await manager.get();
manager.cleanUpMostRecentRetrievedState();
expect(manager.mostRecentRetrievedState).toBeNull();
});

it('leaves mostRecentRetrievedState as null if already null', async () => {
expect(manager.mostRecentRetrievedState).toBeNull();
manager.cleanUpMostRecentRetrievedState();
expect(manager.mostRecentRetrievedState).toBeNull();
});
});
});
123 changes: 123 additions & 0 deletions app/scripts/lib/stores/persistence-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import log from 'loglevel';
import { captureException } from '@sentry/browser';
import { isEmpty } from 'lodash';
import { type MetaMaskStateType, MetaMaskStorageStructure } from './base-store';
import ExtensionStore from './extension-store';
import ReadOnlyNetworkStore from './read-only-network-store';

/**
* The PersistenceManager class serves as a high-level manager for handling
* storage-related operations using a local storage system. It provides methods to read
* and write state, manage metadata, and handle errors or corruption in the
* underlying storage system.
*
* Key Responsibilities:
*
* 1. **State Management:**
* - Tracks the most recently retrieved state
* - reads state from the storage system
* - writes updated state to the storage system
*
* 2. **Metadata Handling:**
* - Manages a `metadata` object containing versioning information for the
* state tree. The version is used to ensure consistency and proper
* handling of migrations.
*
* 3. **Error Management:**
* - Tracks whether data persistence is failing and logs appropriate errors
* - Captures exceptions during write operations and reports them using
* Sentry
*
*
* Usage:
* The `PersistenceManager` is instantiated with a `localStore`, which is an
* implementation of the `BaseStore` class (either `ExtensionStore` or
* `ReadOnlyNetworkStore`). It provides methods for setting and retrieving
* state, managing metadata, and handling cleanup tasks.
*/
export class PersistenceManager {
/**
* dataPersistenceFailing is a boolean that is set to true if the storage
* system attempts to write state and the write operation fails. This is only
* used as a way of deduplicating error reports sent to sentry as it is
* likely that multiple writes will fail concurrently.
*/
#dataPersistenceFailing: boolean = false;

/**
* mostRecentRetrievedState is a property that holds the most recent state
* successfully retrieved from memory. Due to the nature of async read
* operations it is beneficial to have a near real-time snapshot of the state
* for sending data to sentry as well as other developer tooling.
*/
#mostRecentRetrievedState: MetaMaskStorageStructure | null = null;

/**
* metadata is a property that holds the current metadata object. This object
* includes a single key which is 'version' and contains the current version
* number of the state tree.
*/
#metadata?: { version: number };

#isExtensionInitialized: boolean = false;

#localStore: ExtensionStore | ReadOnlyNetworkStore;

constructor({
localStore,
}: {
localStore: ExtensionStore | ReadOnlyNetworkStore;
}) {
this.#localStore = localStore;
}

setMetadata(metadata: { version: number }) {
this.#metadata = metadata;
}

async set(state: MetaMaskStateType) {
if (!state) {
throw new Error('MetaMask - updated state is missing');
}
if (!this.#metadata) {
throw new Error('MetaMask - metadata must be set before calling "set"');
}
try {
await this.#localStore.set({ data: state, meta: this.#metadata });
if (this.#dataPersistenceFailing) {
this.#dataPersistenceFailing = false;
}
} catch (err) {
if (!this.#dataPersistenceFailing) {
this.#dataPersistenceFailing = true;
captureException(err);
}
log.error('error setting state in local store:', err);
} finally {
this.#isExtensionInitialized = true;
}
}

async get() {
const result = await this.#localStore.get();

if (isEmpty(result)) {
this.#mostRecentRetrievedState = null;
return undefined;
}
if (!this.#isExtensionInitialized) {
this.#mostRecentRetrievedState = result;
}
return result;
}

get mostRecentRetrievedState() {
return this.#mostRecentRetrievedState;
}

cleanUpMostRecentRetrievedState() {
if (this.#mostRecentRetrievedState) {
this.#mostRecentRetrievedState = null;
}
}
}
138 changes: 138 additions & 0 deletions app/scripts/lib/stores/read-only-network-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import log from 'loglevel';
import nock from 'nock';
import ReadOnlyNetworkStore from './read-only-network-store';

const FIXTURE_SERVER_HOST = 'localhost';
const FIXTURE_SERVER_PORT = 12345;
const FIXTURE_SERVER_ORIGIN = `http://${FIXTURE_SERVER_HOST}:${FIXTURE_SERVER_PORT}`;
const FIXTURE_SERVER_PATH = '/state.json';

const DEFAULT_INITIAL_STATE = {
data: { config: {} },
};

const MOCK_STATE = { data: { config: { foo: 'bar' } }, meta: { version: 1 } };

/**
* Initiatilizes a ReadOnlyNetworkStore for testing
*
* @returns store - a ReadOnlyNetworkStore
*/
function setupReadOnlyNetworkStore() {
const store = new ReadOnlyNetworkStore();
return store;
}

/**
* Create a Nock scope for the fixture server response.
*
* @returns A Nock interceptor for the fixture server response.
*/
function mockFixtureServerInterceptor(): nock.Interceptor {
return nock(FIXTURE_SERVER_ORIGIN).get(FIXTURE_SERVER_PATH);
}

/**
* Create a Nock scope for the fixture server response, which will have a successful reply.
*
* @param state
*/
function setMockFixtureServerReply(
state: Record<string, unknown> = DEFAULT_INITIAL_STATE,
): void {
mockFixtureServerInterceptor().reply(200, state);
}

describe('ReadOnlyNetworkStore', () => {
beforeEach(() => {
jest.resetModules();
nock.cleanAll();
});

describe('constructor', () => {
it('loads state from the network if fetch is successful and response is ok', async () => {
setMockFixtureServerReply(MOCK_STATE);
const store = setupReadOnlyNetworkStore();

const result = await store.get();

expect(result).toStrictEqual(MOCK_STATE);
});

it('does not throw, and logs a debug message, if fetch is not okay', async () => {
const logDebugSpy = jest
.spyOn(log, 'debug')
.mockImplementation(() => undefined);
mockFixtureServerInterceptor().reply(400);
const store = setupReadOnlyNetworkStore();

const result = await store.get();

expect(result).toBe(null);
expect(logDebugSpy).toHaveBeenCalledWith(
'Received response with a status of 400 Bad Request',
);
});

it('does not throw, and logs a debug message, if fetch throws an error', async () => {
mockFixtureServerInterceptor().replyWithError('error!');
const logDebugSpy = jest
.spyOn(log, 'debug')
.mockImplementation(() => undefined);
const store = setupReadOnlyNetworkStore();

const result = await store.get();

expect(result).toBe(null);
expect(logDebugSpy).toHaveBeenCalledWith(
"Error loading network state: 'request to http://localhost:12345/state.json failed, reason: error!'",
);
});
});

describe('get', () => {
it('returns null if #state is null', async () => {
mockFixtureServerInterceptor().reply(200);
const store = setupReadOnlyNetworkStore();

const result = await store.get();

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

it('returns null if state is null', async () => {
setMockFixtureServerReply(MOCK_STATE);
const store = setupReadOnlyNetworkStore();

const result = await store.get();

expect(result).toStrictEqual(MOCK_STATE);
});
});

describe('set', () => {
it('throws if not passed a state parameter', async () => {
const store = setupReadOnlyNetworkStore();

await expect(
// @ts-expect-error Intentionally passing incorrect type
store.set(undefined),
).rejects.toThrow('MetaMask - updated state is missing');
});

it('sets the state', async () => {
const store = setupReadOnlyNetworkStore();

await store.set({
data: { appState: { test: true } },
meta: { version: 10 },
});
const result = await store.get();

expect(result).toStrictEqual({
data: { appState: { test: true } },
meta: { version: 10 },
});
});
});
});
85 changes: 85 additions & 0 deletions app/scripts/lib/stores/read-only-network-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import log from 'loglevel';
import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout';
import {
type MetaMaskStateType,
BaseStore,
MetaMaskStorageStructure,
} from './base-store';

const fetchWithTimeout = getFetchWithTimeout();

const FIXTURE_SERVER_HOST = 'localhost';
const FIXTURE_SERVER_PORT = 12345;
const FIXTURE_SERVER_URL = `http://${FIXTURE_SERVER_HOST}:${FIXTURE_SERVER_PORT}/state.json`;

/**
* A read-only network-based storage wrapper
*/
export default class ReadOnlyNetworkStore extends BaseStore {
#initialized: boolean = false;

#initializing?: Promise<void>;

#state: MetaMaskStateType | null = null;

constructor() {
super();
this.#initializing = this.#init();
}

/**
* Declares this store as compatible with the current browser
*/
isSupported = true;

/**
* Initializes by loading state from the network
*/
async #init() {
try {
const response = await fetchWithTimeout(FIXTURE_SERVER_URL);

if (response.ok) {
this.#state = await response.json();
} else {
log.debug(
`Received response with a status of ${response.status} ${response.statusText}`,
);
}
} catch (error) {
console.log('error', error);
if (error instanceof Error) {
log.debug(`Error loading network state: '${error.message}'`);
} else {
log.debug(`Error loading network state: An unknown error occurred`);
}
} finally {
this.#initialized = true;
}
}

/**
* Returns state
*/
async get() {
if (!this.#initialized) {
await this.#initializing;
}
return this.#state;
}

/**
* Overwrite in-memory copy of state.
*
* @param data - The data to set
*/
async set(data: MetaMaskStorageStructure): Promise<void> {
if (!data) {
throw new Error('MetaMask - updated state is missing');
}
if (!this.#initialized) {
await this.#initializing;
}
this.#state = data;
}
}
2 changes: 1 addition & 1 deletion app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
@@ -455,7 +455,7 @@ export default class MetamaskController extends EventEmitter {
});

// instance of a class that wraps the extension's storage local API.
this.localStoreApiWrapper = opts.localStore;
this.localStoreApiWrapper = opts.persistanceManager;

this.currentMigrationVersion = opts.currentMigrationVersion;


Unchanged files with check annotations Beta

// TODO: Re think how to test this without exposing internal state
// it('should replace ethers instance when called with a different chainId than was current when the controller was instantiated', async function () {

Check warning on line 1265 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented

Check warning on line 1265 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented
// fetchTradesInfoStub.mockReset();
// const _swapsController = getSwapsController();
// expect(currentEthersInstance).not.toStrictEqual(newEthersInstance);
// });
// it('should not replace ethers instance when called with the same chainId that was current when the controller was instantiated', async function () {

Check warning on line 1286 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented

Check warning on line 1286 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented
// const _swapsController = new SwapsController({
// getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
// provider,
// expect(currentEthersInstance).toStrictEqual(newEthersInstance);
// });
// it('should replace ethers instance, and _ethersProviderChainId, twice when called twice with two different chainIds, and successfully set the _ethersProviderChainId when returning to the original chain', async function () {

Check warning on line 1306 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented

Check warning on line 1306 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented
// const _swapsController = new SwapsController({
// getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
// provider,
});
});
// it('clears polling timeout', function () {

Check warning on line 1390 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented

Check warning on line 1390 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented
// swapsController._pollingTimeout = setTimeout(() => {
// throw new Error('Polling timeout not cleared');
// }, POLLING_TIMEOUT);
describe('stopPollingForQuotes', function () {
// TODO: Re think how to test this without exposing internal state
// it('clears polling timeout', function () {

Check warning on line 1406 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented

Check warning on line 1406 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented
// swapsController._pollingTimeout = setTimeout(() => {
// throw new Error('Polling timeout not cleared');
// }, POLLING_TIMEOUT);
describe('resetPostFetchState', function () {
// TODO: Re think how to test this without exposing internal state
// it('clears polling timeout', function () {

Check warning on line 1429 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented

Check warning on line 1429 in app/scripts/controllers/swaps/swaps.test.ts

GitHub Actions / Test lint / Test lint

Some tests seem to be commented
// swapsController._pollingTimeout = setTimeout(() => {
// throw new Error('Polling timeout not cleared');
// }, POLLING_TIMEOUT);
image_large_url: '',
image_opengraph_url: '',
blurhash: 'U=Io~ufQ9_jtJTfQsTfQ0*fQ$$fQ#nfQX7fQ',
predominant_color: '#fb9f18',

Check warning on line 91 in test/e2e/flask/solana/common-solana.ts

GitHub Actions / Test lint / Test lint

'#fb9f18' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack

Check warning on line 91 in test/e2e/flask/solana/common-solana.ts

GitHub Actions / Test lint / Test lint

'#fb9f18' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
},
image_url: '',
image_properties: {
};
setTokensListDetected(newTokensList());
}, [

Check warning on line 117 in ui/components/app/detected-token/detected-token.js

GitHub Actions / Test lint / Test lint

React Hook useEffect has a missing dependency: 'tokensListDetected'. Either include it or remove the dependency array

Check warning on line 117 in ui/components/app/detected-token/detected-token.js

GitHub Actions / Test lint / Test lint

React Hook useEffect has a missing dependency: 'tokensListDetected'. Either include it or remove the dependency array
isTokenNetworkFilterEqualCurrentNetwork,
detectedTokensMultichain,
detectedTokens,
* No name has been saved for the value and type.
*/
export const NoSavedName = {
name: 'No Saved Name',

Check warning on line 129 in ui/components/app/name/name.stories.tsx

GitHub Actions / Test lint / Test lint

Named exports should not use the name annotation if it is redundant to the name that would be generated by the export name

Check warning on line 129 in ui/components/app/name/name.stories.tsx

GitHub Actions / Test lint / Test lint

Named exports should not use the name annotation if it is redundant to the name that would be generated by the export name
args: {
value: ADDRESS_MOCK,
type: NameType.ETHEREUM_ADDRESS,
useEffect(() => {
return () => interfaceId && dispatch(deleteInterface(interfaceId));
}, [interfaceId]);

Check warning on line 49 in ui/components/app/snaps/snap-home-page/snap-home-renderer.js

GitHub Actions / Test lint / Test lint

React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array

Check warning on line 49 in ui/components/app/snaps/snap-home-page/snap-home-renderer.js

GitHub Actions / Test lint / Test lint

React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array
useEffect(() => {
// Snaps are allowed to redirect to their own pending confirmations (templated or not)