Skip to content

Commit b823e60

Browse files
authored
Merge branch 'stable' into release/7.47.0
2 parents 1d4fcf5 + 483157e commit b823e60

4 files changed

Lines changed: 300 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5353
- fix(bridge): disable Solana as destination network if no Solana account exists
5454
- fix(bridge): prevent quote error during bridge destination account selection
5555

56+
## [7.46.1]
57+
58+
### Fixed
59+
- fix: capture exception with Sentry instead throwing the error ([#15469](https://github.com/MetaMask/metamask-mobile/pull/15469))
60+
5661
## [7.46.0]
5762

5863
### Added
@@ -5461,9 +5466,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54615466
- [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954)
54625467

54635468
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.47.0...HEAD
5464-
[7.47.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.46.0...v7.47.0
5465-
[7.46.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.45.2...v7.46.0
5466-
[7.45.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.45.1...v7.45.2
5469+
[7.47.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.46.1...v7.47.0
5470+
[7.46.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.46.0...v7.46.1
5471+
[7.46.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.45.1...v7.46.0
54675472
[7.45.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.45.0...v7.45.1
54685473
[7.45.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.44.0...v7.45.0
54695474
[7.44.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.43.0...v7.44.0
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { getPersistentState } from './getPersistentState';
2+
import { StateMetadata } from '@metamask/base-controller';
3+
4+
describe('getPersistentState', () => {
5+
it('return empty state', () => {
6+
expect(getPersistentState({}, {})).toStrictEqual({});
7+
});
8+
9+
it('return empty state when no properties are persistent', () => {
10+
const persistentState = getPersistentState(
11+
{ count: 1 },
12+
{ count: { anonymous: false, persist: false } },
13+
);
14+
expect(persistentState).toStrictEqual({});
15+
});
16+
17+
it('return persistent state', () => {
18+
const persistentState = getPersistentState(
19+
{
20+
password: 'secret password',
21+
privateKey: '123',
22+
network: 'mainnet',
23+
tokens: ['DAI', 'USDC'],
24+
},
25+
{
26+
password: {
27+
anonymous: false,
28+
persist: true,
29+
},
30+
privateKey: {
31+
anonymous: false,
32+
persist: true,
33+
},
34+
network: {
35+
anonymous: false,
36+
persist: false,
37+
},
38+
tokens: {
39+
anonymous: false,
40+
persist: false,
41+
},
42+
},
43+
);
44+
expect(persistentState).toStrictEqual({
45+
password: 'secret password',
46+
privateKey: '123',
47+
});
48+
});
49+
50+
it('use function to derive persistent state', () => {
51+
const normalizeTransactionHash = (hash: string) => hash.toLowerCase();
52+
53+
const persistentState = getPersistentState(
54+
{
55+
transactionHash: '0X1234',
56+
},
57+
{
58+
transactionHash: {
59+
anonymous: false,
60+
persist: normalizeTransactionHash,
61+
},
62+
},
63+
);
64+
65+
expect(persistentState).toStrictEqual({ transactionHash: '0x1234' });
66+
});
67+
68+
it('allow returning a partial object from a persist function', () => {
69+
const getPersistentTxMeta = (txMeta: { hash: string; value: number }) => ({
70+
value: txMeta.value,
71+
});
72+
73+
const persistentState = getPersistentState(
74+
{
75+
txMeta: {
76+
hash: '0x123',
77+
value: 10,
78+
},
79+
},
80+
{
81+
txMeta: {
82+
anonymous: false,
83+
persist: getPersistentTxMeta,
84+
},
85+
},
86+
);
87+
88+
expect(persistentState).toStrictEqual({ txMeta: { value: 10 } });
89+
});
90+
91+
it('allow returning a nested partial object from a persist function', () => {
92+
const getPersistentTxMeta = (txMeta: {
93+
hash: string;
94+
value: number;
95+
history: { hash: string; value: number }[];
96+
}) => ({
97+
history: txMeta.history.map((entry) => ({ value: entry.value })),
98+
value: txMeta.value,
99+
});
100+
101+
const persistentState = getPersistentState(
102+
{
103+
txMeta: {
104+
hash: '0x123',
105+
history: [
106+
{
107+
hash: '0x123',
108+
value: 9,
109+
},
110+
],
111+
value: 10,
112+
},
113+
},
114+
{
115+
txMeta: {
116+
anonymous: false,
117+
persist: getPersistentTxMeta,
118+
},
119+
},
120+
);
121+
122+
expect(persistentState).toStrictEqual({
123+
txMeta: { history: [{ value: 9 }], value: 10 },
124+
});
125+
});
126+
127+
it('allow transforming types in a persist function', () => {
128+
const persistentState = getPersistentState(
129+
{
130+
count: '1',
131+
},
132+
{
133+
count: {
134+
anonymous: false,
135+
persist: (count) => Number(count),
136+
},
137+
},
138+
);
139+
140+
expect(persistentState).toStrictEqual({ count: 1 });
141+
});
142+
143+
// New test cases for the two key changes
144+
145+
it('exclude state property when no metadata exists for a key', () => {
146+
const state = {
147+
password: 'secret password',
148+
privateKey: '123',
149+
network: 'mainnet',
150+
};
151+
const metadata = {
152+
password: {
153+
anonymous: false,
154+
persist: true,
155+
},
156+
privateKey: {
157+
anonymous: false,
158+
persist: true,
159+
},
160+
} as unknown as StateMetadata<typeof state>;
161+
162+
const persistentState = getPersistentState(state, metadata);
163+
164+
expect(persistentState).toStrictEqual({
165+
password: 'secret password',
166+
privateKey: '123',
167+
});
168+
});
169+
170+
it('exclude state property when an error occurs during derivation', () => {
171+
const state = {
172+
password: 'secret password',
173+
privateKey: '123',
174+
network: 'mainnet',
175+
};
176+
const metadata = {
177+
password: {
178+
anonymous: false,
179+
persist: true,
180+
},
181+
privateKey: {
182+
anonymous: false,
183+
persist: () => {
184+
throw new Error('Derivation error');
185+
},
186+
},
187+
} as unknown as StateMetadata<typeof state>;
188+
189+
const persistentState = getPersistentState(state, metadata);
190+
191+
expect(persistentState).toStrictEqual({
192+
password: 'secret password',
193+
});
194+
});
195+
196+
it('exclude nested objects without metadata', () => {
197+
const state = {
198+
user: {
199+
name: 'John',
200+
settings: {
201+
theme: 'dark',
202+
notifications: true,
203+
},
204+
},
205+
preferences: {
206+
language: 'en',
207+
currency: 'USD',
208+
},
209+
};
210+
const metadata = {
211+
user: {
212+
anonymous: false,
213+
persist: true,
214+
},
215+
} as unknown as StateMetadata<typeof state>;
216+
217+
const persistentState = getPersistentState(state, metadata);
218+
219+
expect(persistentState).toStrictEqual({
220+
user: {
221+
name: 'John',
222+
settings: {
223+
theme: 'dark',
224+
notifications: true,
225+
},
226+
},
227+
});
228+
});
229+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { StateConstraint, StateMetadata } from '@metamask/base-controller';
2+
import { Json } from '@metamask/utils';
3+
import { captureException } from '@sentry/react-native';
4+
5+
/**! IMPORTANT: THIS IS MEANT TO BE TEMPORARY, WE SHOULD NOT USE THIS AND USE THE VERSION IN BASE-CONTROLLER INSTEAD.
6+
* The reason why we are using this now, it's because we have controllers state without metadata.
7+
* An epic will be created to add the metadata to the controllers state without it.
8+
* As an example of a property without metadata: "swapsTransactions" in TransactionController
9+
*/
10+
11+
/**
12+
* Returns the subset of state that should be persisted.
13+
*
14+
* @param state - The controller state.
15+
* @param metadata - The controller state metadata, which describes which pieces of state should be persisted.
16+
* @returns The subset of controller state that should be persisted.
17+
*/
18+
export function getPersistentState<ControllerState extends StateConstraint>(
19+
state: ControllerState,
20+
metadata: StateMetadata<ControllerState>,
21+
): Record<keyof ControllerState, Json> {
22+
return deriveStateFromMetadata(state, metadata, 'persist');
23+
}
24+
25+
/**
26+
* Use the metadata to derive state according to the given metadata property.
27+
*
28+
* @param state - The full controller state.
29+
* @param metadata - The controller metadata.
30+
* @param metadataProperty - The metadata property to use to derive state.
31+
* @returns The metadata-derived controller state.
32+
*/
33+
function deriveStateFromMetadata<ControllerState extends StateConstraint>(
34+
state: ControllerState,
35+
metadata: StateMetadata<ControllerState>,
36+
metadataProperty: 'anonymous' | 'persist',
37+
): Record<keyof ControllerState, Json> {
38+
return (Object.keys(state) as (keyof ControllerState)[]).reduce<
39+
Record<keyof ControllerState, Json>
40+
>((derivedState, key) => {
41+
try {
42+
const stateMetadata = metadata[key];
43+
if (!stateMetadata) {
44+
throw new Error(`No metadata found for '${String(key)}'`);
45+
}
46+
const propertyMetadata = stateMetadata[metadataProperty];
47+
const stateProperty = state[key];
48+
if (typeof propertyMetadata === 'function') {
49+
derivedState[key] = propertyMetadata(stateProperty);
50+
} else if (propertyMetadata) {
51+
derivedState[key] = stateProperty;
52+
}
53+
return derivedState;
54+
} catch (error) {
55+
// This is what change from the original base controller implementation
56+
// This is a temporary solution to capture the error for extraneous data that we did not detect
57+
captureException(error as unknown as Error);
58+
59+
return derivedState;
60+
}
61+
}, {} as never);
62+
}

app/store/persistConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Device from '../util/device';
99
import { UserState } from '../reducers/user';
1010
import { debounce } from 'lodash';
1111
import Engine, { EngineContext } from '../core/Engine';
12-
import { getPersistentState } from '@metamask/base-controller';
12+
import { getPersistentState } from './getPersistentState/getPersistentState';
1313

1414
const TIMEOUT = 40000;
1515
const STORAGE_DEBOUNCE_DELAY = 200;

0 commit comments

Comments
 (0)