Merge pull request #4884 from MetaMask/TokensPerAccountBasis

Tokens per account & network basis
This commit is contained in:
Dan Finlay 2018-08-07 13:30:26 -07:00 committed by GitHub
commit e9f74f005d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 287 additions and 37 deletions

View File

@ -2,6 +2,8 @@
## Current Master ## Current Master
- [#4884](https://github.com/MetaMask/metamask-extension/pull/4884): Allow to have tokens per account and network.
## 4.9.0 Tue Aug 07 2018 ## 4.9.0 Tue Aug 07 2018
- Add new tokens auto detection - Add new tokens auto detection

View File

@ -85,7 +85,7 @@ class DetectTokensController {
set preferences (preferences) { set preferences (preferences) {
if (!preferences) { return } if (!preferences) { return }
this._preferences = preferences this._preferences = preferences
preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) }) preferences.store.subscribe(({ tokens = [] }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ selectedAddress }) => { preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) { if (this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress this.selectedAddress = selectedAddress

View File

@ -13,6 +13,7 @@ class PreferencesController {
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user * @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {string} store.currentAccountTab Indicates the selected tab in the ui * @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists * @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature * user wishes to see that feature
@ -24,6 +25,7 @@ class PreferencesController {
const initState = extend({ const initState = extend({
frequentRpcList: [], frequentRpcList: [],
currentAccountTab: 'history', currentAccountTab: 'history',
accountTokens: {},
tokens: [], tokens: [],
useBlockie: false, useBlockie: false,
featureFlags: {}, featureFlags: {},
@ -33,8 +35,9 @@ class PreferencesController {
}, opts.initState) }, opts.initState)
this.diagnostics = opts.diagnostics this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this._subscribeProviderType()
} }
// PUBLIC METHODS // PUBLIC METHODS
@ -77,12 +80,19 @@ class PreferencesController {
*/ */
setAddresses (addresses) { setAddresses (addresses) {
const oldIdentities = this.store.getState().identities const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
const identities = addresses.reduce((ids, address, index) => { const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {} const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId} ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids return ids
}, {}) }, {})
this.store.updateState({ identities }) const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}
tokens[address] = oldTokens
return tokens
}, {})
this.store.updateState({ identities, accountTokens })
} }
/** /**
@ -93,11 +103,13 @@ class PreferencesController {
*/ */
removeAddress (address) { removeAddress (address) {
const identities = this.store.getState().identities const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
if (!identities[address]) { if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`) throw new Error(`${address} can't be deleted cause it was not found`)
} }
delete identities[address] delete identities[address]
this.store.updateState({ identities }) delete accountTokens[address]
this.store.updateState({ identities, accountTokens })
// If the selected account is no longer valid, // If the selected account is no longer valid,
// select an arbitrary other account: // select an arbitrary other account:
@ -117,14 +129,17 @@ class PreferencesController {
*/ */
addAddresses (addresses) { addAddresses (addresses) {
const identities = this.store.getState().identities const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
addresses.forEach((address) => { addresses.forEach((address) => {
// skip if already exists // skip if already exists
if (identities[address]) return if (identities[address]) return
// add missing identity // add missing identity
const identityCount = Object.keys(identities).length const identityCount = Object.keys(identities).length
accountTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address } identities[address] = { name: `Account ${identityCount + 1}`, address }
}) })
this.store.updateState({ identities }) this.store.updateState({ identities, accountTokens })
} }
/* /*
@ -175,15 +190,15 @@ class PreferencesController {
* Setter for the `selectedAddress` property * Setter for the `selectedAddress` property
* *
* @param {string} _address A new hex address for an account * @param {string} _address A new hex address for an account
* @returns {Promise<void>} Promise resolves with undefined * @returns {Promise<void>} Promise resolves with tokens
* *
*/ */
setSelectedAddress (_address) { setSelectedAddress (_address) {
return new Promise((resolve, reject) => { const address = normalizeAddress(_address)
const address = normalizeAddress(_address) this._updateTokens(address)
this.store.updateState({ selectedAddress: address }) this.store.updateState({ selectedAddress: address })
resolve() const tokens = this.store.getState().tokens
}) return Promise.resolve(tokens)
} }
/** /**
@ -232,9 +247,7 @@ class PreferencesController {
} else { } else {
tokens.push(newEntry) tokens.push(newEntry)
} }
this._updateAccountTokens(tokens)
this.store.updateState({ tokens })
return Promise.resolve(tokens) return Promise.resolve(tokens)
} }
@ -247,10 +260,8 @@ class PreferencesController {
*/ */
removeToken (rawAddress) { removeToken (rawAddress) {
const tokens = this.store.getState().tokens const tokens = this.store.getState().tokens
const updatedTokens = tokens.filter(token => token.address !== rawAddress) const updatedTokens = tokens.filter(token => token.address !== rawAddress)
this._updateAccountTokens(updatedTokens)
this.store.updateState({ tokens: updatedTokens })
return Promise.resolve(updatedTokens) return Promise.resolve(updatedTokens)
} }
@ -376,6 +387,57 @@ class PreferencesController {
// //
// PRIVATE METHODS // PRIVATE METHODS
// //
/**
* Subscription to network provider type.
*
*
*/
_subscribeProviderType () {
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
})
}
/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
*
* @param {array} tokens Array of tokens to be updated.
*
*/
_updateAccountTokens (tokens) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens })
}
/**
* Updates `tokens` of current account and network.
*
* @param {string} selectedAddress Account address to be updated with.
*
*/
_updateTokens (selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
}
/**
* A getter for `tokens` and `accountTokens` related states.
*
* @param {string} selectedAddress A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
*
*/
_getTokenRelatedStates (selectedAddress) {
const accountTokens = this.store.getState().accountTokens
if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress
const providerType = this.network.providerStore.getState().type
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = []
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
} }
module.exports = PreferencesController module.exports = PreferencesController

View File

@ -87,6 +87,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({ this.preferencesController = new PreferencesController({
initState: initState.PreferencesController, initState: initState.PreferencesController,
initLangCode: opts.initLangCode, initLangCode: opts.initLangCode,
network: this.networkController,
}) })
// currency controller // currency controller
@ -1436,7 +1437,7 @@ module.exports = class MetamaskController extends EventEmitter {
} }
/** /**
* A method for activating the retrieval of price data and auto detect tokens, * A method for activating the retrieval of price data,
* which should only be fetched when the UI is visible. * which should only be fetched when the UI is visible.
* @private * @private
* @param {boolean} active - True if price data should be getting fetched. * @param {boolean} active - True if price data should be getting fetched.

View File

@ -0,0 +1,40 @@
// next version number
const version = 28
/*
normalizes txParams on unconfirmed txs
*/
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
if (newState.PreferencesController) {
if (newState.PreferencesController.tokens) {
const identities = newState.TransactionController.identities
const tokens = newState.PreferencesController.tokens
newState.PreferencesController.accountTokens = {}
for (const identity in identities) {
newState.PreferencesController.accountTokens[identity] = {'mainnet': tokens}
}
newState.PreferencesController.tokens = []
}
}
return newState
}

View File

@ -38,4 +38,5 @@ module.exports = [
require('./025'), require('./025'),
require('./026'), require('./026'),
require('./027'), require('./027'),
require('./028'),
] ]

View File

@ -7,10 +7,11 @@ const PreferencesController = require('../../../../app/scripts/controllers/prefe
describe('DetectTokensController', () => { describe('DetectTokensController', () => {
const sandbox = sinon.createSandbox() const sandbox = sinon.createSandbox()
let clock let clock, keyringMemStore, network, preferences
let keyringMemStore beforeEach(async () => {
before(async () => {
keyringMemStore = new ObservableStore({ isUnlocked: false}) keyringMemStore = new ObservableStore({ isUnlocked: false})
network = new NetworkController({ provider: { type: 'mainnet' }})
preferences = new PreferencesController({ network })
}) })
after(() => { after(() => {
sandbox.restore() sandbox.restore()
@ -25,9 +26,7 @@ describe('DetectTokensController', () => {
it('should be called on every polling period', async () => { it('should be called on every polling period', async () => {
clock = sandbox.useFakeTimers() clock = sandbox.useFakeTimers()
const network = new NetworkController()
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -45,9 +44,7 @@ describe('DetectTokensController', () => {
}) })
it('should not check tokens while in test network', async () => { it('should not check tokens while in test network', async () => {
const network = new NetworkController()
network.setProviderType('rinkeby') network.setProviderType('rinkeby')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -61,9 +58,7 @@ describe('DetectTokensController', () => {
}) })
it('should only check and add tokens while in main network', async () => { it('should only check and add tokens while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -80,9 +75,7 @@ describe('DetectTokensController', () => {
}) })
it('should not detect same token while in main network', async () => { it('should not detect same token while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController()
preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8) preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
@ -100,9 +93,7 @@ describe('DetectTokensController', () => {
}) })
it('should trigger detect new tokens when change address', async () => { it('should trigger detect new tokens when change address', async () => {
const network = new NetworkController()
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -112,9 +103,7 @@ describe('DetectTokensController', () => {
}) })
it('should trigger detect new tokens when submit password', async () => { it('should trigger detect new tokens when submit password', async () => {
const network = new NetworkController()
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.selectedAddress = '0x0' controller.selectedAddress = '0x0'
@ -124,9 +113,7 @@ describe('DetectTokensController', () => {
}) })
it('should not trigger detect new tokens when not open or not unlocked', async () => { it('should not trigger detect new tokens when not open or not unlocked', async () => {
const network = new NetworkController()
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = false controller.isUnlocked = false

View File

@ -1,11 +1,14 @@
const assert = require('assert') const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences') const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('preferences controller', function () { describe('preferences controller', function () {
let preferencesController let preferencesController
let network
beforeEach(() => { beforeEach(() => {
preferencesController = new PreferencesController() network = {providerStore: new ObservableStore({ type: 'mainnet' })}
preferencesController = new PreferencesController({ network })
}) })
describe('setAddresses', function () { describe('setAddresses', function () {
@ -28,6 +31,20 @@ describe('preferences controller', function () {
}) })
}) })
it('should create account tokens for each account in the store', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
const accountTokens = preferencesController.store.getState().accountTokens
assert.deepEqual(accountTokens, {
'0xda22le': {},
'0x7e57e2': {},
})
})
it('should replace its list of addresses', function () { it('should replace its list of addresses', function () {
preferencesController.setAddresses([ preferencesController.setAddresses([
'0xda22le', '0xda22le',
@ -64,6 +81,17 @@ describe('preferences controller', function () {
assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined) assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined)
}) })
it('should remove an address from state and respective tokens', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.removeAddress('0xda22le')
assert.equal(preferencesController.store.getState().accountTokens['0xda22le'], undefined)
})
it('should switch accounts if the selected address is removed', function () { it('should switch accounts if the selected address is removed', function () {
preferencesController.setAddresses([ preferencesController.setAddresses([
'0xda22le', '0xda22le',
@ -158,6 +186,42 @@ describe('preferences controller', function () {
await preferencesController.addToken(address, symbol, decimals) await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address') assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address')
}) })
it('should add token per account', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0xda22le')
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two account and tokens are equal')
})
it('should add token per network', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
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' })
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two networks and tokens are equal')
})
}) })
describe('removeToken', function () { describe('removeToken', function () {
@ -182,6 +246,98 @@ describe('preferences controller', function () {
const [token1] = tokens const [token1] = tokens
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5}) assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
}) })
it('should remove a token from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in account')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for account')
})
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' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in network')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for network')
})
})
describe('on setSelectedAddress', function () {
it('should update tokens from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
await preferencesController.setSelectedAddress('0x7e57e2')
const initialTokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different accounts and tokens')
await preferencesController.setSelectedAddress('0x7e57e2')
const tokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same account')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same account')
})
})
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' })
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different networks and tokens')
network.providerStore.updateState({ type: 'mainnet' })
const tokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same network')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
})
}) })
}) })

View File

@ -1483,11 +1483,12 @@ function showAccountDetail (address) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
log.debug(`background.setSelectedAddress`) log.debug(`background.setSelectedAddress`)
background.setSelectedAddress(address, (err) => { background.setSelectedAddress(address, (err, tokens) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
return dispatch(actions.displayWarning(err.message)) return dispatch(actions.displayWarning(err.message))
} }
dispatch(updateTokens(tokens))
dispatch({ dispatch({
type: actions.SHOW_ACCOUNT_DETAIL, type: actions.SHOW_ACCOUNT_DETAIL,
value: address, value: address,