mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
add erc-721 token detection and flag to disable sending (#11210)
* add erc-721 token detection and flag to disable sending * addressing feedback * remove redundant provider instantiation * fix issue caused by unprotected destructuring * add tests and documentation * move add isERC721 flag to useTokenTracker hook * Update and unit tests * use memoizedTokens in useTokenTracker Co-authored-by: Dan Miller <danjm.com@gmail.com>
This commit is contained in:
parent
be244f4908
commit
e10ddbe3a3
@ -2387,6 +2387,10 @@
|
|||||||
"message": "verify the network details",
|
"message": "verify the network details",
|
||||||
"description": "Serves as link text for the 'unrecognizedChain' key. This text will be embedded inside the translation for that key."
|
"description": "Serves as link text for the 'unrecognizedChain' key. This text will be embedded inside the translation for that key."
|
||||||
},
|
},
|
||||||
|
"unsendableAsset": {
|
||||||
|
"message": "Sending collectible (ERC-721) tokens is not currently supported",
|
||||||
|
"description": "This is an error message we show the user if they attempt to send a collectible asset type, for which currently don't support sending"
|
||||||
|
},
|
||||||
"updatedWithDate": {
|
"updatedWithDate": {
|
||||||
"message": "Updated $1"
|
"message": "Updated $1"
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@ import PreferencesController from './preferences';
|
|||||||
|
|
||||||
describe('DetectTokensController', function () {
|
describe('DetectTokensController', function () {
|
||||||
const sandbox = sinon.createSandbox();
|
const sandbox = sinon.createSandbox();
|
||||||
let keyringMemStore, network, preferences;
|
let keyringMemStore, network, preferences, provider;
|
||||||
|
|
||||||
const noop = () => undefined;
|
const noop = () => undefined;
|
||||||
|
|
||||||
@ -23,12 +23,16 @@ describe('DetectTokensController', function () {
|
|||||||
keyringMemStore = new ObservableStore({ isUnlocked: false });
|
keyringMemStore = new ObservableStore({ isUnlocked: false });
|
||||||
network = new NetworkController();
|
network = new NetworkController();
|
||||||
network.setInfuraProjectId('foo');
|
network.setInfuraProjectId('foo');
|
||||||
preferences = new PreferencesController({ network });
|
network.initializeProvider(networkControllerProviderConfig);
|
||||||
|
provider = network.getProviderAndBlockTracker().provider;
|
||||||
|
preferences = new PreferencesController({ network, provider });
|
||||||
preferences.setAddresses([
|
preferences.setAddresses([
|
||||||
'0x7e57e2',
|
'0x7e57e2',
|
||||||
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
|
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
|
||||||
]);
|
]);
|
||||||
network.initializeProvider(networkControllerProviderConfig);
|
sandbox
|
||||||
|
.stub(preferences, '_detectIsERC721')
|
||||||
|
.returns(Promise.resolve(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function () {
|
after(function () {
|
||||||
@ -125,6 +129,7 @@ describe('DetectTokensController', function () {
|
|||||||
address: existingTokenAddress.toLowerCase(),
|
address: existingTokenAddress.toLowerCase(),
|
||||||
decimals: existingToken.decimals,
|
decimals: existingToken.decimals,
|
||||||
symbol: existingToken.symbol,
|
symbol: existingToken.symbol,
|
||||||
|
isERC721: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -177,11 +182,13 @@ describe('DetectTokensController', function () {
|
|||||||
address: existingTokenAddress.toLowerCase(),
|
address: existingTokenAddress.toLowerCase(),
|
||||||
decimals: existingToken.decimals,
|
decimals: existingToken.decimals,
|
||||||
symbol: existingToken.symbol,
|
symbol: existingToken.symbol,
|
||||||
|
isERC721: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
address: tokenAddressToAdd.toLowerCase(),
|
address: tokenAddressToAdd.toLowerCase(),
|
||||||
decimals: tokenToAdd.decimals,
|
decimals: tokenToAdd.decimals,
|
||||||
symbol: tokenToAdd.symbol,
|
symbol: tokenToAdd.symbol,
|
||||||
|
isERC721: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -234,11 +241,13 @@ describe('DetectTokensController', function () {
|
|||||||
address: existingTokenAddress.toLowerCase(),
|
address: existingTokenAddress.toLowerCase(),
|
||||||
decimals: existingToken.decimals,
|
decimals: existingToken.decimals,
|
||||||
symbol: existingToken.symbol,
|
symbol: existingToken.symbol,
|
||||||
|
isERC721: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
address: tokenAddressToAdd.toLowerCase(),
|
address: tokenAddressToAdd.toLowerCase(),
|
||||||
decimals: tokenToAdd.decimals,
|
decimals: tokenToAdd.decimals,
|
||||||
symbol: tokenToAdd.symbol,
|
symbol: tokenToAdd.symbol,
|
||||||
|
isERC721: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -2,14 +2,21 @@ import { strict as assert } from 'assert';
|
|||||||
import { ObservableStore } from '@metamask/obs-store';
|
import { ObservableStore } from '@metamask/obs-store';
|
||||||
import { ethErrors } from 'eth-rpc-errors';
|
import { ethErrors } from 'eth-rpc-errors';
|
||||||
import { normalize as normalizeAddress } from 'eth-sig-util';
|
import { normalize as normalizeAddress } from 'eth-sig-util';
|
||||||
import ethers from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
import abiERC721 from 'human-standard-collectible-abi';
|
||||||
|
import contractsMap from '@metamask/contract-metadata';
|
||||||
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens';
|
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens';
|
||||||
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
|
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
|
||||||
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
||||||
import { isValidHexAddress } from '../../../shared/modules/hexstring-utils';
|
import {
|
||||||
|
isValidHexAddress,
|
||||||
|
toChecksumHexAddress,
|
||||||
|
} from '../../../shared/modules/hexstring-utils';
|
||||||
import { NETWORK_EVENTS } from './network';
|
import { NETWORK_EVENTS } from './network';
|
||||||
|
|
||||||
|
const ERC721METADATA_INTERFACE_ID = '0x5b5e139f';
|
||||||
|
|
||||||
export default class PreferencesController {
|
export default class PreferencesController {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -73,11 +80,18 @@ export default class PreferencesController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.network = opts.network;
|
this.network = opts.network;
|
||||||
|
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
|
||||||
this.store = new ObservableStore(initState);
|
this.store = new ObservableStore(initState);
|
||||||
this.store.setMaxListeners(12);
|
this.store.setMaxListeners(12);
|
||||||
this.openPopup = opts.openPopup;
|
this.openPopup = opts.openPopup;
|
||||||
this.migrateAddressBookState = opts.migrateAddressBookState;
|
this.migrateAddressBookState = opts.migrateAddressBookState;
|
||||||
this._subscribeToNetworkDidChange();
|
|
||||||
|
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
|
||||||
|
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
|
||||||
|
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
|
||||||
|
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
|
||||||
|
});
|
||||||
|
|
||||||
this._subscribeToInfuraAvailability();
|
this._subscribeToInfuraAvailability();
|
||||||
|
|
||||||
global.setPreference = (key, value) => {
|
global.setPreference = (key, value) => {
|
||||||
@ -393,6 +407,8 @@ export default class PreferencesController {
|
|||||||
});
|
});
|
||||||
const previousIndex = tokens.indexOf(previousEntry);
|
const previousIndex = tokens.indexOf(previousEntry);
|
||||||
|
|
||||||
|
newEntry.isERC721 = await this._detectIsERC721(newEntry.address);
|
||||||
|
|
||||||
if (previousEntry) {
|
if (previousEntry) {
|
||||||
tokens[previousIndex] = newEntry;
|
tokens[previousIndex] = newEntry;
|
||||||
} else {
|
} else {
|
||||||
@ -403,6 +419,24 @@ export default class PreferencesController {
|
|||||||
return Promise.resolve(tokens);
|
return Promise.resolve(tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds isERC721 field to token object
|
||||||
|
* (Called when a user attempts to add tokens that were previously added which do not yet had isERC721 field)
|
||||||
|
*
|
||||||
|
* @param {string} tokenAddress - The contract address of the token requiring the isERC721 field added.
|
||||||
|
* @returns {Promise<object>} The new token object with the added isERC721 field.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async updateTokenType(tokenAddress) {
|
||||||
|
const { tokens } = this.store.getState();
|
||||||
|
const tokenIndex = tokens.findIndex((token) => {
|
||||||
|
return token.address === tokenAddress;
|
||||||
|
});
|
||||||
|
tokens[tokenIndex].isERC721 = await this._detectIsERC721(tokenAddress);
|
||||||
|
this.store.updateState({ tokens });
|
||||||
|
return Promise.resolve(tokens[tokenIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a specified token from the tokens array and adds it to hiddenTokens array
|
* Removes a specified token from the tokens array and adds it to hiddenTokens array
|
||||||
*
|
*
|
||||||
@ -480,11 +514,8 @@ export default class PreferencesController {
|
|||||||
let addressBookKey = rpcDetail.chainId;
|
let addressBookKey = rpcDetail.chainId;
|
||||||
if (!addressBookKey) {
|
if (!addressBookKey) {
|
||||||
// We need to find the networkId to determine what these addresses were keyed by
|
// We need to find the networkId to determine what these addresses were keyed by
|
||||||
const provider = new ethers.providers.JsonRpcProvider(
|
|
||||||
rpcDetail.rpcUrl,
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
addressBookKey = await provider.send('net_version');
|
addressBookKey = await this.ethersProvider.send('net_version');
|
||||||
assert(typeof addressBookKey === 'string');
|
assert(typeof addressBookKey === 'string');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.debug(error);
|
log.debug(error);
|
||||||
@ -701,17 +732,6 @@ export default class PreferencesController {
|
|||||||
// PRIVATE METHODS
|
// PRIVATE METHODS
|
||||||
//
|
//
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle updating token list to reflect current network by listening for the
|
|
||||||
* NETWORK_DID_CHANGE event.
|
|
||||||
*/
|
|
||||||
_subscribeToNetworkDidChange() {
|
|
||||||
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
|
|
||||||
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
|
|
||||||
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_subscribeToInfuraAvailability() {
|
_subscribeToInfuraAvailability() {
|
||||||
this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => {
|
this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => {
|
||||||
this._setInfuraBlocked(true);
|
this._setInfuraBlocked(true);
|
||||||
@ -763,6 +783,43 @@ export default class PreferencesController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether or not a token is ERC-721 compatible.
|
||||||
|
*
|
||||||
|
* @param {string} tokensAddress - the token contract address.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async _detectIsERC721(tokenAddress) {
|
||||||
|
const checksumAddress = toChecksumHexAddress(tokenAddress);
|
||||||
|
// if this token is already in our contract metadata map we don't need
|
||||||
|
// to check against the contract
|
||||||
|
if (contractsMap[checksumAddress]?.erc721 === true) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
const tokenContract = await this._createEthersContract(
|
||||||
|
tokenAddress,
|
||||||
|
abiERC721,
|
||||||
|
this.ethersProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await tokenContract
|
||||||
|
.supportsInterface(ERC721METADATA_INTERFACE_ID)
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('error', error);
|
||||||
|
log.debug(error);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _createEthersContract(tokenAddress, abi, ethersProvider) {
|
||||||
|
const tokenContract = await new ethers.Contract(
|
||||||
|
tokenAddress,
|
||||||
|
abi,
|
||||||
|
ethersProvider,
|
||||||
|
);
|
||||||
|
return tokenContract;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates `tokens` and `hiddenTokens` of current account and network.
|
* Updates `tokens` and `hiddenTokens` of current account and network.
|
||||||
*
|
*
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import contractMaps from '@metamask/contract-metadata';
|
||||||
|
import abiERC721 from 'human-standard-collectible-abi';
|
||||||
import {
|
import {
|
||||||
MAINNET_CHAIN_ID,
|
MAINNET_CHAIN_ID,
|
||||||
RINKEBY_CHAIN_ID,
|
RINKEBY_CHAIN_ID,
|
||||||
} from '../../../shared/constants/network';
|
} from '../../../shared/constants/network';
|
||||||
import PreferencesController from './preferences';
|
import PreferencesController from './preferences';
|
||||||
|
import NetworkController from './network';
|
||||||
|
|
||||||
describe('preferences controller', function () {
|
describe('preferences controller', function () {
|
||||||
let preferencesController;
|
let preferencesController;
|
||||||
@ -13,19 +16,32 @@ describe('preferences controller', function () {
|
|||||||
let triggerNetworkChange;
|
let triggerNetworkChange;
|
||||||
let switchToMainnet;
|
let switchToMainnet;
|
||||||
let switchToRinkeby;
|
let switchToRinkeby;
|
||||||
|
let provider;
|
||||||
const migrateAddressBookState = sinon.stub();
|
const migrateAddressBookState = sinon.stub();
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
currentChainId = MAINNET_CHAIN_ID;
|
currentChainId = MAINNET_CHAIN_ID;
|
||||||
network = {
|
const networkControllerProviderConfig = {
|
||||||
getCurrentChainId: () => currentChainId,
|
getAccounts: () => undefined,
|
||||||
on: sinon.spy(),
|
|
||||||
};
|
};
|
||||||
|
network = new NetworkController();
|
||||||
|
network.setInfuraProjectId('foo');
|
||||||
|
network.initializeProvider(networkControllerProviderConfig);
|
||||||
|
provider = network.getProviderAndBlockTracker().provider;
|
||||||
|
|
||||||
|
sandbox.stub(network, 'getCurrentChainId').callsFake(() => currentChainId);
|
||||||
|
sandbox
|
||||||
|
.stub(network, 'getProviderConfig')
|
||||||
|
.callsFake(() => ({ type: 'mainnet' }));
|
||||||
|
const spy = sandbox.spy(network, 'on');
|
||||||
|
|
||||||
preferencesController = new PreferencesController({
|
preferencesController = new PreferencesController({
|
||||||
migrateAddressBookState,
|
migrateAddressBookState,
|
||||||
network,
|
network,
|
||||||
|
provider,
|
||||||
});
|
});
|
||||||
triggerNetworkChange = network.on.firstCall.args[1];
|
triggerNetworkChange = spy.firstCall.args[1];
|
||||||
switchToMainnet = () => {
|
switchToMainnet = () => {
|
||||||
currentChainId = MAINNET_CHAIN_ID;
|
currentChainId = MAINNET_CHAIN_ID;
|
||||||
triggerNetworkChange();
|
triggerNetworkChange();
|
||||||
@ -86,6 +102,104 @@ describe('preferences controller', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateTokenType', function () {
|
||||||
|
it('should add isERC721 = true to token object in state when token is collectible and in our contract-metadata repo', async function () {
|
||||||
|
const contractAddresses = Object.keys(contractMaps);
|
||||||
|
const erc721ContractAddresses = contractAddresses.filter(
|
||||||
|
(contractAddress) => contractMaps[contractAddress].erc721 === true,
|
||||||
|
);
|
||||||
|
const address = erc721ContractAddresses[0];
|
||||||
|
const { symbol, decimals } = contractMaps[address];
|
||||||
|
preferencesController.store.updateState({
|
||||||
|
tokens: [{ address, symbol, decimals }],
|
||||||
|
});
|
||||||
|
const result = await preferencesController.updateTokenType(address);
|
||||||
|
assert.equal(result.isERC721, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add isERC721 = true to token object in state when token is collectible and not in our contract-metadata repo', async function () {
|
||||||
|
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
|
||||||
|
preferencesController.store.updateState({
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
address: tokenAddress,
|
||||||
|
symbol: 'TESTNFT',
|
||||||
|
decimals: '0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
sinon
|
||||||
|
.stub(preferencesController, '_detectIsERC721')
|
||||||
|
.callsFake(() => true);
|
||||||
|
|
||||||
|
const result = await preferencesController.updateTokenType(tokenAddress);
|
||||||
|
assert.equal(
|
||||||
|
preferencesController._detectIsERC721.getCall(0).args[0],
|
||||||
|
tokenAddress,
|
||||||
|
);
|
||||||
|
assert.equal(result.isERC721, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_detectIsERC721', function () {
|
||||||
|
it('should return true when token is in our contract-metadata repo', async function () {
|
||||||
|
const tokenAddress = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d';
|
||||||
|
|
||||||
|
const result = await preferencesController._detectIsERC721(tokenAddress);
|
||||||
|
assert.equal(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when the token is not in our contract-metadata repo but tokenContract.supportsInterface returns true', async function () {
|
||||||
|
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
|
||||||
|
|
||||||
|
const supportsInterfaceStub = sinon.stub().returns(Promise.resolve(true));
|
||||||
|
sinon
|
||||||
|
.stub(preferencesController, '_createEthersContract')
|
||||||
|
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
|
||||||
|
|
||||||
|
const result = await preferencesController._detectIsERC721(tokenAddress);
|
||||||
|
assert.equal(
|
||||||
|
preferencesController._createEthersContract.getCall(0).args[0],
|
||||||
|
tokenAddress,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
preferencesController._createEthersContract.getCall(0).args[1],
|
||||||
|
abiERC721,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
preferencesController._createEthersContract.getCall(0).args[2],
|
||||||
|
preferencesController.ethersProvider,
|
||||||
|
);
|
||||||
|
assert.equal(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the token is not in our contract-metadata repo and tokenContract.supportsInterface returns false', async function () {
|
||||||
|
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
|
||||||
|
|
||||||
|
const supportsInterfaceStub = sinon
|
||||||
|
.stub()
|
||||||
|
.returns(Promise.resolve(false));
|
||||||
|
sinon
|
||||||
|
.stub(preferencesController, '_createEthersContract')
|
||||||
|
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
|
||||||
|
|
||||||
|
const result = await preferencesController._detectIsERC721(tokenAddress);
|
||||||
|
assert.equal(
|
||||||
|
preferencesController._createEthersContract.getCall(0).args[0],
|
||||||
|
tokenAddress,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
preferencesController._createEthersContract.getCall(0).args[1],
|
||||||
|
abiERC721,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
preferencesController._createEthersContract.getCall(0).args[2],
|
||||||
|
preferencesController.ethersProvider,
|
||||||
|
);
|
||||||
|
assert.equal(result, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('removeAddress', function () {
|
describe('removeAddress', function () {
|
||||||
it('should remove an address from state', function () {
|
it('should remove an address from state', function () {
|
||||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||||
@ -291,7 +405,12 @@ describe('preferences controller', function () {
|
|||||||
assert.equal(tokens.length, 1, 'one token removed');
|
assert.equal(tokens.length, 1, 'one token removed');
|
||||||
|
|
||||||
const [token1] = tokens;
|
const [token1] = tokens;
|
||||||
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
|
assert.deepEqual(token1, {
|
||||||
|
address: '0xb',
|
||||||
|
symbol: 'B',
|
||||||
|
decimals: 5,
|
||||||
|
isERC721: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove a token from its state on corresponding address', async function () {
|
it('should remove a token from its state on corresponding address', async function () {
|
||||||
@ -310,7 +429,12 @@ describe('preferences controller', function () {
|
|||||||
assert.equal(tokensFirst.length, 1, 'one token removed in account');
|
assert.equal(tokensFirst.length, 1, 'one token removed in account');
|
||||||
|
|
||||||
const [token1] = tokensFirst;
|
const [token1] = tokensFirst;
|
||||||
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
|
assert.deepEqual(token1, {
|
||||||
|
address: '0xb',
|
||||||
|
symbol: 'B',
|
||||||
|
decimals: 5,
|
||||||
|
isERC721: false,
|
||||||
|
});
|
||||||
|
|
||||||
await preferencesController.setSelectedAddress('0x7e57e3');
|
await preferencesController.setSelectedAddress('0x7e57e3');
|
||||||
const tokensSecond = preferencesController.getTokens();
|
const tokensSecond = preferencesController.getTokens();
|
||||||
@ -335,7 +459,12 @@ describe('preferences controller', function () {
|
|||||||
assert.equal(tokensFirst.length, 1, 'one token removed in network');
|
assert.equal(tokensFirst.length, 1, 'one token removed in network');
|
||||||
|
|
||||||
const [token1] = tokensFirst;
|
const [token1] = tokensFirst;
|
||||||
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
|
assert.deepEqual(token1, {
|
||||||
|
address: '0xb',
|
||||||
|
symbol: 'B',
|
||||||
|
decimals: 5,
|
||||||
|
isERC721: false,
|
||||||
|
});
|
||||||
|
|
||||||
switchToRinkeby();
|
switchToRinkeby();
|
||||||
const tokensSecond = preferencesController.getTokens();
|
const tokensSecond = preferencesController.getTokens();
|
||||||
|
@ -132,11 +132,17 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
this.networkController = new NetworkController(initState.NetworkController);
|
this.networkController = new NetworkController(initState.NetworkController);
|
||||||
this.networkController.setInfuraProjectId(opts.infuraProjectId);
|
this.networkController.setInfuraProjectId(opts.infuraProjectId);
|
||||||
|
|
||||||
|
// now we can initialize the RPC provider, which other controllers require
|
||||||
|
this.initializeProvider();
|
||||||
|
this.provider = this.networkController.getProviderAndBlockTracker().provider;
|
||||||
|
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker;
|
||||||
|
|
||||||
this.preferencesController = new PreferencesController({
|
this.preferencesController = new PreferencesController({
|
||||||
initState: initState.PreferencesController,
|
initState: initState.PreferencesController,
|
||||||
initLangCode: opts.initLangCode,
|
initLangCode: opts.initLangCode,
|
||||||
openPopup: opts.openPopup,
|
openPopup: opts.openPopup,
|
||||||
network: this.networkController,
|
network: this.networkController,
|
||||||
|
provider: this.provider,
|
||||||
migrateAddressBookState: this.migrateAddressBookState.bind(this),
|
migrateAddressBookState: this.migrateAddressBookState.bind(this),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -183,11 +189,6 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
initState.NotificationController,
|
initState.NotificationController,
|
||||||
);
|
);
|
||||||
|
|
||||||
// now we can initialize the RPC provider, which other controllers require
|
|
||||||
this.initializeProvider();
|
|
||||||
this.provider = this.networkController.getProviderAndBlockTracker().provider;
|
|
||||||
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker;
|
|
||||||
|
|
||||||
// token exchange rate tracker
|
// token exchange rate tracker
|
||||||
this.tokenRatesController = new TokenRatesController({
|
this.tokenRatesController = new TokenRatesController({
|
||||||
preferences: this.preferencesController.store,
|
preferences: this.preferencesController.store,
|
||||||
@ -731,6 +732,10 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
preferencesController,
|
preferencesController,
|
||||||
),
|
),
|
||||||
addToken: nodeify(preferencesController.addToken, preferencesController),
|
addToken: nodeify(preferencesController.addToken, preferencesController),
|
||||||
|
updateTokenType: nodeify(
|
||||||
|
preferencesController.updateTokenType,
|
||||||
|
preferencesController,
|
||||||
|
),
|
||||||
removeToken: nodeify(
|
removeToken: nodeify(
|
||||||
preferencesController.removeToken,
|
preferencesController.removeToken,
|
||||||
preferencesController,
|
preferencesController,
|
||||||
|
@ -153,6 +153,7 @@
|
|||||||
"fast-safe-stringify": "^2.0.7",
|
"fast-safe-stringify": "^2.0.7",
|
||||||
"fuse.js": "^3.2.0",
|
"fuse.js": "^3.2.0",
|
||||||
"globalthis": "^1.0.1",
|
"globalthis": "^1.0.1",
|
||||||
|
"human-standard-collectible-abi": "^1.0.2",
|
||||||
"human-standard-token-abi": "^2.0.0",
|
"human-standard-token-abi": "^2.0.0",
|
||||||
"immer": "^8.0.1",
|
"immer": "^8.0.1",
|
||||||
"json-rpc-engine": "^6.1.0",
|
"json-rpc-engine": "^6.1.0",
|
||||||
|
@ -27,6 +27,7 @@ const AssetListItem = ({
|
|||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
identiconBorder,
|
identiconBorder,
|
||||||
|
isERC721,
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -121,10 +122,12 @@ const AssetListItem = ({
|
|||||||
}
|
}
|
||||||
midContent={midContent}
|
midContent={midContent}
|
||||||
rightContent={
|
rightContent={
|
||||||
<>
|
!isERC721 && (
|
||||||
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
|
<>
|
||||||
{sendTokenButton}
|
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
|
||||||
</>
|
{sendTokenButton}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -143,6 +146,7 @@ AssetListItem.propTypes = {
|
|||||||
'primary': PropTypes.string,
|
'primary': PropTypes.string,
|
||||||
'secondary': PropTypes.string,
|
'secondary': PropTypes.string,
|
||||||
'identiconBorder': PropTypes.bool,
|
'identiconBorder': PropTypes.bool,
|
||||||
|
'isERC721': PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
AssetListItem.defaultProps = {
|
AssetListItem.defaultProps = {
|
||||||
|
@ -15,6 +15,7 @@ export default function TokenCell({
|
|||||||
string,
|
string,
|
||||||
image,
|
image,
|
||||||
onClick,
|
onClick,
|
||||||
|
isERC721,
|
||||||
}) {
|
}) {
|
||||||
const userAddress = useSelector(getSelectedAddress);
|
const userAddress = useSelector(getSelectedAddress);
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
@ -50,6 +51,7 @@ export default function TokenCell({
|
|||||||
warning={warning}
|
warning={warning}
|
||||||
primary={`${string || 0}`}
|
primary={`${string || 0}`}
|
||||||
secondary={formattedFiat}
|
secondary={formattedFiat}
|
||||||
|
isERC721={isERC721}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -62,6 +64,7 @@ TokenCell.propTypes = {
|
|||||||
string: PropTypes.string,
|
string: PropTypes.string,
|
||||||
image: PropTypes.string,
|
image: PropTypes.string,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
|
isERC721: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
TokenCell.defaultProps = {
|
TokenCell.defaultProps = {
|
||||||
|
@ -91,6 +91,7 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
Icon={SendIcon}
|
Icon={SendIcon}
|
||||||
label={t('send')}
|
label={t('send')}
|
||||||
data-testid="eth-overview-send"
|
data-testid="eth-overview-send"
|
||||||
|
disabled={token.isERC721}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
className="token-overview__button"
|
className="token-overview__button"
|
||||||
@ -145,6 +146,7 @@ TokenOverview.propTypes = {
|
|||||||
address: PropTypes.string.isRequired,
|
address: PropTypes.string.isRequired,
|
||||||
decimals: PropTypes.number,
|
decimals: PropTypes.number,
|
||||||
symbol: PropTypes.string,
|
symbol: PropTypes.string,
|
||||||
|
isERC721: PropTypes.bool,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,3 +5,4 @@ export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract';
|
|||||||
export const ETH_GAS_PRICE_FETCH_WARNING_KEY = 'ethGasPriceFetchWarning';
|
export const ETH_GAS_PRICE_FETCH_WARNING_KEY = 'ethGasPriceFetchWarning';
|
||||||
export const GAS_PRICE_FETCH_FAILURE_ERROR_KEY = 'gasPriceFetchFailed';
|
export const GAS_PRICE_FETCH_FAILURE_ERROR_KEY = 'gasPriceFetchFailed';
|
||||||
export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive';
|
export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive';
|
||||||
|
export const UNSENDABLE_ASSET_ERROR_KEY = 'unsendableAsset';
|
||||||
|
@ -23,11 +23,19 @@ export function useTokenTracker(
|
|||||||
const matchingTokens = hideZeroBalanceTokens
|
const matchingTokens = hideZeroBalanceTokens
|
||||||
? tokenWithBalances.filter((token) => Number(token.balance) > 0)
|
? tokenWithBalances.filter((token) => Number(token.balance) > 0)
|
||||||
: tokenWithBalances;
|
: tokenWithBalances;
|
||||||
setTokensWithBalances(matchingTokens);
|
// TODO: improve this pattern for adding this field when we improve support for
|
||||||
|
// EIP721 tokens.
|
||||||
|
const matchingTokensWithIsERC721Flag = matchingTokens.map((token) => {
|
||||||
|
const additionalTokenData = memoizedTokens.find(
|
||||||
|
(t) => t.address === token.address,
|
||||||
|
);
|
||||||
|
return { ...token, isERC721: additionalTokenData?.isERC721 };
|
||||||
|
});
|
||||||
|
setTokensWithBalances(matchingTokensWithIsERC721Flag);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
},
|
},
|
||||||
[hideZeroBalanceTokens],
|
[hideZeroBalanceTokens, memoizedTokens],
|
||||||
);
|
);
|
||||||
|
|
||||||
const showError = useCallback((err) => {
|
const showError = useCallback((err) => {
|
||||||
|
@ -22,6 +22,9 @@ export default class SendAssetRow extends Component {
|
|||||||
setSendToken: PropTypes.func.isRequired,
|
setSendToken: PropTypes.func.isRequired,
|
||||||
nativeCurrency: PropTypes.string,
|
nativeCurrency: PropTypes.string,
|
||||||
nativeCurrencyImage: PropTypes.string,
|
nativeCurrencyImage: PropTypes.string,
|
||||||
|
setUnsendableAssetError: PropTypes.func.isRequired,
|
||||||
|
updateSendErrors: PropTypes.func.isRequired,
|
||||||
|
updateTokenType: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -31,13 +34,41 @@ export default class SendAssetRow extends Component {
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
isShowingDropdown: false,
|
isShowingDropdown: false,
|
||||||
|
sendableTokens: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const sendableTokens = this.props.tokens.filter((token) => !token.isERC721);
|
||||||
|
this.setState({ sendableTokens });
|
||||||
|
}
|
||||||
|
|
||||||
openDropdown = () => this.setState({ isShowingDropdown: true });
|
openDropdown = () => this.setState({ isShowingDropdown: true });
|
||||||
|
|
||||||
closeDropdown = () => this.setState({ isShowingDropdown: false });
|
closeDropdown = () => this.setState({ isShowingDropdown: false });
|
||||||
|
|
||||||
selectToken = (token) => {
|
clearUnsendableAssetError = () => {
|
||||||
|
this.props.setUnsendableAssetError(false);
|
||||||
|
this.props.updateSendErrors({
|
||||||
|
unsendableAssetError: null,
|
||||||
|
gasLoadingError: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
selectToken = async (token) => {
|
||||||
|
if (token && token.isERC721 === undefined) {
|
||||||
|
const updatedToken = await this.props.updateTokenType(token.address);
|
||||||
|
if (updatedToken.isERC721) {
|
||||||
|
this.props.setUnsendableAssetError(true);
|
||||||
|
this.props.updateSendErrors({
|
||||||
|
unsendableAssetError: 'unsendableAssetError',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((token && token.isERC721 === false) || token === undefined) {
|
||||||
|
this.clearUnsendableAssetError();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
isShowingDropdown: false,
|
isShowingDropdown: false,
|
||||||
@ -65,7 +96,9 @@ export default class SendAssetRow extends Component {
|
|||||||
<SendRowWrapper label={`${t('asset')}:`}>
|
<SendRowWrapper label={`${t('asset')}:`}>
|
||||||
<div className="send-v2__asset-dropdown">
|
<div className="send-v2__asset-dropdown">
|
||||||
{this.renderSendToken()}
|
{this.renderSendToken()}
|
||||||
{this.props.tokens.length > 0 ? this.renderAssetDropdown() : null}
|
{this.state.sendableTokens.length > 0
|
||||||
|
? this.renderAssetDropdown()
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</SendRowWrapper>
|
</SendRowWrapper>
|
||||||
);
|
);
|
||||||
@ -96,7 +129,9 @@ export default class SendAssetRow extends Component {
|
|||||||
/>
|
/>
|
||||||
<div className="send-v2__asset-dropdown__list">
|
<div className="send-v2__asset-dropdown__list">
|
||||||
{this.renderNativeCurrency(true)}
|
{this.renderNativeCurrency(true)}
|
||||||
{this.props.tokens.map((token) => this.renderAsset(token, true))}
|
{this.state.sendableTokens.map((token) =>
|
||||||
|
this.renderAsset(token, true),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -119,7 +154,7 @@ export default class SendAssetRow extends Component {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
this.props.tokens.length > 0
|
this.state.sendableTokens.length > 0
|
||||||
? 'send-v2__asset-dropdown__asset'
|
? 'send-v2__asset-dropdown__asset'
|
||||||
: 'send-v2__asset-dropdown__single-asset'
|
: 'send-v2__asset-dropdown__single-asset'
|
||||||
}
|
}
|
||||||
@ -146,7 +181,7 @@ export default class SendAssetRow extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!insideDropdown && this.props.tokens.length > 0 && (
|
{!insideDropdown && this.state.sendableTokens.length > 0 && (
|
||||||
<i className="fa fa-caret-down fa-lg send-v2__asset-dropdown__caret" />
|
<i className="fa fa-caret-down fa-lg send-v2__asset-dropdown__caret" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,11 @@ import {
|
|||||||
getSendTokenAddress,
|
getSendTokenAddress,
|
||||||
getAssetImages,
|
getAssetImages,
|
||||||
} from '../../../../selectors';
|
} from '../../../../selectors';
|
||||||
import { updateSendToken } from '../../../../ducks/send/send.duck';
|
import { updateTokenType } from '../../../../store/actions';
|
||||||
|
import {
|
||||||
|
updateSendErrors,
|
||||||
|
updateSendToken,
|
||||||
|
} from '../../../../ducks/send/send.duck';
|
||||||
import SendAssetRow from './send-asset-row.component';
|
import SendAssetRow from './send-asset-row.component';
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
@ -24,6 +28,10 @@ function mapStateToProps(state) {
|
|||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
setSendToken: (token) => dispatch(updateSendToken(token)),
|
setSendToken: (token) => dispatch(updateSendToken(token)),
|
||||||
|
updateTokenType: (tokenAddress) => dispatch(updateTokenType(tokenAddress)),
|
||||||
|
updateSendErrors: (error) => {
|
||||||
|
dispatch(updateSendErrors(error));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
ETH_GAS_PRICE_FETCH_WARNING_KEY,
|
ETH_GAS_PRICE_FETCH_WARNING_KEY,
|
||||||
GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
|
GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
|
||||||
GAS_PRICE_EXCESSIVE_ERROR_KEY,
|
GAS_PRICE_EXCESSIVE_ERROR_KEY,
|
||||||
|
UNSENDABLE_ASSET_ERROR_KEY,
|
||||||
} from '../../../helpers/constants/error-keys';
|
} from '../../../helpers/constants/error-keys';
|
||||||
import SendAmountRow from './send-amount-row';
|
import SendAmountRow from './send-amount-row';
|
||||||
import SendGasRow from './send-gas-row';
|
import SendGasRow from './send-gas-row';
|
||||||
@ -17,6 +18,10 @@ export default class SendContent extends Component {
|
|||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
unsendableAssetError: false,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
updateGas: PropTypes.func,
|
updateGas: PropTypes.func,
|
||||||
showAddToAddressBookModal: PropTypes.func,
|
showAddToAddressBookModal: PropTypes.func,
|
||||||
@ -32,6 +37,9 @@ export default class SendContent extends Component {
|
|||||||
|
|
||||||
updateGas = (updateData) => this.props.updateGas(updateData);
|
updateGas = (updateData) => this.props.updateGas(updateData);
|
||||||
|
|
||||||
|
setUnsendableAssetError = (unsendableAssetError) =>
|
||||||
|
this.setState({ unsendableAssetError });
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
warning,
|
warning,
|
||||||
@ -41,6 +49,7 @@ export default class SendContent extends Component {
|
|||||||
noGasPrice,
|
noGasPrice,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const { unsendableAssetError } = this.state;
|
||||||
let gasError;
|
let gasError;
|
||||||
if (gasIsExcessive) gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY;
|
if (gasIsExcessive) gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY;
|
||||||
else if (noGasPrice) gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY;
|
else if (noGasPrice) gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY;
|
||||||
@ -50,10 +59,13 @@ export default class SendContent extends Component {
|
|||||||
<div className="send-v2__form">
|
<div className="send-v2__form">
|
||||||
{gasError && this.renderError(gasError)}
|
{gasError && this.renderError(gasError)}
|
||||||
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)}
|
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)}
|
||||||
{error && this.renderError()}
|
{unsendableAssetError && this.renderError(UNSENDABLE_ASSET_ERROR_KEY)}
|
||||||
|
{error && this.renderError(error)}
|
||||||
{warning && this.renderWarning()}
|
{warning && this.renderWarning()}
|
||||||
{this.maybeRenderAddContact()}
|
{this.maybeRenderAddContact()}
|
||||||
<SendAssetRow />
|
<SendAssetRow
|
||||||
|
setUnsendableAssetError={this.setUnsendableAssetError}
|
||||||
|
/>
|
||||||
<SendAmountRow updateGas={this.updateGas} />
|
<SendAmountRow updateGas={this.updateGas} />
|
||||||
<SendGasRow />
|
<SendGasRow />
|
||||||
{this.props.showHexData && (
|
{this.props.showHexData && (
|
||||||
@ -97,12 +109,11 @@ export default class SendContent extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError(gasError = '') {
|
renderError(error) {
|
||||||
const { t } = this.context;
|
const { t } = this.context;
|
||||||
const { error } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Dialog type="error" className="send__error-dialog">
|
<Dialog type="error" className="send__error-dialog">
|
||||||
{gasError === '' ? t(error) : t(gasError)}
|
{t(error)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -223,7 +223,10 @@ export const transactionFeeSelector = function (state, txData) {
|
|||||||
|
|
||||||
// if the gas price from our infura endpoint is null or undefined
|
// if the gas price from our infura endpoint is null or undefined
|
||||||
// use the metaswap average price estimation as a fallback
|
// use the metaswap average price estimation as a fallback
|
||||||
let { txParams: { gasPrice } = {} } = txData;
|
let {
|
||||||
|
txParams: { gasPrice },
|
||||||
|
} = txData;
|
||||||
|
|
||||||
if (!gasPrice) {
|
if (!gasPrice) {
|
||||||
gasPrice = getAveragePriceEstimateInHexWEI(state) || '0x0';
|
gasPrice = getAveragePriceEstimateInHexWEI(state) || '0x0';
|
||||||
}
|
}
|
||||||
|
@ -1234,6 +1234,21 @@ export function addToken(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateTokenType(tokenAddress) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
let token = {};
|
||||||
|
dispatch(showLoadingIndication());
|
||||||
|
try {
|
||||||
|
token = await promisifiedBackground.updateTokenType(tokenAddress);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
} finally {
|
||||||
|
dispatch(hideLoadingIndication());
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function removeToken(address) {
|
export function removeToken(address) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(showLoadingIndication());
|
dispatch(showLoadingIndication());
|
||||||
|
Loading…
Reference in New Issue
Block a user