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

Add migration notification for users with Sai (#7450)

Maker has upgraded its Dai token to "Multi-Collateral Dai" (MCD) and requires
all users interacting with Dai migrate their tokens to the new version. Dai
now exclusively refers to Multi-Collateral Dai and what was previouly called
Dai is now Sai (Single Collateral Dai).

In this description, Sai refers to what was (prior to the 2019-11-18) known as Dai.
Dai is the _new_ token.

This changeset:

1. Only affects users who had non-zero Sai at the old contract address
2. Displays a persistent notification for users with Sai
3. Updates the token symbol for users already tracking the Sai token
4. Bumps our direct and indirect eth-contract-metadata dependencies

The notification copy:

> A message from Maker: The new Multi-Collateral Dai token has been released. Your old tokens are now called Sai. Please upgrade your Sai tokens to the new Dai.

The copy is from the Maker team.
This commit is contained in:
Whymarrh Whitby 2019-11-18 18:16:28 -03:30 committed by GitHub
parent b3395502f2
commit 86b165ea83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 589 additions and 12 deletions

View File

@ -1,4 +1,10 @@
{
"migrateSai": {
"message": "A message from Maker: The new Multi-Collateral Dai token has been released. Your old tokens are now called Sai. Please upgrade your Sai tokens to the new Dai."
},
"migrate": {
"message": "Migrate"
},
"showIncomingTransactions": {
"message": "Show Incoming Transactions"
},

View File

@ -0,0 +1,67 @@
const version = 39
const clone = require('clone')
const ethUtil = require('ethereumjs-util')
const DAI_V1_CONTRACT_ADDRESS = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'
const DAI_V1_TOKEN_SYMBOL = 'DAI'
const SAI_TOKEN_SYMBOL = 'SAI'
function isOldDai (token = {}) {
return token && typeof token === 'object' &&
token.symbol === DAI_V1_TOKEN_SYMBOL &&
ethUtil.toChecksumAddress(token.address) === DAI_V1_CONTRACT_ADDRESS
}
/**
* This migration renames the Dai token to Sai.
*
* As of 2019-11-18 Dai is now called Sai (refs https://git.io/JeooP) to facilitate
* Maker's upgrade to Multi-Collateral Dai and this migration renames the token
* at the old address.
*/
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
versionedData.data = transformState(state)
return versionedData
},
}
function transformState (state) {
const { PreferencesController } = state
if (PreferencesController) {
const tokens = PreferencesController.tokens || []
if (Array.isArray(tokens)) {
for (const token of tokens) {
if (isOldDai(token)) {
token.symbol = SAI_TOKEN_SYMBOL
}
}
}
const accountTokens = PreferencesController.accountTokens || {}
if (accountTokens && typeof accountTokens === 'object') {
for (const address of Object.keys(accountTokens)) {
const networkTokens = accountTokens[address]
if (networkTokens && typeof networkTokens === 'object') {
for (const network of Object.keys(networkTokens)) {
const tokensOnNetwork = networkTokens[network]
if (Array.isArray(tokensOnNetwork)) {
for (const token of tokensOnNetwork) {
if (isOldDai(token)) {
token.symbol = SAI_TOKEN_SYMBOL
}
}
}
}
}
}
}
}
return state
}

View File

@ -49,4 +49,5 @@ module.exports = [
require('./036'),
require('./037'),
require('./038'),
require('./039'),
]

View File

@ -87,7 +87,7 @@
"dnode": "^1.2.2",
"end-of-stream": "^1.1.0",
"eth-block-tracker": "^4.4.2",
"eth-contract-metadata": "1.9.3",
"eth-contract-metadata": "^1.11.0",
"eth-ens-namehash": "^2.0.8",
"eth-json-rpc-errors": "^1.1.0",
"eth-json-rpc-filters": "^4.1.1",
@ -114,7 +114,7 @@
"extensionizer": "^1.0.1",
"fast-json-patch": "^2.0.4",
"fuse.js": "^3.2.0",
"gaba": "^1.8.0",
"gaba": "^1.9.0",
"human-standard-token-abi": "^2.0.0",
"jazzicon": "^1.2.0",
"json-rpc-engine": "^5.1.5",

View File

@ -0,0 +1,419 @@
const assert = require('assert')
const migration39 = require('../../../app/scripts/migrations/039')
describe('migration #39', () => {
it('should update the version metadata', (done) => {
const oldStorage = {
'meta': {
'version': 38,
},
'data': {},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.meta, {
'version': 39,
})
done()
})
.catch(done)
})
it('should update old DAI token symbol to SAI in tokens', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': [{
'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'decimals': 18,
'symbol': 'DAI',
}, {
'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'symbol': 'BAT',
'decimals': 18,
}, {
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'symbol': 'META',
'decimals': 18,
}],
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data.PreferencesController, {
'tokens': [{
'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'decimals': 18,
'symbol': 'SAI',
}, {
'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'symbol': 'BAT',
'decimals': 18,
}, {
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'symbol': 'META',
'decimals': 18,
}],
})
done()
})
.catch(done)
})
it('should update old DAI token symbol to SAI in accountTokens', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'accountTokens': {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': {
'mainnet': [
{
'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'decimals': 18,
'symbol': 'DAI',
},
],
},
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
'mainnet': [],
'rinkeby': [],
},
'0x8e5d75d60224ea0c33d1041e75de68b1c3cb6dd5': {},
'0xb3958fb96c8201486ae20be1d5c9f58083df343a': {
'mainnet': [
{
'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'decimals': 18,
'symbol': 'DAI',
},
{
'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'decimals': 18,
'symbol': 'BAT',
},
{
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'decimals': 18,
'symbol': 'META',
},
],
},
},
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data.PreferencesController, {
'accountTokens': {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': {
'mainnet': [
{
'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'decimals': 18,
'symbol': 'SAI',
},
],
},
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
'mainnet': [],
'rinkeby': [],
},
'0x8e5d75d60224ea0c33d1041e75de68b1c3cb6dd5': {},
'0xb3958fb96c8201486ae20be1d5c9f58083df343a': {
'mainnet': [
{
'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'decimals': 18,
'symbol': 'SAI',
},
{
'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'decimals': 18,
'symbol': 'BAT',
},
{
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'decimals': 18,
'symbol': 'META',
},
],
},
},
})
done()
})
.catch(done)
})
it('should NOT change any state if accountTokens is not an object', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'accountTokens': [],
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if accountTokens is an object with invalid values', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'accountTokens': {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': [
{
'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'decimals': 18,
'symbol': 'DAI',
},
],
'0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359': null,
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
'mainnet': [
null,
undefined,
[],
42,
],
'rinkeby': null,
},
},
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if accountTokens includes the new DAI token', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'accountTokens': {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': {
'mainnet': [
{
'address': '0x6B175474E89094C44Da98b954EedeAC495271d0F',
'decimals': 18,
'symbol': 'DAI',
},
],
},
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
'mainnet': [],
'rinkeby': [],
},
'0x8e5d75d60224ea0c33d1041e75de68b1c3cb6dd5': {},
'0xb3958fb96c8201486ae20be1d5c9f58083df343a': {
'mainnet': [
{
'address': '0x6B175474E89094C44Da98b954EedeAC495271d0F',
'decimals': 18,
'symbol': 'DAI',
},
{
'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'decimals': 18,
'symbol': 'BAT',
},
{
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'decimals': 18,
'symbol': 'META',
},
],
},
},
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if tokens includes the new DAI token', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': [{
'address': '0x6B175474E89094C44Da98b954EedeAC495271d0F',
'symbol': 'DAI',
'decimals': 18,
}, {
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'symbol': 'META',
'decimals': 18,
}],
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if tokens does not include DAI', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': [{
'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'symbol': 'BAT',
'decimals': 18,
}, {
'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4',
'symbol': 'META',
'decimals': 18,
}],
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if a tokens property has invalid entries', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': [
null,
[],
undefined,
42,
],
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if a tokens property is not an array', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': {},
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if a tokens property is null', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': null,
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if a tokens property is missing', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if a accountTokens property is missing', (done) => {
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
},
},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
it('should NOT change any state if PreferencesController is missing', (done) => {
const oldStorage = {
'meta': {},
'data': {},
}
migration39.migrate(oldStorage)
.then((newStorage) => {
assert.deepEqual(newStorage.data, oldStorage.data)
done()
})
.catch(done)
})
})

View File

@ -0,0 +1,46 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import HomeNotification from '../home-notification'
export default class DaiV1MigrationNotification extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static defaultProps = {
string: '',
symbol: '',
}
static propTypes = {
string: PropTypes.string,
symbol: PropTypes.string,
}
render () {
const { t } = this.context
const { string: balanceString, symbol } = this.props
if (!balanceString || !symbol) {
return null
}
if (balanceString === '0') {
return null
}
return (
<HomeNotification
descriptionText={t('migrateSai')}
acceptText={t('migrate')}
onAccept={() => {
window.open('https://migrate.makerdao.com', '_blank', 'noopener')
}}
ignoreText={t('learnMore')}
onIgnore={() => {
window.open('https://blog.makerdao.com/multi-collateral-dai-is-live/', '_blank', 'noopener')
}}
/>
)
}
}

View File

@ -0,0 +1,20 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import DaiMigrationNotification from './dai-migration-notification.component'
import withTokenTracker from '../../../helpers/higher-order-components/with-token-tracker'
import { getSelectedAddress, getDaiV1Token } from '../../../selectors/selectors'
const mapStateToProps = (state) => {
const userAddress = getSelectedAddress(state)
const oldDai = getDaiV1Token(state)
return {
userAddress,
token: oldDai,
}
}
export default compose(
connect(mapStateToProps),
withTokenTracker,
)(DaiMigrationNotification)

View File

@ -0,0 +1 @@
export { default } from './dai-migration-notification.container'

View File

@ -4,6 +4,7 @@ import Media from 'react-media'
import { Redirect } from 'react-router-dom'
import { formatDate } from '../../helpers/utils/util'
import HomeNotification from '../../components/app/home-notification'
import DaiMigrationNotification from '../../components/app/dai-migration-component'
import MultipleNotifications from '../../components/app/multiple-notifications'
import WalletView from '../../components/app/wallet-view'
import TransactionView from '../../components/app/transaction-view'
@ -23,6 +24,7 @@ export default class Home extends PureComponent {
static defaultProps = {
unsetMigratedPrivacyMode: null,
hasDaiV1Token: false,
}
static propTypes = {
@ -43,6 +45,7 @@ export default class Home extends PureComponent {
restoreFromThreeBox: PropTypes.func,
setShowRestorePromptToFalse: PropTypes.func,
threeBoxLastUpdated: PropTypes.number,
hasDaiV1Token: PropTypes.bool,
}
componentWillMount () {
@ -86,6 +89,7 @@ export default class Home extends PureComponent {
forgottenPassword,
providerRequests,
history,
hasDaiV1Token,
showPrivacyModeNotification,
unsetMigratedPrivacyMode,
shouldShowSeedPhraseReminder,
@ -172,6 +176,11 @@ export default class Home extends PureComponent {
/>
: null
}
{
hasDaiV1Token
? <DaiMigrationNotification />
: null
}
</MultipleNotifications>
</TransactionView>
)

View File

@ -3,7 +3,7 @@ import { compose } from 'recompose'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
import { getCurrentEthBalance } from '../../selectors/selectors'
import { getCurrentEthBalance, getDaiV1Token } from '../../selectors/selectors'
import {
unsetMigratedPrivacyMode,
restoreFromThreeBox,
@ -44,6 +44,7 @@ const mapStateToProps = state => {
showRestorePrompt,
selectedAddress,
threeBoxLastUpdated,
hasDaiV1Token: Boolean(getDaiV1Token(state)),
}
}

View File

@ -49,6 +49,7 @@ const selectors = {
getAccountType,
getNumberOfAccounts,
getNumberOfTokens,
getDaiV1Token,
isEthereumNetwork,
getMetaMetricState,
getRpcPrefsForCurrentProvider,
@ -225,6 +226,12 @@ function getAddressBookEntryName (state, address) {
return entry && entry.name !== '' ? entry.name : addressSlicer(address)
}
function getDaiV1Token (state) {
const OLD_DAI_CONTRACT_ADDRESS = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'
const tokens = state.metamask.tokens || []
return tokens.find(({address}) => checksumAddress(address) === OLD_DAI_CONTRACT_ADDRESS)
}
function accountsWithSendEtherInfoSelector (state) {
const accounts = getMetaMaskAccounts(state)
const { identities } = state.metamask

View File

@ -10038,10 +10038,10 @@ eth-block-tracker@^4.4.2:
pify "^3.0.0"
safe-event-emitter "^1.0.1"
eth-contract-metadata@1.9.3, eth-contract-metadata@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/eth-contract-metadata/-/eth-contract-metadata-1.9.3.tgz#d627d81cb6dadbe9d9261ec9594617ada38a25f2"
integrity sha512-qDdH9n2yw5GqWW5E6wrh7KZ8WicpEzofrpuJG3FWiJew+Yt6RapnqtXN8ljvxY+UTZPd1QzLXswKfpJyzsH4Tw==
eth-contract-metadata@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/eth-contract-metadata/-/eth-contract-metadata-1.11.0.tgz#4d23a8208d5d53be9d4c0696ed8492b505c6bca1"
integrity sha512-Bbvio71M+lH+qXd8XXddpTc8hhjL9m4fNPOxmZFIX8z0/VooUdwV8YmmDAbkU5WVioZi+Jp1XaoO7VwzXnDboA==
eth-ens-namehash@2.0.8, eth-ens-namehash@^2.0.8:
version "2.0.8"
@ -12160,13 +12160,13 @@ fuse.js@^3.4.4:
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.5.tgz#8954fb43f9729bd5dbcb8c08f251db552595a7a6"
integrity sha512-s9PGTaQIkT69HaeoTVjwGsLfb8V8ScJLx5XGFcKHg0MqLUH/UZ4EKOtqtXX9k7AFqCGxD1aJmYb8Q5VYDibVRQ==
gaba@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/gaba/-/gaba-1.8.0.tgz#5370e5d662de6aa8e4e41de791da0996a7e12dbe"
integrity sha512-M20fZ6yKRefxgxb82l5Of0VutFxvc1Uxg8LSncaiq5kWQZO1UNe5pkxQc4EQT9rGAcBm6ASv7FG0B04syIELRA==
gaba@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/gaba/-/gaba-1.9.0.tgz#ccd9f99c56687b5acd39f9e3ceb435b2a59b6aa1"
integrity sha512-HoVreAdZssL0jNHuzZ7WP+YKZ0riu44jVDWxhQ9hsgPuzxbVEsz9fO/HDxqAdNZS1Cswayq6+ciZ3HSCFWMKbQ==
dependencies:
await-semaphore "^0.1.3"
eth-contract-metadata "^1.9.1"
eth-contract-metadata "^1.11.0"
eth-ens-namehash "^2.0.8"
eth-json-rpc-infura "^4.0.1"
eth-keyring-controller "^5.3.0"