Skip to content

Commit 4070d34

Browse files
bergeronsalimtb
andauthored
chore(cherry-pick): migration to remove tokens with null decimals (#29251)
Cherry picks #29245 to v12.9.2 Co-authored-by: Salim TOUBAL <[email protected]>
1 parent 7985a89 commit 4070d34

File tree

3 files changed

+412
-0
lines changed

3 files changed

+412
-0
lines changed

app/scripts/migrations/133.1.test.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { cloneDeep } from 'lodash';
2+
import { TokensControllerState } from '@metamask/assets-controllers';
3+
import { migrate, version } from './133.1';
4+
5+
const sentryCaptureExceptionMock = jest.fn();
6+
const sentryCaptureMessageMock = jest.fn();
7+
8+
global.sentry = {
9+
captureException: sentryCaptureExceptionMock,
10+
captureMessage: sentryCaptureMessageMock,
11+
};
12+
13+
const oldVersion = 133;
14+
15+
const mockStateWithNullDecimals = {
16+
meta: { version: oldVersion },
17+
data: {
18+
TokensController: {
19+
allTokens: {
20+
'0x1': {
21+
'0x123': [
22+
{ address: '0x1', symbol: 'TOKEN1', decimals: null },
23+
{ address: '0x2', symbol: 'TOKEN2', decimals: 18 },
24+
],
25+
},
26+
},
27+
allDetectedTokens: {
28+
'0x1': {
29+
'0x123': [
30+
{ address: '0x5', symbol: 'TOKEN5', decimals: null },
31+
{ address: '0x6', symbol: 'TOKEN6', decimals: 6 },
32+
],
33+
},
34+
},
35+
tokens: [
36+
{ address: '0x7', symbol: 'TOKEN7', decimals: null },
37+
{ address: '0x8', symbol: 'TOKEN8', decimals: 18 },
38+
],
39+
detectedTokens: [
40+
{ address: '0x9', symbol: 'TOKEN9', decimals: null },
41+
{ address: '0xA', symbol: 'TOKEN10', decimals: 6 },
42+
],
43+
},
44+
},
45+
};
46+
47+
describe(`migration #${version}`, () => {
48+
afterEach(() => jest.resetAllMocks());
49+
50+
it('updates the version metadata', async () => {
51+
const oldStorage = cloneDeep(mockStateWithNullDecimals);
52+
53+
const newStorage = await migrate(oldStorage);
54+
55+
expect(newStorage.meta).toStrictEqual({ version });
56+
});
57+
58+
it('removes tokens with null decimals from allTokens', async () => {
59+
const oldStorage = cloneDeep(mockStateWithNullDecimals);
60+
61+
const newStorage = await migrate(oldStorage);
62+
63+
const tokensControllerState = newStorage.data
64+
.TokensController as TokensControllerState;
65+
const { allTokens } = tokensControllerState;
66+
67+
expect(allTokens).toEqual({
68+
'0x1': {
69+
'0x123': [{ address: '0x2', symbol: 'TOKEN2', decimals: 18 }],
70+
},
71+
});
72+
});
73+
74+
it('removes tokens with null decimals from allDetectedTokens', async () => {
75+
const oldStorage = cloneDeep(mockStateWithNullDecimals);
76+
77+
const newStorage = await migrate(oldStorage);
78+
79+
const tokensControllerState = newStorage.data
80+
.TokensController as TokensControllerState;
81+
const { allDetectedTokens } = tokensControllerState;
82+
83+
expect(allDetectedTokens).toEqual({
84+
'0x1': {
85+
'0x123': [{ address: '0x6', symbol: 'TOKEN6', decimals: 6 }],
86+
},
87+
});
88+
});
89+
90+
it('removes tokens with null decimals from tokens array', async () => {
91+
const oldStorage = cloneDeep(mockStateWithNullDecimals);
92+
93+
const newStorage = await migrate(oldStorage);
94+
95+
const tokensControllerState = newStorage.data
96+
.TokensController as TokensControllerState;
97+
const { tokens } = tokensControllerState;
98+
99+
expect(tokens).toEqual([
100+
{ address: '0x8', symbol: 'TOKEN8', decimals: 18 },
101+
]);
102+
});
103+
104+
it('removes tokens with null decimals from detectedTokens array', async () => {
105+
const oldStorage = cloneDeep(mockStateWithNullDecimals);
106+
107+
const newStorage = await migrate(oldStorage);
108+
109+
const tokensControllerState = newStorage.data
110+
.TokensController as TokensControllerState;
111+
const { detectedTokens } = tokensControllerState;
112+
113+
expect(detectedTokens).toEqual([
114+
{ address: '0xA', symbol: 'TOKEN10', decimals: 6 },
115+
]);
116+
});
117+
118+
it('logs tokens with null decimals before removing them', async () => {
119+
const oldStorage = cloneDeep(mockStateWithNullDecimals);
120+
121+
await migrate(oldStorage);
122+
123+
expect(sentryCaptureMessageMock).toHaveBeenCalledTimes(4);
124+
expect(sentryCaptureMessageMock).toHaveBeenCalledWith(
125+
`Migration ${version}: Removed token with decimals === null in allTokens. Address: 0x1`,
126+
);
127+
expect(sentryCaptureMessageMock).toHaveBeenCalledWith(
128+
`Migration ${version}: Removed token with decimals === null in allDetectedTokens. Address: 0x5`,
129+
);
130+
expect(sentryCaptureMessageMock).toHaveBeenCalledWith(
131+
`Migration ${version}: Removed token with decimals === null in tokens. Address: 0x7`,
132+
);
133+
expect(sentryCaptureMessageMock).toHaveBeenCalledWith(
134+
`Migration ${version}: Removed token with decimals === null in detectedTokens. Address: 0x9`,
135+
);
136+
});
137+
138+
it('does nothing if all tokens have valid decimals', async () => {
139+
const validState = {
140+
meta: { version: oldVersion },
141+
data: {
142+
TokensController: {
143+
allTokens: {
144+
'0x1': {
145+
'0x123': [{ address: '0x2', symbol: 'TOKEN2', decimals: 18 }],
146+
},
147+
},
148+
allDetectedTokens: {
149+
'0x1': {
150+
'0x123': [{ address: '0x6', symbol: 'TOKEN6', decimals: 6 }],
151+
},
152+
},
153+
tokens: [{ address: '0x8', symbol: 'TOKEN8', decimals: 18 }],
154+
detectedTokens: [{ address: '0xA', symbol: 'TOKEN10', decimals: 6 }],
155+
},
156+
},
157+
};
158+
159+
const oldStorage = cloneDeep(validState);
160+
const newStorage = await migrate(oldStorage);
161+
162+
expect(newStorage.data).toStrictEqual(oldStorage.data);
163+
expect(sentryCaptureMessageMock).not.toHaveBeenCalled();
164+
});
165+
166+
it('does nothing if TokensController is missing', async () => {
167+
const oldStorage = {
168+
meta: { version: oldVersion },
169+
data: {},
170+
};
171+
172+
const newStorage = await migrate(cloneDeep(oldStorage));
173+
174+
expect(newStorage.data).toStrictEqual(oldStorage.data);
175+
expect(sentryCaptureMessageMock).not.toHaveBeenCalled();
176+
});
177+
178+
const invalidState = [
179+
{
180+
errorMessage: `Migration ${version}: Invalid allTokens state of type 'string'`,
181+
label: 'Invalid allTokens',
182+
state: { TokensController: { allTokens: 'invalid' } },
183+
},
184+
{
185+
errorMessage: `Migration ${version}: Invalid allDetectedTokens state of type 'string'`,
186+
label: 'Invalid allDetectedTokens',
187+
state: { TokensController: { allDetectedTokens: 'invalid' } },
188+
},
189+
{
190+
errorMessage: `Migration ${version}: Invalid tokens state of type 'string'`,
191+
label: 'Invalid tokens',
192+
state: { TokensController: { tokens: 'invalid' } },
193+
},
194+
{
195+
errorMessage: `Migration ${version}: Invalid detectedTokens state of type 'string'`,
196+
label: 'Invalid detectedTokens',
197+
state: { TokensController: { detectedTokens: 'invalid' } },
198+
},
199+
];
200+
201+
// @ts-expect-error 'each' function is not recognized by TypeScript types
202+
it.each(invalidState)(
203+
'captures error when state is invalid due to: $label',
204+
async ({
205+
errorMessage,
206+
state,
207+
}: {
208+
errorMessage: string;
209+
state: Record<string, unknown>;
210+
}) => {
211+
const oldStorage = {
212+
meta: { version: oldVersion },
213+
data: state,
214+
};
215+
216+
const newStorage = await migrate(cloneDeep(oldStorage));
217+
218+
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
219+
new Error(errorMessage),
220+
);
221+
expect(newStorage.data).toStrictEqual(oldStorage.data);
222+
},
223+
);
224+
});

0 commit comments

Comments
 (0)