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

key tokens by chainId (#10510)

This commit is contained in:
Brad Decker 2021-02-26 09:40:25 -06:00 committed by GitHub
parent dedadee346
commit caa32d87fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 596 additions and 30 deletions

View File

@ -8,6 +8,7 @@ import log from 'loglevel';
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { NETWORK_EVENTS } from './network';
export default class PreferencesController {
/**
@ -72,7 +73,7 @@ export default class PreferencesController {
this.store.setMaxListeners(12);
this.openPopup = opts.openPopup;
this.migrateAddressBookState = opts.migrateAddressBookState;
this._subscribeProviderType();
this._subscribeToNetworkDidChange();
global.setPreference = (key, value) => {
return this.setFeatureFlag(key, value);
@ -667,12 +668,11 @@ export default class PreferencesController {
//
/**
* Subscription to network provider type.
*
*
* Handle updating token list to reflect current network by listening for the
* NETWORK_DID_CHANGE event.
*/
_subscribeProviderType() {
this.network.providerStore.subscribe(() => {
_subscribeToNetworkDidChange() {
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
});
@ -689,12 +689,12 @@ export default class PreferencesController {
_updateAccountTokens(tokens, assetImages, hiddenTokens) {
const {
accountTokens,
providerType,
chainId,
selectedAddress,
accountHiddenTokens,
} = this._getTokenRelatedStates();
accountTokens[selectedAddress][providerType] = tokens;
accountHiddenTokens[selectedAddress][providerType] = hiddenTokens;
accountTokens[selectedAddress][chainId] = tokens;
accountHiddenTokens[selectedAddress][chainId] = hiddenTokens;
this.store.updateState({
accountTokens,
tokens,
@ -730,27 +730,27 @@ export default class PreferencesController {
// eslint-disable-next-line no-param-reassign
selectedAddress = this.store.getState().selectedAddress;
}
const providerType = this.network.providerStore.getState().type;
const chainId = this.network.getCurrentChainId();
if (!(selectedAddress in accountTokens)) {
accountTokens[selectedAddress] = {};
}
if (!(selectedAddress in accountHiddenTokens)) {
accountHiddenTokens[selectedAddress] = {};
}
if (!(providerType in accountTokens[selectedAddress])) {
accountTokens[selectedAddress][providerType] = [];
if (!(chainId in accountTokens[selectedAddress])) {
accountTokens[selectedAddress][chainId] = [];
}
if (!(providerType in accountHiddenTokens[selectedAddress])) {
accountHiddenTokens[selectedAddress][providerType] = [];
if (!(chainId in accountHiddenTokens[selectedAddress])) {
accountHiddenTokens[selectedAddress][chainId] = [];
}
const tokens = accountTokens[selectedAddress][providerType];
const hiddenTokens = accountHiddenTokens[selectedAddress][providerType];
const tokens = accountTokens[selectedAddress][chainId];
const hiddenTokens = accountHiddenTokens[selectedAddress][chainId];
return {
tokens,
accountTokens,
hiddenTokens,
accountHiddenTokens,
providerType,
chainId,
selectedAddress,
};
}

View File

@ -0,0 +1,125 @@
import { cloneDeep } from 'lodash';
import {
GOERLI,
GOERLI_CHAIN_ID,
KOVAN,
KOVAN_CHAIN_ID,
MAINNET,
MAINNET_CHAIN_ID,
NETWORK_TYPE_RPC,
RINKEBY,
RINKEBY_CHAIN_ID,
ROPSTEN,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network';
const version = 52;
/**
* Migrate tokens in Preferences to be keyed by chainId instead of
* providerType. To prevent breaking user's MetaMask and selected
* tokens, this migration copies the RPC entry into *every* custom RPC
* chainId.
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
versionedData.data = transformState(state);
return versionedData;
},
};
function transformState(state = {}) {
if (state.PreferencesController) {
const {
accountTokens,
accountHiddenTokens,
frequentRpcListDetail,
} = state.PreferencesController;
const newAccountTokens = {};
const newAccountHiddenTokens = {};
if (accountTokens && Object.keys(accountTokens).length > 0) {
for (const address of Object.keys(accountTokens)) {
newAccountTokens[address] = {};
if (accountTokens[address][NETWORK_TYPE_RPC]) {
frequentRpcListDetail.forEach((detail) => {
newAccountTokens[address][detail.chainId] =
accountTokens[address][NETWORK_TYPE_RPC];
});
}
for (const providerType of Object.keys(accountTokens[address])) {
switch (providerType) {
case MAINNET:
newAccountTokens[address][MAINNET_CHAIN_ID] =
accountTokens[address][MAINNET];
break;
case ROPSTEN:
newAccountTokens[address][ROPSTEN_CHAIN_ID] =
accountTokens[address][ROPSTEN];
break;
case RINKEBY:
newAccountTokens[address][RINKEBY_CHAIN_ID] =
accountTokens[address][RINKEBY];
break;
case GOERLI:
newAccountTokens[address][GOERLI_CHAIN_ID] =
accountTokens[address][GOERLI];
break;
case KOVAN:
newAccountTokens[address][KOVAN_CHAIN_ID] =
accountTokens[address][KOVAN];
break;
default:
break;
}
}
}
state.PreferencesController.accountTokens = newAccountTokens;
}
if (accountHiddenTokens && Object.keys(accountHiddenTokens).length > 0) {
for (const address of Object.keys(accountHiddenTokens)) {
newAccountHiddenTokens[address] = {};
if (accountHiddenTokens[address][NETWORK_TYPE_RPC]) {
frequentRpcListDetail.forEach((detail) => {
newAccountHiddenTokens[address][detail.chainId] =
accountHiddenTokens[address][NETWORK_TYPE_RPC];
});
}
for (const providerType of Object.keys(accountHiddenTokens[address])) {
switch (providerType) {
case MAINNET:
newAccountHiddenTokens[address][MAINNET_CHAIN_ID] =
accountHiddenTokens[address][MAINNET];
break;
case ROPSTEN:
newAccountHiddenTokens[address][ROPSTEN_CHAIN_ID] =
accountHiddenTokens[address][ROPSTEN];
break;
case RINKEBY:
newAccountHiddenTokens[address][RINKEBY_CHAIN_ID] =
accountHiddenTokens[address][RINKEBY];
break;
case GOERLI:
newAccountHiddenTokens[address][GOERLI_CHAIN_ID] =
accountHiddenTokens[address][GOERLI];
break;
case KOVAN:
newAccountHiddenTokens[address][KOVAN_CHAIN_ID] =
accountHiddenTokens[address][KOVAN];
break;
default:
break;
}
}
}
state.PreferencesController.accountHiddenTokens = newAccountHiddenTokens;
}
}
return state;
}

View File

@ -56,6 +56,7 @@ const migrations = [
require('./049').default,
require('./050').default,
require('./051').default,
require('./052').default,
];
export default migrations;

View File

@ -1,19 +1,39 @@
import assert from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import sinon from 'sinon';
import PreferencesController from '../../../../app/scripts/controllers/preferences';
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../../shared/constants/network';
describe('preferences controller', function () {
let preferencesController;
let network;
let currentChainId;
let triggerNetworkChange;
let switchToMainnet;
let switchToRinkeby;
const migrateAddressBookState = sinon.stub();
beforeEach(function () {
network = { providerStore: new ObservableStore({ type: 'mainnet' }) };
currentChainId = MAINNET_CHAIN_ID;
network = {
getCurrentChainId: () => currentChainId,
on: sinon.spy(),
};
preferencesController = new PreferencesController({
migrateAddressBookState,
network,
});
triggerNetworkChange = network.on.firstCall.args[1];
switchToMainnet = () => {
currentChainId = MAINNET_CHAIN_ID;
triggerNetworkChange();
};
switchToRinkeby = () => {
currentChainId = RINKEBY_CHAIN_ID;
triggerNetworkChange();
};
});
afterEach(function () {
@ -230,12 +250,10 @@ describe('preferences controller', function () {
const symbolFirst = 'ABBR';
const symbolSecond = 'ABBB';
const decimals = 5;
network.providerStore.updateState({ type: 'mainnet' });
await preferencesController.addToken(addressFirst, symbolFirst, decimals);
const tokensFirstAddress = preferencesController.getTokens();
network.providerStore.updateState({ type: 'rinkeby' });
switchToRinkeby();
await preferencesController.addToken(
addressSecond,
symbolSecond,
@ -304,14 +322,13 @@ describe('preferences controller', function () {
});
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' });
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
network.providerStore.updateState({ type: 'rinkeby' });
switchToRinkeby();
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
const initialTokensSecond = preferencesController.getTokens();
network.providerStore.updateState({ type: 'mainnet' });
switchToMainnet();
await preferencesController.removeToken('0xa');
const tokensFirst = preferencesController.getTokens();
@ -320,7 +337,7 @@ describe('preferences controller', function () {
const [token1] = tokensFirst;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
network.providerStore.updateState({ type: 'rinkeby' });
switchToRinkeby();
const tokensSecond = preferencesController.getTokens();
assert.deepEqual(
tokensSecond,
@ -371,11 +388,10 @@ describe('preferences controller', function () {
describe('on updateStateNetworkType', function () {
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' });
await preferencesController.addToken('0xa', 'A', 4);
await preferencesController.addToken('0xb', 'B', 5);
const initialTokensFirst = preferencesController.getTokens();
network.providerStore.updateState({ type: 'rinkeby' });
switchToRinkeby();
await preferencesController.addToken('0xa', 'C', 4);
await preferencesController.addToken('0xb', 'D', 5);
const initialTokensSecond = preferencesController.getTokens();
@ -386,9 +402,9 @@ describe('preferences controller', function () {
'tokens not equal for different networks and tokens',
);
network.providerStore.updateState({ type: 'mainnet' });
switchToMainnet();
const tokensFirst = preferencesController.getTokens();
network.providerStore.updateState({ type: 'rinkeby' });
switchToRinkeby();
const tokensSecond = preferencesController.getTokens();
assert.deepEqual(
tokensFirst,

View File

@ -0,0 +1,424 @@
import assert from 'assert';
import migration52 from '../../../app/scripts/migrations/052';
import {
GOERLI,
GOERLI_CHAIN_ID,
KOVAN,
KOVAN_CHAIN_ID,
MAINNET,
MAINNET_CHAIN_ID,
NETWORK_TYPE_RPC,
RINKEBY,
RINKEBY_CHAIN_ID,
ROPSTEN,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network';
const TOKEN1 = { symbol: 'TST', address: '0x10', decimals: 18 };
const TOKEN2 = { symbol: 'TXT', address: '0x11', decimals: 18 };
const TOKEN3 = { symbol: 'TVT', address: '0x12', decimals: 18 };
const TOKEN4 = { symbol: 'TAT', address: '0x13', decimals: 18 };
describe('migration #52', function () {
it('should update the version metadata', async function () {
const oldStorage = {
meta: {
version: 52,
},
data: {},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.meta, {
version: 52,
});
});
it(`should move ${MAINNET} tokens and hidden tokens to be keyed by ${MAINNET_CHAIN_ID} for each address`, async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[MAINNET]: [TOKEN1],
},
'0x1112': {
[MAINNET]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[MAINNET]: [TOKEN1, TOKEN2],
},
'0x1112': {
[MAINNET]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[MAINNET_CHAIN_ID]: [TOKEN1],
},
'0x1112': {
[MAINNET_CHAIN_ID]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[MAINNET_CHAIN_ID]: [TOKEN1, TOKEN2],
},
'0x1112': {
[MAINNET_CHAIN_ID]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
});
});
it(`should move ${RINKEBY} tokens and hidden tokens to be keyed by ${RINKEBY_CHAIN_ID} for each address`, async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[RINKEBY]: [TOKEN1],
},
'0x1112': {
[RINKEBY]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[RINKEBY]: [TOKEN1, TOKEN2],
},
'0x1112': {
[RINKEBY]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[RINKEBY_CHAIN_ID]: [TOKEN1],
},
'0x1112': {
[RINKEBY_CHAIN_ID]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[RINKEBY_CHAIN_ID]: [TOKEN1, TOKEN2],
},
'0x1112': {
[RINKEBY_CHAIN_ID]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
});
});
it(`should move ${KOVAN} tokens and hidden tokens to be keyed by ${KOVAN_CHAIN_ID} for each address`, async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[KOVAN]: [TOKEN1],
},
'0x1112': {
[KOVAN]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[KOVAN]: [TOKEN1, TOKEN2],
},
'0x1112': {
[KOVAN]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[KOVAN_CHAIN_ID]: [TOKEN1],
},
'0x1112': {
[KOVAN_CHAIN_ID]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[KOVAN_CHAIN_ID]: [TOKEN1, TOKEN2],
},
'0x1112': {
[KOVAN_CHAIN_ID]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
});
});
it(`should move ${GOERLI} tokens and hidden tokens to be keyed by ${GOERLI_CHAIN_ID} for each address`, async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[GOERLI]: [TOKEN1],
},
'0x1112': {
[GOERLI]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[GOERLI]: [TOKEN1, TOKEN2],
},
'0x1112': {
[GOERLI]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[GOERLI_CHAIN_ID]: [TOKEN1],
},
'0x1112': {
[GOERLI_CHAIN_ID]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[GOERLI_CHAIN_ID]: [TOKEN1, TOKEN2],
},
'0x1112': {
[GOERLI_CHAIN_ID]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
});
});
it(`should move ${ROPSTEN} tokens and hidden tokens to be keyed by ${ROPSTEN_CHAIN_ID} for each address`, async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[ROPSTEN]: [TOKEN1],
},
'0x1112': {
[ROPSTEN]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[ROPSTEN]: [TOKEN1, TOKEN2],
},
'0x1112': {
[ROPSTEN]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
accountHiddenTokens: {
'0x1111': {
[ROPSTEN_CHAIN_ID]: [TOKEN1],
},
'0x1112': {
[ROPSTEN_CHAIN_ID]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[ROPSTEN_CHAIN_ID]: [TOKEN1, TOKEN2],
},
'0x1112': {
[ROPSTEN_CHAIN_ID]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
});
});
it(`should duplicate ${NETWORK_TYPE_RPC} tokens and hidden tokens to all custom networks for each address`, async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
frequentRpcListDetail: [
{ chainId: '0xab' },
{ chainId: '0x12' },
{ chainId: '0xfa' },
],
accountHiddenTokens: {
'0x1111': {
[NETWORK_TYPE_RPC]: [TOKEN1],
},
'0x1112': {
[NETWORK_TYPE_RPC]: [TOKEN3],
},
},
accountTokens: {
'0x1111': {
[NETWORK_TYPE_RPC]: [TOKEN1, TOKEN2],
},
'0x1112': {
[NETWORK_TYPE_RPC]: [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
frequentRpcListDetail: [
{ chainId: '0xab' },
{ chainId: '0x12' },
{ chainId: '0xfa' },
],
accountHiddenTokens: {
'0x1111': {
'0xab': [TOKEN1],
'0x12': [TOKEN1],
'0xfa': [TOKEN1],
},
'0x1112': {
'0xab': [TOKEN3],
'0x12': [TOKEN3],
'0xfa': [TOKEN3],
},
},
accountTokens: {
'0x1111': {
'0xab': [TOKEN1, TOKEN2],
'0x12': [TOKEN1, TOKEN2],
'0xfa': [TOKEN1, TOKEN2],
},
'0x1112': {
'0xab': [TOKEN1, TOKEN3],
'0x12': [TOKEN1, TOKEN3],
'0xfa': [TOKEN1, TOKEN3],
},
},
bar: 'baz',
},
foo: 'bar',
});
});
it(`should overwrite ${NETWORK_TYPE_RPC} tokens with built in networks if chainIds match`, async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
frequentRpcListDetail: [{ chainId: '0x1' }],
accountHiddenTokens: {
'0x1111': {
[NETWORK_TYPE_RPC]: [TOKEN3],
[MAINNET]: [TOKEN1],
},
},
accountTokens: {
'0x1111': {
[NETWORK_TYPE_RPC]: [TOKEN1, TOKEN2],
[MAINNET]: [TOKEN3, TOKEN4],
},
},
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
frequentRpcListDetail: [{ chainId: '0x1' }],
accountHiddenTokens: {
'0x1111': {
'0x1': [TOKEN1],
},
},
accountTokens: {
'0x1111': {
'0x1': [TOKEN3, TOKEN4],
},
},
bar: 'baz',
},
foo: 'bar',
});
});
it('should do nothing if no PreferencesController key', async function () {
const oldStorage = {
meta: {},
data: {
foo: 'bar',
},
};
const newStorage = await migration52.migrate(oldStorage);
assert.deepStrictEqual(newStorage.data, {
foo: 'bar',
});
});
});