mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
parent
0d1e79dda5
commit
4826c8c95e
@ -2531,6 +2531,9 @@
|
||||
"sendTokens": {
|
||||
"message": "Send Tokens"
|
||||
},
|
||||
"sendingDisabled": {
|
||||
"message": "Sending of ERC-1155 NFT assets is not yet supported."
|
||||
},
|
||||
"sendingNativeAsset": {
|
||||
"message": "Sending $1",
|
||||
"description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)"
|
||||
@ -3154,6 +3157,9 @@
|
||||
"tokenDetectionAnnouncement": {
|
||||
"message": "New! Improved token detection is available on Ethereum Mainnet as an experimental feature. $1"
|
||||
},
|
||||
"tokenId": {
|
||||
"message": "Token ID"
|
||||
},
|
||||
"tokenSymbol": {
|
||||
"message": "Token Symbol"
|
||||
},
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
} from '../../../../shared/constants/network';
|
||||
import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils';
|
||||
import { readAddressAsContract } from '../../../../shared/modules/contract-utils';
|
||||
import { isEqualCaseInsensitive } from '../../../../ui/helpers/utils/util';
|
||||
import TransactionStateManager from './tx-state-manager';
|
||||
import TxGasUtil from './tx-gas-utils';
|
||||
import PendingTransactionTracker from './pending-tx-tracker';
|
||||
@ -1287,7 +1288,7 @@ export default class TransactionController extends EventEmitter {
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
|
||||
].find((methodName) => methodName === name && name.toLowerCase());
|
||||
].find((methodName) => isEqualCaseInsensitive(methodName, name));
|
||||
|
||||
let result;
|
||||
if (data && tokenMethodName) {
|
||||
|
@ -40,7 +40,10 @@ import {
|
||||
SubjectMetadataController,
|
||||
} from '@metamask/snap-controllers';
|
||||
|
||||
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
|
||||
import {
|
||||
TRANSACTION_STATUSES,
|
||||
TRANSACTION_TYPES,
|
||||
} from '../../shared/constants/transaction';
|
||||
import {
|
||||
GAS_API_BASE_URL,
|
||||
GAS_DEV_API_BASE_URL,
|
||||
@ -64,6 +67,9 @@ import {
|
||||
} from '../../shared/constants/app';
|
||||
|
||||
import { hexToDecimal } from '../../ui/helpers/utils/conversions.util';
|
||||
import { getTokenValueParam } from '../../ui/helpers/utils/token-util';
|
||||
import { getTransactionData } from '../../ui/helpers/utils/transactions.util';
|
||||
import { isEqualCaseInsensitive } from '../../ui/helpers/utils/util';
|
||||
import ComposableObservableStore from './lib/ComposableObservableStore';
|
||||
import AccountTracker from './lib/account-tracker';
|
||||
import createLoggerMiddleware from './lib/createLoggerMiddleware';
|
||||
@ -599,6 +605,43 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.platform.showTransactionNotification(txMeta, rpcPrefs);
|
||||
|
||||
const { txReceipt } = txMeta;
|
||||
|
||||
// if this is a transferFrom method generated from within the app it may be a collectible transfer transaction
|
||||
// in which case we will want to check and update ownership status of the transferred collectible.
|
||||
if (
|
||||
txMeta.type === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM &&
|
||||
txMeta.txParams !== undefined
|
||||
) {
|
||||
const {
|
||||
data,
|
||||
to: contractAddress,
|
||||
from: userAddress,
|
||||
} = txMeta.txParams;
|
||||
const { chainId } = txMeta;
|
||||
const transactionData = getTransactionData(data);
|
||||
const tokenAmountOrTokenId = getTokenValueParam(transactionData);
|
||||
const { allCollectibles } = this.collectiblesController.state;
|
||||
|
||||
// check if its a known collectible
|
||||
const knownCollectible = allCollectibles?.[userAddress]?.[
|
||||
chainId
|
||||
].find(
|
||||
({ address, tokenId }) =>
|
||||
isEqualCaseInsensitive(address, contractAddress) &&
|
||||
tokenId === tokenAmountOrTokenId,
|
||||
);
|
||||
|
||||
// if it is we check and update ownership status.
|
||||
if (knownCollectible) {
|
||||
this.collectiblesController.checkAndUpdateSingleCollectibleOwnershipStatus(
|
||||
knownCollectible,
|
||||
false,
|
||||
// TODO add this when checkAndUpdateSingleCollectibleOwnershipStatus is updated
|
||||
// { userAddress, chainId },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const metamaskState = await this.getState();
|
||||
|
||||
if (txReceipt && txReceipt.status === '0x0') {
|
||||
@ -1163,7 +1206,15 @@ export default class MetamaskController extends EventEmitter {
|
||||
collectiblesController,
|
||||
),
|
||||
|
||||
checkAndUpdateCollectiblesOwnershipStatus: collectiblesController.checkAndUpdateCollectiblesOwnershipStatus.bind(
|
||||
checkAndUpdateAllCollectiblesOwnershipStatus: collectiblesController.checkAndUpdateAllCollectiblesOwnershipStatus.bind(
|
||||
collectiblesController,
|
||||
),
|
||||
|
||||
checkAndUpdateSingleCollectibleOwnershipStatus: collectiblesController.checkAndUpdateSingleCollectibleOwnershipStatus.bind(
|
||||
collectiblesController,
|
||||
),
|
||||
|
||||
isCollectibleOwner: collectiblesController.isCollectibleOwner.bind(
|
||||
collectiblesController,
|
||||
),
|
||||
|
||||
|
@ -547,6 +547,7 @@
|
||||
"@ethereumjs/common": true,
|
||||
"@ethereumjs/tx": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"buffer": true,
|
||||
@ -565,7 +566,6 @@
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
"human-standard-collectible-abi": true,
|
||||
"human-standard-multi-collectible-abi": true,
|
||||
"human-standard-token-abi": true,
|
||||
"immer": true,
|
||||
"isomorphic-fetch": true,
|
||||
|
@ -547,6 +547,7 @@
|
||||
"@ethereumjs/common": true,
|
||||
"@ethereumjs/tx": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"buffer": true,
|
||||
@ -565,7 +566,6 @@
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
"human-standard-collectible-abi": true,
|
||||
"human-standard-multi-collectible-abi": true,
|
||||
"human-standard-token-abi": true,
|
||||
"immer": true,
|
||||
"isomorphic-fetch": true,
|
||||
|
@ -547,6 +547,7 @@
|
||||
"@ethereumjs/common": true,
|
||||
"@ethereumjs/tx": true,
|
||||
"@metamask/contract-metadata": true,
|
||||
"@metamask/metamask-eth-abis": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"buffer": true,
|
||||
@ -565,7 +566,6 @@
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
"human-standard-collectible-abi": true,
|
||||
"human-standard-multi-collectible-abi": true,
|
||||
"human-standard-token-abi": true,
|
||||
"immer": true,
|
||||
"isomorphic-fetch": true,
|
||||
|
@ -1052,6 +1052,16 @@
|
||||
"buffer-equal": true
|
||||
}
|
||||
},
|
||||
"are-we-there-yet": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"util.inherits": true
|
||||
},
|
||||
"packages": {
|
||||
"delegates": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"arr-diff": {
|
||||
"packages": {
|
||||
"arr-flatten": true,
|
||||
@ -1460,6 +1470,7 @@
|
||||
"anymatch": true,
|
||||
"async-each": true,
|
||||
"braces": true,
|
||||
"fsevents": true,
|
||||
"glob-parent": true,
|
||||
"inherits": true,
|
||||
"is-binary-path": true,
|
||||
@ -1726,6 +1737,16 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"detect-libc": {
|
||||
"builtin": {
|
||||
"child_process.spawnSync": true,
|
||||
"fs.readdirSync": true,
|
||||
"os.platform": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env": true
|
||||
}
|
||||
},
|
||||
"detective": {
|
||||
"packages": {
|
||||
"acorn-node": true,
|
||||
@ -2429,6 +2450,45 @@
|
||||
"process.version": true
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"fs.stat": true,
|
||||
"path.join": true,
|
||||
"util.inherits": true
|
||||
},
|
||||
"globals": {
|
||||
"__dirname": true,
|
||||
"process.nextTick": true,
|
||||
"process.platform": true,
|
||||
"setImmediate": true
|
||||
},
|
||||
"native": true,
|
||||
"packages": {
|
||||
"node-pre-gyp": true
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"builtin": {
|
||||
"util.format": true
|
||||
},
|
||||
"globals": {
|
||||
"clearInterval": true,
|
||||
"process": true,
|
||||
"setImmediate": true,
|
||||
"setInterval": true
|
||||
},
|
||||
"packages": {
|
||||
"aproba": true,
|
||||
"console-control-strings": true,
|
||||
"has-unicode": true,
|
||||
"object-assign": true,
|
||||
"signal-exit": true,
|
||||
"string-width": true,
|
||||
"strip-ansi": true,
|
||||
"wide-align": true
|
||||
}
|
||||
},
|
||||
"get-assigned-identifiers": {
|
||||
"builtin": {
|
||||
"assert.equal": true
|
||||
@ -2807,6 +2867,16 @@
|
||||
"process.argv": true
|
||||
}
|
||||
},
|
||||
"has-unicode": {
|
||||
"builtin": {
|
||||
"os.type": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env.LANG": true,
|
||||
"process.env.LC_ALL": true,
|
||||
"process.env.LC_CTYPE": true
|
||||
}
|
||||
},
|
||||
"has-value": {
|
||||
"packages": {
|
||||
"get-value": true,
|
||||
@ -2978,6 +3048,11 @@
|
||||
"is-plain-object": true
|
||||
}
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"packages": {
|
||||
"number-is-nan": true
|
||||
}
|
||||
},
|
||||
"is-glob": {
|
||||
"packages": {
|
||||
"is-extglob": true
|
||||
@ -3508,6 +3583,56 @@
|
||||
"setTimeout": true
|
||||
}
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"fs.existsSync": true,
|
||||
"fs.readFileSync": true,
|
||||
"fs.renameSync": true,
|
||||
"path.dirname": true,
|
||||
"path.existsSync": true,
|
||||
"path.join": true,
|
||||
"path.resolve": true,
|
||||
"url.parse": true,
|
||||
"url.resolve": true,
|
||||
"util.inherits": true
|
||||
},
|
||||
"globals": {
|
||||
"__dirname": true,
|
||||
"console.log": true,
|
||||
"process.arch": true,
|
||||
"process.cwd": true,
|
||||
"process.env": true,
|
||||
"process.platform": true,
|
||||
"process.version.substr": true,
|
||||
"process.versions": true
|
||||
},
|
||||
"packages": {
|
||||
"detect-libc": true,
|
||||
"nopt": true,
|
||||
"npmlog": true,
|
||||
"rimraf": true,
|
||||
"semver": true
|
||||
}
|
||||
},
|
||||
"nopt": {
|
||||
"builtin": {
|
||||
"path": true,
|
||||
"stream.Stream": true,
|
||||
"url": true
|
||||
},
|
||||
"globals": {
|
||||
"console": true,
|
||||
"process.argv": true,
|
||||
"process.env.DEBUG_NOPT": true,
|
||||
"process.env.NOPT_DEBUG": true,
|
||||
"process.platform": true
|
||||
},
|
||||
"packages": {
|
||||
"abbrev": true,
|
||||
"osenv": true
|
||||
}
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"builtin": {
|
||||
"url.parse": true,
|
||||
@ -3535,6 +3660,22 @@
|
||||
"once": true
|
||||
}
|
||||
},
|
||||
"npmlog": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"util": true
|
||||
},
|
||||
"globals": {
|
||||
"process.nextTick": true,
|
||||
"process.stderr": true
|
||||
},
|
||||
"packages": {
|
||||
"are-we-there-yet": true,
|
||||
"console-control-strings": true,
|
||||
"gauge": true,
|
||||
"set-blocking": true
|
||||
}
|
||||
},
|
||||
"object-copy": {
|
||||
"packages": {
|
||||
"copy-descriptor": true,
|
||||
@ -3616,6 +3757,54 @@
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"os-homedir": {
|
||||
"builtin": {
|
||||
"os.homedir": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env": true,
|
||||
"process.getuid": true,
|
||||
"process.platform": true
|
||||
}
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"globals": {
|
||||
"process.env.SystemRoot": true,
|
||||
"process.env.TEMP": true,
|
||||
"process.env.TMP": true,
|
||||
"process.env.TMPDIR": true,
|
||||
"process.env.windir": true,
|
||||
"process.platform": true
|
||||
}
|
||||
},
|
||||
"osenv": {
|
||||
"builtin": {
|
||||
"child_process.exec": true,
|
||||
"path": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env.COMPUTERNAME": true,
|
||||
"process.env.ComSpec": true,
|
||||
"process.env.EDITOR": true,
|
||||
"process.env.HOSTNAME": true,
|
||||
"process.env.PATH": true,
|
||||
"process.env.PROMPT": true,
|
||||
"process.env.PS1": true,
|
||||
"process.env.Path": true,
|
||||
"process.env.SHELL": true,
|
||||
"process.env.USER": true,
|
||||
"process.env.USERDOMAIN": true,
|
||||
"process.env.USERNAME": true,
|
||||
"process.env.VISUAL": true,
|
||||
"process.env.path": true,
|
||||
"process.nextTick": true,
|
||||
"process.platform": true
|
||||
},
|
||||
"packages": {
|
||||
"os-homedir": true,
|
||||
"os-tmpdir": true
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"packages": {
|
||||
"p-try": true
|
||||
@ -4325,6 +4514,12 @@
|
||||
"lru-cache": true
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"globals": {
|
||||
"process.stderr": true,
|
||||
"process.stdout": true
|
||||
}
|
||||
},
|
||||
"set-value": {
|
||||
"packages": {
|
||||
"extend-shallow": true,
|
||||
@ -4588,6 +4783,7 @@
|
||||
},
|
||||
"string-width": {
|
||||
"packages": {
|
||||
"code-point-at": true,
|
||||
"emoji-regex": true,
|
||||
"is-fullwidth-code-point": true,
|
||||
"strip-ansi": true
|
||||
@ -5240,6 +5436,11 @@
|
||||
"isexe": true
|
||||
}
|
||||
},
|
||||
"wide-align": {
|
||||
"packages": {
|
||||
"string-width": true
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"builtin": {
|
||||
"fs.createWriteStream": true,
|
||||
|
@ -107,7 +107,7 @@
|
||||
"@keystonehq/metamask-airgapped-keyring": "0.2.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@metamask/contract-metadata": "^1.31.0",
|
||||
"@metamask/controllers": "^23.0.0",
|
||||
"@metamask/controllers": "^24.0.0",
|
||||
"@metamask/eth-ledger-bridge-keyring": "^0.10.0",
|
||||
"@metamask/eth-token-tracker": "^3.0.1",
|
||||
"@metamask/etherscan-link": "^2.1.0",
|
||||
@ -166,6 +166,7 @@
|
||||
"fast-safe-stringify": "^2.0.7",
|
||||
"fuse.js": "^3.2.0",
|
||||
"globalthis": "^1.0.1",
|
||||
"human-standard-collectible-abi": "^1.0.2",
|
||||
"human-standard-token-abi": "^2.0.0",
|
||||
"immer": "^9.0.6",
|
||||
"json-rpc-engine": "^6.1.0",
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getTokenTrackerLink } from '@metamask/etherscan-link';
|
||||
import { isEqual } from 'lodash';
|
||||
import Box from '../../ui/box';
|
||||
import Card from '../../ui/card';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
@ -14,6 +15,7 @@ import {
|
||||
FLEX_DIRECTION,
|
||||
OVERFLOW_WRAP,
|
||||
DISPLAY,
|
||||
BLOCK_SIZES,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import {
|
||||
@ -29,8 +31,11 @@ import {
|
||||
} from '../../../selectors';
|
||||
import AssetNavigation from '../../../pages/asset/components/asset-navigation';
|
||||
import { getCollectibleContracts } from '../../../ducks/metamask/metamask';
|
||||
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { removeAndIgnoreCollectible } from '../../../store/actions';
|
||||
import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes';
|
||||
import {
|
||||
checkAndUpdateSingleCollectibleOwnershipStatus,
|
||||
removeAndIgnoreCollectible,
|
||||
} from '../../../store/actions';
|
||||
import {
|
||||
GOERLI_CHAIN_ID,
|
||||
KOVAN_CHAIN_ID,
|
||||
@ -42,11 +47,25 @@ import {
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||
import CollectibleOptions from '../collectible-options/collectible-options';
|
||||
import Button from '../../ui/button';
|
||||
import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send';
|
||||
import InfoTooltip from '../../ui/info-tooltip';
|
||||
import { ERC721 } from '../../../helpers/constants/common';
|
||||
import { usePrevious } from '../../../hooks/usePrevious';
|
||||
|
||||
export default function CollectibleDetails({ collectible }) {
|
||||
const { image, name, description, address, tokenId } = collectible;
|
||||
const {
|
||||
image,
|
||||
name,
|
||||
description,
|
||||
address,
|
||||
tokenId,
|
||||
standard,
|
||||
isCurrentlyOwned,
|
||||
} = collectible;
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
|
||||
const ipfsGateway = useSelector(getIpfsGateway);
|
||||
const collectibleContracts = useSelector(getCollectibleContracts);
|
||||
@ -60,13 +79,19 @@ export default function CollectibleDetails({ collectible }) {
|
||||
(state) => getSelectedIdentity(state).name,
|
||||
);
|
||||
const collectibleImageURL = getAssetImageURL(image, ipfsGateway);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onRemove = () => {
|
||||
dispatch(removeAndIgnoreCollectible(address, tokenId));
|
||||
history.push(DEFAULT_ROUTE);
|
||||
};
|
||||
|
||||
const prevCollectible = usePrevious(collectible);
|
||||
useEffect(() => {
|
||||
if (!isEqual(prevCollectible, collectible)) {
|
||||
checkAndUpdateSingleCollectibleOwnershipStatus(collectible);
|
||||
}
|
||||
}, [collectible, prevCollectible]);
|
||||
|
||||
const getOpenSeaLink = () => {
|
||||
switch (currentNetwork) {
|
||||
case MAINNET_CHAIN_ID:
|
||||
@ -84,6 +109,43 @@ export default function CollectibleDetails({ collectible }) {
|
||||
};
|
||||
|
||||
const openSeaLink = getOpenSeaLink();
|
||||
const sendDisabled = standard !== ERC721;
|
||||
const inPopUp = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP;
|
||||
|
||||
const onSend = async () => {
|
||||
await dispatch(
|
||||
updateSendAsset({
|
||||
type: ASSET_TYPES.COLLECTIBLE,
|
||||
details: collectible,
|
||||
}),
|
||||
);
|
||||
history.push(SEND_ROUTE);
|
||||
};
|
||||
|
||||
const renderSendButton = () => {
|
||||
if (isCurrentlyOwned === false) {
|
||||
return <div style={{ height: '30px' }} />;
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
width={inPopUp ? BLOCK_SIZES.FULL : BLOCK_SIZES.HALF}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onSend}
|
||||
disabled={sendDisabled}
|
||||
className="collectible-details__send-button"
|
||||
>
|
||||
{t('send')}
|
||||
</Button>
|
||||
{sendDisabled ? (
|
||||
<InfoTooltip position="top" contentText={t('sendingDisabled')} />
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AssetNavigation
|
||||
@ -115,43 +177,49 @@ export default function CollectibleDetails({ collectible }) {
|
||||
</Card>
|
||||
<Box
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
className="collectible-details__top-section__info"
|
||||
className="collectible-details__info"
|
||||
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
|
||||
>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H5}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
>
|
||||
{`#${tokenId}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
className="collectible-details__description"
|
||||
boxProps={{ margin: 0, marginBottom: 2 }}
|
||||
>
|
||||
{t('description')}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.UI4}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
boxProps={{ margin: 0 }}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H5}
|
||||
boxProps={{ margin: 0 }}
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
>
|
||||
#{tokenId}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
className="collectible-details__description"
|
||||
boxProps={{ margin: 0, marginBottom: 2 }}
|
||||
>
|
||||
{t('description')}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.UI4}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</div>
|
||||
{inPopUp ? null : renderSendButton()}
|
||||
</Box>
|
||||
</div>
|
||||
<Box>
|
||||
<Box marginBottom={2}>
|
||||
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
@ -226,6 +294,7 @@ export default function CollectibleDetails({ collectible }) {
|
||||
</a>
|
||||
</Typography>
|
||||
</Box>
|
||||
{inPopUp ? renderSendButton() : null}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
@ -236,6 +305,7 @@ CollectibleDetails.propTypes = {
|
||||
collectible: PropTypes.shape({
|
||||
address: PropTypes.string.isRequired,
|
||||
tokenId: PropTypes.string.isRequired,
|
||||
isCurrentlyOwned: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
|
@ -19,12 +19,15 @@ $spacer-break-small: 16px;
|
||||
margin-bottom: $spacer-break-large;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
@media screen and (min-width: $break-large) {
|
||||
max-width: calc(100% - #{$card-width-break-large} - #{$spacer-break-large});
|
||||
flex: 0 0 calc(100% - #{$card-width-break-large} - #{$spacer-break-large});
|
||||
}
|
||||
&__info {
|
||||
@media screen and (min-width: $break-large) {
|
||||
max-width:
|
||||
calc(
|
||||
100% - #{$card-width-break-large} - #{$spacer-break-large}
|
||||
);
|
||||
flex: 0 0 calc(100% - #{$card-width-break-large} - #{$spacer-break-large});
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,4 +69,12 @@ $spacer-break-small: 16px;
|
||||
flex: 0 0 $link-title-width;
|
||||
max-width: 0 0 $link-title-width;
|
||||
}
|
||||
|
||||
&__send-button {
|
||||
margin-inline-end: 8px;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,13 +31,20 @@ export default function CollectiblesItems({
|
||||
collections = {},
|
||||
previouslyOwnedCollection = {},
|
||||
}) {
|
||||
const defaultDropdownState = { [PREVIOUSLY_OWNED_KEY]: false };
|
||||
const [dropdownState, setDropdownState] = useState(defaultDropdownState);
|
||||
const collectionsKeys = Object.keys(collections);
|
||||
|
||||
// if there is only one collection present set it to open when component mounts
|
||||
const [dropdownState, setDropdownState] = useState(() => {
|
||||
return collectionsKeys.length === 1
|
||||
? {
|
||||
[PREVIOUSLY_OWNED_KEY]: false,
|
||||
[collectionsKeys[0]]: true,
|
||||
}
|
||||
: { [PREVIOUSLY_OWNED_KEY]: false };
|
||||
});
|
||||
|
||||
const ipfsGateway = useSelector(getIpfsGateway);
|
||||
|
||||
Object.keys(collections).forEach((key) => {
|
||||
defaultDropdownState[key] = true;
|
||||
});
|
||||
const history = useHistory();
|
||||
|
||||
const renderCollectionImage = (
|
||||
@ -152,7 +159,7 @@ export default function CollectiblesItems({
|
||||
<div className="collectibles-items">
|
||||
<Box padding={[6, 4]} flexDirection={FLEX_DIRECTION.COLUMN}>
|
||||
<>
|
||||
{Object.keys(collections).map((key) => {
|
||||
{collectionsKeys.map((key) => {
|
||||
const {
|
||||
collectibles,
|
||||
collectionName,
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isEqual } from 'lodash';
|
||||
import Box from '../../ui/box';
|
||||
import Button from '../../ui/button';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
@ -25,9 +26,10 @@ import {
|
||||
import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors';
|
||||
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
|
||||
import {
|
||||
checkAndUpdateCollectiblesOwnershipStatus,
|
||||
checkAndUpdateAllCollectiblesOwnershipStatus,
|
||||
detectCollectibles,
|
||||
} from '../../../store/actions';
|
||||
import { usePrevious } from '../../../hooks/usePrevious';
|
||||
|
||||
export default function CollectiblesTab({ onAddNFT }) {
|
||||
const collectibles = useSelector(getCollectibles);
|
||||
@ -40,34 +42,46 @@ export default function CollectiblesTab({ onAddNFT }) {
|
||||
const history = useHistory();
|
||||
const t = useI18nContext();
|
||||
const dispatch = useDispatch();
|
||||
const [collections, setCollections] = useState({});
|
||||
const [previouslyOwnedCollection, setPreviouslyOwnedCollection] = useState({
|
||||
collectionName: 'Previously Owned',
|
||||
collectibles: [],
|
||||
});
|
||||
|
||||
const getCollections = () => {
|
||||
const collections = {};
|
||||
const previouslyOwnedCollection = {
|
||||
collectionName: 'Previously Owned',
|
||||
collectibles: [],
|
||||
const prevCollectibles = usePrevious(collectibles);
|
||||
useEffect(() => {
|
||||
const getCollections = () => {
|
||||
const newCollections = {};
|
||||
const newPreviouslyOwnedCollections = {
|
||||
collectionName: 'Previously Owned',
|
||||
collectibles: [],
|
||||
};
|
||||
|
||||
collectibles.forEach((collectible) => {
|
||||
if (collectible?.isCurrentlyOwned === false) {
|
||||
newPreviouslyOwnedCollections.collectibles.push(collectible);
|
||||
} else if (newCollections[collectible.address]) {
|
||||
newCollections[collectible.address].collectibles.push(collectible);
|
||||
} else {
|
||||
const collectionContract = collectibleContracts.find(
|
||||
({ address }) => address === collectible.address,
|
||||
);
|
||||
newCollections[collectible.address] = {
|
||||
collectionName: collectionContract?.name || collectible.name,
|
||||
collectionImage:
|
||||
collectionContract?.logo || collectible.collectionImage,
|
||||
collectibles: [collectible],
|
||||
};
|
||||
}
|
||||
});
|
||||
setCollections(newCollections);
|
||||
setPreviouslyOwnedCollection(newPreviouslyOwnedCollections);
|
||||
};
|
||||
collectibles.forEach((collectible) => {
|
||||
if (collectible?.isCurrentlyOwned === false) {
|
||||
previouslyOwnedCollection.collectibles.push(collectible);
|
||||
} else if (collections[collectible.address]) {
|
||||
collections[collectible.address].collectibles.push(collectible);
|
||||
} else {
|
||||
const collectionContract = collectibleContracts.find(
|
||||
({ address }) => address === collectible.address,
|
||||
);
|
||||
collections[collectible.address] = {
|
||||
collectionName: collectionContract?.name || collectible.name,
|
||||
collectionImage:
|
||||
collectionContract?.logo || collectible.collectionImage,
|
||||
collectibles: [collectible],
|
||||
};
|
||||
}
|
||||
});
|
||||
return [collections, previouslyOwnedCollection];
|
||||
};
|
||||
|
||||
const [collections, previouslyOwnedCollection] = getCollections();
|
||||
if (!isEqual(prevCollectibles, collectibles)) {
|
||||
getCollections();
|
||||
}
|
||||
}, [collectibles, prevCollectibles, collectibleContracts]);
|
||||
|
||||
const onEnableAutoDetect = () => {
|
||||
history.push(EXPERIMENTAL_ROUTE);
|
||||
@ -77,12 +91,13 @@ export default function CollectiblesTab({ onAddNFT }) {
|
||||
if (isMainnet) {
|
||||
dispatch(detectCollectibles());
|
||||
}
|
||||
checkAndUpdateCollectiblesOwnershipStatus();
|
||||
checkAndUpdateAllCollectiblesOwnershipStatus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="collectibles-tab">
|
||||
{collectibles.length > 0 ? (
|
||||
{Object.keys(collections).length > 0 ||
|
||||
previouslyOwnedCollection.collectibles.length > 0 ? (
|
||||
<CollectiblesItems
|
||||
collections={collections}
|
||||
previouslyOwnedCollection={previouslyOwnedCollection}
|
||||
|
@ -173,12 +173,12 @@ describe('Collectible Items', () => {
|
||||
const detectCollectiblesStub = jest.fn();
|
||||
const setCollectiblesDetectionNoticeDismissedStub = jest.fn();
|
||||
const getStateStub = jest.fn();
|
||||
const checkAndUpdateCollectiblesOwnershipStatusStub = jest.fn();
|
||||
const checkAndUpdateAllCollectiblesOwnershipStatusStub = jest.fn();
|
||||
setBackgroundConnection({
|
||||
setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub,
|
||||
detectCollectibles: detectCollectiblesStub,
|
||||
getState: getStateStub,
|
||||
checkAndUpdateCollectiblesOwnershipStatus: checkAndUpdateCollectiblesOwnershipStatusStub,
|
||||
checkAndUpdateAllCollectiblesOwnershipStatus: checkAndUpdateAllCollectiblesOwnershipStatusStub,
|
||||
});
|
||||
const historyPushMock = jest.fn();
|
||||
|
||||
@ -276,11 +276,13 @@ describe('Collectible Items', () => {
|
||||
});
|
||||
expect(detectCollectiblesStub).not.toHaveBeenCalled();
|
||||
expect(
|
||||
checkAndUpdateCollectiblesOwnershipStatusStub,
|
||||
checkAndUpdateAllCollectiblesOwnershipStatusStub,
|
||||
).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.queryByText('Refresh list'));
|
||||
expect(detectCollectiblesStub).toHaveBeenCalled();
|
||||
expect(checkAndUpdateCollectiblesOwnershipStatusStub).toHaveBeenCalled();
|
||||
expect(
|
||||
checkAndUpdateAllCollectiblesOwnershipStatusStub,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render a link "Refresh list" when some collectibles are present on a non-mainnet chain, which, when clicked calls a method checkAndUpdateCollectiblesOwnershipStatus', () => {
|
||||
@ -291,10 +293,12 @@ describe('Collectible Items', () => {
|
||||
useCollectibleDetection: true,
|
||||
});
|
||||
expect(
|
||||
checkAndUpdateCollectiblesOwnershipStatusStub,
|
||||
checkAndUpdateAllCollectiblesOwnershipStatusStub,
|
||||
).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.queryByText('Refresh list'));
|
||||
expect(checkAndUpdateCollectiblesOwnershipStatusStub).toHaveBeenCalled();
|
||||
expect(
|
||||
checkAndUpdateAllCollectiblesOwnershipStatusStub,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render a link "Enable Autodetect" when some collectibles are present and collectible auto-detection preference is set to false, which, when clicked sends user to the experimental tab of settings', () => {
|
||||
|
@ -25,6 +25,7 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
nonce: PropTypes.string,
|
||||
subtitleComponent: PropTypes.node,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
image: PropTypes.string,
|
||||
titleComponent: PropTypes.node,
|
||||
warning: PropTypes.string,
|
||||
origin: PropTypes.string.isRequired,
|
||||
@ -87,6 +88,7 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
errorMessage,
|
||||
hasSimulationError,
|
||||
title,
|
||||
image,
|
||||
titleComponent,
|
||||
subtitleComponent,
|
||||
hideSubtitle,
|
||||
@ -140,6 +142,7 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
})}
|
||||
action={action}
|
||||
title={title}
|
||||
image={image}
|
||||
titleComponent={titleComponent}
|
||||
subtitleComponent={subtitleComponent}
|
||||
hideSubtitle={hideSubtitle}
|
||||
|
@ -3,7 +3,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Identicon from '../../../../ui/identicon';
|
||||
import { useGasFeeContext } from '../../../../../contexts/gasFee';
|
||||
|
||||
const ConfirmPageContainerSummary = (props) => {
|
||||
const {
|
||||
@ -17,9 +16,30 @@ const ConfirmPageContainerSummary = (props) => {
|
||||
nonce,
|
||||
origin,
|
||||
hideTitle,
|
||||
image,
|
||||
} = props;
|
||||
|
||||
const { supportsEIP1559V2 } = useGasFeeContext();
|
||||
const renderImage = () => {
|
||||
if (image) {
|
||||
return (
|
||||
<img
|
||||
className="confirm-page-container-summary__icon"
|
||||
width={36}
|
||||
src={image}
|
||||
/>
|
||||
);
|
||||
} else if (identiconAddress) {
|
||||
return (
|
||||
<Identicon
|
||||
className="confirm-page-container-summary__icon"
|
||||
diameter={36}
|
||||
address={identiconAddress}
|
||||
image={image}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classnames('confirm-page-container-summary', className)}>
|
||||
@ -34,25 +54,21 @@ const ConfirmPageContainerSummary = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="confirm-page-container-summary__title">
|
||||
{identiconAddress && (
|
||||
<Identicon
|
||||
className="confirm-page-container-summary__identicon"
|
||||
diameter={36}
|
||||
address={identiconAddress}
|
||||
/>
|
||||
)}
|
||||
{!hideTitle ? (
|
||||
<div className="confirm-page-container-summary__title-text">
|
||||
{titleComponent || title}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{!hideSubtitle && !supportsEIP1559V2 && (
|
||||
<div className="confirm-page-container-summary__subtitle">
|
||||
{subtitleComponent}
|
||||
<>
|
||||
<div className="confirm-page-container-summary__title">
|
||||
{renderImage()}
|
||||
{!hideTitle ? (
|
||||
<div className="confirm-page-container-summary__title-text">
|
||||
{titleComponent || title}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{hideSubtitle ? null : (
|
||||
<div className="confirm-page-container-summary__subtitle">
|
||||
{subtitleComponent}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -60,6 +76,7 @@ const ConfirmPageContainerSummary = (props) => {
|
||||
ConfirmPageContainerSummary.propTypes = {
|
||||
action: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
image: PropTypes.string,
|
||||
titleComponent: PropTypes.node,
|
||||
subtitleComponent: PropTypes.node,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
|
@ -1,8 +1,11 @@
|
||||
.confirm-page-container-summary {
|
||||
padding: 16px 24px 0;
|
||||
padding: 0 24px;
|
||||
background-color: #f9fafa;
|
||||
height: 133px;
|
||||
height: 120px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
&__origin {
|
||||
@include H6;
|
||||
@ -36,13 +39,13 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__identicon {
|
||||
&__icon {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&__title-text {
|
||||
@include H1;
|
||||
@include H2;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@ -50,10 +53,13 @@
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
@include H6;
|
||||
|
||||
color: var(--oslo-gray);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 42px;
|
||||
}
|
||||
|
||||
&--border {
|
||||
|
@ -39,6 +39,7 @@ export default class ConfirmPageContainer extends Component {
|
||||
showEdit: PropTypes.bool,
|
||||
subtitleComponent: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
titleComponent: PropTypes.node,
|
||||
hideSenderToRecipient: PropTypes.bool,
|
||||
showAccountInHeader: PropTypes.bool,
|
||||
@ -103,6 +104,7 @@ export default class ConfirmPageContainer extends Component {
|
||||
contentComponent,
|
||||
action,
|
||||
title,
|
||||
image,
|
||||
titleComponent,
|
||||
subtitleComponent,
|
||||
hideSubtitle,
|
||||
@ -206,6 +208,7 @@ export default class ConfirmPageContainer extends Component {
|
||||
<ConfirmPageContainerContent
|
||||
action={action}
|
||||
title={title}
|
||||
image={image}
|
||||
titleComponent={titleComponent}
|
||||
subtitleComponent={subtitleComponent}
|
||||
hideSubtitle={hideSubtitle}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
DISPLAY,
|
||||
FLEX_WRAP,
|
||||
ALIGN_ITEMS,
|
||||
TEXT_ALIGN,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
export default function TransactionDetailItem({
|
||||
@ -52,6 +53,7 @@ export default function TransactionDetailItem({
|
||||
fontWeight={boldHeadings ? FONT_WEIGHT.BOLD : FONT_WEIGHT.NORMAL}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
margin={[1, 0, 1, 1]}
|
||||
boxProps={{ textAlign: TEXT_ALIGN.RIGHT }}
|
||||
>
|
||||
{detailTotal}
|
||||
</Typography>
|
||||
|
@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Jazzicon from '../jazzicon';
|
||||
import { getAssetImageURL } from '../../../helpers/utils/util';
|
||||
import BlockieIdenticon from './blockieIdenticon';
|
||||
|
||||
const getStyles = (diameter) => ({
|
||||
@ -54,6 +55,10 @@ export default class Identicon extends PureComponent {
|
||||
* Add list of token in object
|
||||
*/
|
||||
tokenList: PropTypes.object,
|
||||
/**
|
||||
* User preferred IPFS gateway
|
||||
*/
|
||||
ipfsGateway: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -68,7 +73,12 @@ export default class Identicon extends PureComponent {
|
||||
};
|
||||
|
||||
renderImage() {
|
||||
const { className, diameter, image, alt, imageBorder } = this.props;
|
||||
const { className, diameter, alt, imageBorder, ipfsGateway } = this.props;
|
||||
let { image } = this.props;
|
||||
|
||||
if (image.toLowerCase().startsWith('ipfs://')) {
|
||||
image = getAssetImageURL(image, ipfsGateway);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
|
@ -3,13 +3,14 @@ import Identicon from './identicon.component';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {
|
||||
metamask: { useBlockie, useTokenDetection, tokenList },
|
||||
metamask: { useBlockie, useTokenDetection, tokenList, ipfsGateway },
|
||||
} = state;
|
||||
|
||||
return {
|
||||
useBlockie,
|
||||
useTokenDetection,
|
||||
tokenList,
|
||||
ipfsGateway,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,10 @@ import {
|
||||
addEth,
|
||||
} from '../../helpers/utils/confirm-tx.util';
|
||||
|
||||
import { getTokenData, sumHexes } from '../../helpers/utils/transactions.util';
|
||||
import {
|
||||
getTransactionData,
|
||||
sumHexes,
|
||||
} from '../../helpers/utils/transactions.util';
|
||||
|
||||
import { conversionUtil } from '../../../shared/modules/conversion.utils';
|
||||
import { getAveragePriceEstimateInHexWEI } from '../../selectors/custom-gas';
|
||||
@ -282,7 +285,7 @@ export function setTransactionToConfirm(transactionId) {
|
||||
if (txParams.data) {
|
||||
const { to: tokenAddress, data } = txParams;
|
||||
|
||||
const tokenData = getTokenData(data);
|
||||
const tokenData = getTransactionData(data);
|
||||
const tokens = getTokens(state);
|
||||
const currentToken = tokens?.find(({ address }) =>
|
||||
isEqualCaseInsensitive(tokenAddress, address),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
import abi from 'human-standard-token-abi';
|
||||
import abiERC721 from 'human-standard-collectible-abi';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { addHexPrefix } from 'ethereumjs-util';
|
||||
import { debounce } from 'lodash';
|
||||
@ -24,7 +25,9 @@ import {
|
||||
import {
|
||||
addGasBuffer,
|
||||
calcGasTotal,
|
||||
generateTokenTransferData,
|
||||
generateERC20TransferData,
|
||||
generateERC721TransferData,
|
||||
getAssetTransferData,
|
||||
isBalanceSufficient,
|
||||
isTokenBalanceSufficient,
|
||||
} from '../../pages/send/send.utils';
|
||||
@ -54,6 +57,7 @@ import {
|
||||
updateTransaction,
|
||||
addPollingTokenToAppState,
|
||||
removePollingTokenFromAppState,
|
||||
isCollectibleOwner,
|
||||
} from '../../store/actions';
|
||||
import { setCustomGasLimit } from '../gas/gas.duck';
|
||||
import {
|
||||
@ -87,7 +91,7 @@ import {
|
||||
isValidHexAddress,
|
||||
} from '../../../shared/modules/hexstring-utils';
|
||||
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
|
||||
import { ETH, GWEI } from '../../helpers/constants/common';
|
||||
import { ERC20, ETH, GWEI } from '../../helpers/constants/common';
|
||||
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
|
||||
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
|
||||
// typedefs
|
||||
@ -155,10 +159,12 @@ export const GAS_INPUT_MODES = {
|
||||
* The types of assets that a user can send
|
||||
* 1. NATIVE - The native asset for the current network, such as ETH
|
||||
* 2. TOKEN - An ERC20 token.
|
||||
* 2. COLLECTIBLE - An ERC721 or ERC1155 token.
|
||||
*/
|
||||
export const ASSET_TYPES = {
|
||||
NATIVE: 'NATIVE',
|
||||
TOKEN: 'TOKEN',
|
||||
COLLECTIBLE: 'COLLECTIBLE',
|
||||
};
|
||||
|
||||
/**
|
||||
@ -218,13 +224,16 @@ async function estimateGasLimitForSend({
|
||||
return GAS_LIMITS.BASE_TOKEN_ESTIMATE;
|
||||
}
|
||||
paramsForGasEstimate.value = '0x0';
|
||||
// We have to generate the erc20 contract call to transfer tokens in
|
||||
|
||||
// We have to generate the erc20/erc721 contract call to transfer tokens in
|
||||
// order to get a proper estimate for gasLimit.
|
||||
paramsForGasEstimate.data = generateTokenTransferData({
|
||||
paramsForGasEstimate.data = getAssetTransferData({
|
||||
sendToken,
|
||||
fromAddress: selectedAddress,
|
||||
toAddress: to,
|
||||
amount: value,
|
||||
sendToken,
|
||||
});
|
||||
|
||||
paramsForGasEstimate.to = sendToken.address;
|
||||
} else {
|
||||
if (!data) {
|
||||
@ -469,7 +478,7 @@ export const initializeSendState = createAsyncThunk(
|
||||
|
||||
// Set a basic gasLimit in the event that other estimation fails
|
||||
let gasLimit =
|
||||
asset.type === ASSET_TYPES.TOKEN
|
||||
asset.type === ASSET_TYPES.TOKEN || asset.type === ASSET_TYPES.COLLECTIBLE
|
||||
? GAS_LIMITS.BASE_TOKEN_ESTIMATE
|
||||
: GAS_LIMITS.SIMPLE;
|
||||
if (
|
||||
@ -510,6 +519,17 @@ export const initializeSendState = createAsyncThunk(
|
||||
}
|
||||
balance = await getERC20Balance(asset.details, fromAddress);
|
||||
}
|
||||
|
||||
if (asset.type === ASSET_TYPES.COLLECTIBLE) {
|
||||
if (asset.details === null) {
|
||||
// If we're sending a collectible but details have not been provided we must
|
||||
// abort and set the send slice into invalid status.
|
||||
throw new Error(
|
||||
'Send slice initialized as collectibles send without token details',
|
||||
);
|
||||
}
|
||||
balance = '0x1';
|
||||
}
|
||||
return {
|
||||
address: fromAddress,
|
||||
nativeBalance: account.balance,
|
||||
@ -867,7 +887,10 @@ const slice = createSlice({
|
||||
updateAsset: (state, action) => {
|
||||
state.asset.type = action.payload.type;
|
||||
state.asset.balance = action.payload.balance;
|
||||
if (state.asset.type === ASSET_TYPES.TOKEN) {
|
||||
if (
|
||||
state.asset.type === ASSET_TYPES.TOKEN ||
|
||||
state.asset.type === ASSET_TYPES.COLLECTIBLE
|
||||
) {
|
||||
state.asset.details = action.payload.details;
|
||||
} else {
|
||||
// clear the details object when sending native currency
|
||||
@ -939,12 +962,25 @@ const slice = createSlice({
|
||||
// amount.
|
||||
state.draftTransaction.txParams.to = state.asset.details.address;
|
||||
state.draftTransaction.txParams.value = '0x0';
|
||||
state.draftTransaction.txParams.data = generateTokenTransferData({
|
||||
state.draftTransaction.txParams.data = generateERC20TransferData({
|
||||
toAddress: state.recipient.address,
|
||||
amount: state.amount.value,
|
||||
sendToken: state.asset.details,
|
||||
});
|
||||
break;
|
||||
case ASSET_TYPES.COLLECTIBLE:
|
||||
// When sending a token the to address is the contract address of
|
||||
// the token being sent. The value is set to '0x0' and the data
|
||||
// is generated from the recipient address, token being sent and
|
||||
// amount.
|
||||
state.draftTransaction.txParams.to = state.asset.details.address;
|
||||
state.draftTransaction.txParams.value = '0x0';
|
||||
state.draftTransaction.txParams.data = generateERC721TransferData({
|
||||
toAddress: state.recipient.address,
|
||||
fromAddress: state.account.address,
|
||||
tokenId: state.asset.details.tokenId,
|
||||
});
|
||||
break;
|
||||
case ASSET_TYPES.NATIVE:
|
||||
default:
|
||||
// When sending native currency the to and value fields use the
|
||||
@ -1018,7 +1054,9 @@ const slice = createSlice({
|
||||
recipient.error = null;
|
||||
recipient.warning = null;
|
||||
} else {
|
||||
const isSendingToken = asset.type === ASSET_TYPES.TOKEN;
|
||||
const isSendingToken =
|
||||
asset.type === ASSET_TYPES.TOKEN ||
|
||||
asset.type === ASSET_TYPES.COLLECTIBLE;
|
||||
const { chainId, tokens, tokenAddressList } = action.payload;
|
||||
if (
|
||||
isBurnAddress(recipient.userInput) ||
|
||||
@ -1395,12 +1433,37 @@ export function updateSendAsset({ type, details }) {
|
||||
details,
|
||||
state.send.account.address ?? getSelectedAddress(state),
|
||||
);
|
||||
// TODO remove along with migration of isERC721 tokens and stripping away this designation
|
||||
if (details && details.isERC721 === undefined) {
|
||||
const updatedAssetDetails = await updateTokenType(details.address);
|
||||
details.isERC721 = updatedAssetDetails.isERC721;
|
||||
}
|
||||
|
||||
details.standard = ERC20;
|
||||
await dispatch(hideLoadingIndication());
|
||||
} else if (type === ASSET_TYPES.COLLECTIBLE) {
|
||||
let isCurrentOwner = true;
|
||||
try {
|
||||
isCurrentOwner = await isCollectibleOwner(
|
||||
getSelectedAddress(state),
|
||||
details.address,
|
||||
details.tokenId,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.message.includes('Unable to verify ownership.')) {
|
||||
// this would indicate that either our attempts to verify ownership failed because of network issues,
|
||||
// or, somehow a token has been added to collectibles state with an incorrect chainId.
|
||||
} else {
|
||||
// Any other error is unexpected and should be surfaced.
|
||||
dispatch(displayWarning(error.message));
|
||||
}
|
||||
}
|
||||
if (isCurrentOwner) {
|
||||
balance = '0x1';
|
||||
} else {
|
||||
throw new Error(
|
||||
'Send slice initialized as collectible send with a collectible not currently owned by the select account',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// if changing to native currency, get it from the account key in send
|
||||
// state which is kept in sync when accounts change.
|
||||
@ -1559,6 +1622,7 @@ export function signTransaction() {
|
||||
draftTransaction: { id, txParams },
|
||||
recipient: { address },
|
||||
amount: { value },
|
||||
account: { address: selectedAddress },
|
||||
eip1559support,
|
||||
} = state[name];
|
||||
if (stage === SEND_STAGES.EDIT) {
|
||||
@ -1598,6 +1662,34 @@ export function signTransaction() {
|
||||
to: undefined,
|
||||
data: undefined,
|
||||
});
|
||||
dispatch(showConfTxPage());
|
||||
dispatch(hideLoadingIndication());
|
||||
} catch (error) {
|
||||
dispatch(hideLoadingIndication());
|
||||
dispatch(displayWarning(error.message));
|
||||
}
|
||||
} else if (asset.type === ASSET_TYPES.COLLECTIBLE) {
|
||||
// When sending a collectible transaction we have to use the collectible.transferFrom method
|
||||
// on the collectible contract to construct the transaction. This results in
|
||||
// the proper transaction data and properties being set and a new
|
||||
// transaction being added to background state. Once the new transaction
|
||||
// is added to state a subsequent confirmation will be queued.
|
||||
try {
|
||||
const collectibleContract = global.eth
|
||||
.contract(abiERC721)
|
||||
.at(asset.details.address);
|
||||
|
||||
collectibleContract.transferFrom(
|
||||
selectedAddress,
|
||||
address,
|
||||
asset.details.tokenId,
|
||||
{
|
||||
...txParams,
|
||||
to: undefined,
|
||||
data: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
dispatch(showConfTxPage());
|
||||
dispatch(hideLoadingIndication());
|
||||
} catch (error) {
|
||||
@ -1655,7 +1747,7 @@ export function editTransaction(
|
||||
throw new Error(
|
||||
`send/editTransaction dispatched with assetType 'TOKEN' but missing assetData or assetDetails parameter`,
|
||||
);
|
||||
} else {
|
||||
} else if (assetType === ASSET_TYPES.TOKEN) {
|
||||
const {
|
||||
data,
|
||||
from,
|
||||
@ -1693,6 +1785,36 @@ export function editTransaction(
|
||||
nickname,
|
||||
}),
|
||||
);
|
||||
} else if (assetType === ASSET_TYPES.COLLECTIBLE) {
|
||||
const {
|
||||
data,
|
||||
from,
|
||||
to: tokenAddress,
|
||||
gas: gasLimit,
|
||||
gasPrice,
|
||||
} = txParams;
|
||||
const address = getTokenAddressParam(tokenData);
|
||||
const nickname = getAddressBookEntry(state, address)?.name ?? '';
|
||||
|
||||
await dispatch(
|
||||
updateSendAsset({
|
||||
type: ASSET_TYPES.COLLECTIBLE,
|
||||
details: { ...assetDetails, address: tokenAddress },
|
||||
}),
|
||||
);
|
||||
|
||||
await dispatch(
|
||||
actions.editTransaction({
|
||||
data,
|
||||
id: transactionId,
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
from,
|
||||
amount: '0x1',
|
||||
address,
|
||||
nickname,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ jest.mock('../../store/actions', () => {
|
||||
estimateGas: jest.fn(() => Promise.resolve('0x0')),
|
||||
getGasFeeEstimatesAndStartPolling: jest.fn(() => Promise.resolve()),
|
||||
updateTokenType: jest.fn(() => Promise.resolve({ isERC721: false })),
|
||||
isCollectibleOwner: jest.fn(() => Promise.resolve(true)),
|
||||
};
|
||||
});
|
||||
|
||||
@ -2004,6 +2005,7 @@ describe('Send Slice', () => {
|
||||
draftTransaction: {},
|
||||
recipient: {},
|
||||
amount: {},
|
||||
account: {},
|
||||
},
|
||||
};
|
||||
|
||||
@ -2139,7 +2141,7 @@ describe('Send Slice', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should set up the appropriate state for editing a token asset transaction', async () => {
|
||||
it('should set up the appropriate state for editing a collectible asset transaction', async () => {
|
||||
const editTransactionState = {
|
||||
metamask: {
|
||||
blockGasLimit: '0x3a98',
|
||||
@ -2159,7 +2161,7 @@ describe('Send Slice', () => {
|
||||
data: '',
|
||||
from: '0xAddress',
|
||||
to: '0xTokenAddress',
|
||||
gas: GAS_LIMITS.SIMPLE,
|
||||
gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
|
||||
gasPrice: '0x3b9aca00', // 1000000000
|
||||
value: '0x0',
|
||||
},
|
||||
@ -2203,56 +2205,54 @@ describe('Send Slice', () => {
|
||||
|
||||
await store.dispatch(
|
||||
editTransaction(
|
||||
ASSET_TYPES.TOKEN,
|
||||
ASSET_TYPES.COLLECTIBLE,
|
||||
1,
|
||||
{
|
||||
name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
|
||||
name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
|
||||
args: {
|
||||
_to: '0xRecipientAddress',
|
||||
_value: ethers.BigNumber.from(15000),
|
||||
},
|
||||
},
|
||||
{ address: '0xAddress', symbol: 'SYMB', decimals: 18 },
|
||||
{
|
||||
address: '0xf5de760f2e916647fd766B4AD9E85ff943cE3A2b',
|
||||
description: 'A test NFT dispensed from faucet.paradigm.xyz.',
|
||||
image:
|
||||
'https://ipfs.io/ipfs/bafybeifvwitulq6elvka2hoqhwixfhgb42l4aiukmtrw335osetikviuuu',
|
||||
name: 'MultiFaucet Test NFT',
|
||||
standard: 'ERC721',
|
||||
tokenId: '26847',
|
||||
},
|
||||
),
|
||||
);
|
||||
const actionResult = store.getActions();
|
||||
|
||||
expect(actionResult).toHaveLength(7);
|
||||
expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION');
|
||||
expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION');
|
||||
expect(actionResult[2].type).toStrictEqual('send/updateAsset');
|
||||
expect(actionResult[2].payload).toStrictEqual({
|
||||
balance: '0x0',
|
||||
type: ASSET_TYPES.TOKEN,
|
||||
expect(actionResult).toHaveLength(5);
|
||||
expect(actionResult[0].type).toStrictEqual('send/updateAsset');
|
||||
expect(actionResult[0].payload).toStrictEqual({
|
||||
balance: '0x1',
|
||||
type: ASSET_TYPES.COLLECTIBLE,
|
||||
details: {
|
||||
address: '0xTokenAddress',
|
||||
decimals: 18,
|
||||
symbol: 'SYMB',
|
||||
isERC721: false,
|
||||
description: 'A test NFT dispensed from faucet.paradigm.xyz.',
|
||||
image:
|
||||
'https://ipfs.io/ipfs/bafybeifvwitulq6elvka2hoqhwixfhgb42l4aiukmtrw335osetikviuuu',
|
||||
name: 'MultiFaucet Test NFT',
|
||||
standard: 'ERC721',
|
||||
tokenId: '26847',
|
||||
},
|
||||
});
|
||||
expect(actionResult[3].type).toStrictEqual(
|
||||
expect(actionResult[1].type).toStrictEqual(
|
||||
'send/computeEstimatedGasLimit/pending',
|
||||
);
|
||||
expect(actionResult[4].type).toStrictEqual(
|
||||
expect(actionResult[2].type).toStrictEqual(
|
||||
'metamask/gas/SET_CUSTOM_GAS_LIMIT',
|
||||
);
|
||||
expect(actionResult[5].type).toStrictEqual(
|
||||
expect(actionResult[3].type).toStrictEqual(
|
||||
'send/computeEstimatedGasLimit/fulfilled',
|
||||
);
|
||||
expect(actionResult[6].type).toStrictEqual('send/editTransaction');
|
||||
expect(actionResult[6].payload).toStrictEqual({
|
||||
address: '0xrecipientaddress', // getting address from tokenData does .toLowerCase
|
||||
amount: '0x3a98',
|
||||
data: '',
|
||||
from: '0xAddress',
|
||||
gasLimit: GAS_LIMITS.SIMPLE,
|
||||
gasPrice: '0x3b9aca00',
|
||||
id: 1,
|
||||
nickname: '',
|
||||
});
|
||||
expect(actionResult[4].type).toStrictEqual('send/editTransaction');
|
||||
|
||||
const action = actionResult[6];
|
||||
const action = actionResult[4];
|
||||
|
||||
const result = sendReducer(initialState, action);
|
||||
|
||||
@ -2275,6 +2275,143 @@ describe('Send Slice', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set up the appropriate state for editing a token asset transaction', async () => {
|
||||
const editTransactionState = {
|
||||
metamask: {
|
||||
blockGasLimit: '0x3a98',
|
||||
selectedAddress: '',
|
||||
provider: {
|
||||
chainId: RINKEBY_CHAIN_ID,
|
||||
},
|
||||
tokens: [],
|
||||
addressBook: {
|
||||
[RINKEBY_CHAIN_ID]: {},
|
||||
},
|
||||
identities: {},
|
||||
unapprovedTxs: {
|
||||
1: {
|
||||
id: 1,
|
||||
txParams: {
|
||||
data: '',
|
||||
from: '0xAddress',
|
||||
to: '0xTokenAddress',
|
||||
gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
|
||||
gasPrice: '0x3b9aca00', // 1000000000
|
||||
value: '0x0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
send: {
|
||||
account: {
|
||||
address: '0xAddress',
|
||||
balance: '0x0',
|
||||
},
|
||||
asset: {
|
||||
type: '',
|
||||
},
|
||||
gas: {
|
||||
gasPrice: '',
|
||||
},
|
||||
amount: {
|
||||
value: '',
|
||||
},
|
||||
draftTransaction: {
|
||||
userInputHexData: '',
|
||||
},
|
||||
recipient: {
|
||||
address: 'Address',
|
||||
nickname: 'NickName',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
global.eth = {
|
||||
contract: sinon.stub().returns({
|
||||
at: sinon.stub().returns({
|
||||
balanceOf: sinon.stub().returns(undefined),
|
||||
}),
|
||||
}),
|
||||
getCode: jest.fn(() => '0xa'),
|
||||
};
|
||||
|
||||
const store = mockStore(editTransactionState);
|
||||
|
||||
await store.dispatch(
|
||||
editTransaction(
|
||||
ASSET_TYPES.TOKEN,
|
||||
1,
|
||||
{
|
||||
name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
|
||||
args: {
|
||||
_to: '0xRecipientAddress',
|
||||
_value: ethers.BigNumber.from(15000),
|
||||
},
|
||||
},
|
||||
{ address: '0xAddress', symbol: 'SYMB', decimals: 18 },
|
||||
),
|
||||
);
|
||||
const actionResult = store.getActions();
|
||||
|
||||
expect(actionResult).toHaveLength(7);
|
||||
expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION');
|
||||
expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION');
|
||||
expect(actionResult[2].type).toStrictEqual('send/updateAsset');
|
||||
expect(actionResult[2].payload).toStrictEqual({
|
||||
balance: '0x0',
|
||||
type: ASSET_TYPES.TOKEN,
|
||||
details: {
|
||||
address: '0xTokenAddress',
|
||||
decimals: 18,
|
||||
symbol: 'SYMB',
|
||||
isERC721: false,
|
||||
standard: 'ERC20',
|
||||
},
|
||||
});
|
||||
expect(actionResult[3].type).toStrictEqual(
|
||||
'send/computeEstimatedGasLimit/pending',
|
||||
);
|
||||
expect(actionResult[4].type).toStrictEqual(
|
||||
'metamask/gas/SET_CUSTOM_GAS_LIMIT',
|
||||
);
|
||||
expect(actionResult[5].type).toStrictEqual(
|
||||
'send/computeEstimatedGasLimit/fulfilled',
|
||||
);
|
||||
expect(actionResult[6].type).toStrictEqual('send/editTransaction');
|
||||
expect(actionResult[6].payload).toStrictEqual({
|
||||
address: '0xrecipientaddress', // getting address from tokenData does .toLowerCase
|
||||
amount: '0x3a98',
|
||||
data: '',
|
||||
from: '0xAddress',
|
||||
gasLimit: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
|
||||
gasPrice: '0x3b9aca00',
|
||||
id: 1,
|
||||
nickname: '',
|
||||
});
|
||||
|
||||
const action = actionResult[6];
|
||||
|
||||
const result = sendReducer(initialState, action);
|
||||
|
||||
expect(result.gas.gasLimit).toStrictEqual(action.payload.gasLimit);
|
||||
expect(result.gas.gasPrice).toStrictEqual(action.payload.gasPrice);
|
||||
|
||||
expect(result.amount.value).toStrictEqual(action.payload.amount);
|
||||
|
||||
expect(result.draftTransaction.txParams.to).toStrictEqual(
|
||||
action.payload.address,
|
||||
);
|
||||
expect(result.draftTransaction.txParams.value).toStrictEqual(
|
||||
action.payload.amount,
|
||||
);
|
||||
expect(result.draftTransaction.txParams.gasPrice).toStrictEqual(
|
||||
action.payload.gasPrice,
|
||||
);
|
||||
expect(result.draftTransaction.txParams.gas).toStrictEqual(
|
||||
action.payload.gasLimit,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectors', () => {
|
||||
|
@ -6,6 +6,7 @@ export const PRIMARY = 'PRIMARY';
|
||||
export const SECONDARY = 'SECONDARY';
|
||||
|
||||
export const ERC20 = 'ERC20';
|
||||
export const ERC721 = 'ERC721';
|
||||
|
||||
export const GAS_ESTIMATE_TYPES = {
|
||||
SLOW: 'SLOW',
|
||||
|
@ -33,7 +33,7 @@ const hstInterface = new ethers.utils.Interface(abi);
|
||||
* @param data
|
||||
* @returns {EthersContractCall | undefined}
|
||||
*/
|
||||
export function getTokenData(data) {
|
||||
export function getTransactionData(data) {
|
||||
try {
|
||||
return hstInterface.parseTransaction({ data });
|
||||
} catch (error) {
|
||||
|
@ -7,9 +7,9 @@ import {
|
||||
import * as utils from './transactions.util';
|
||||
|
||||
describe('Transactions utils', () => {
|
||||
describe('getTokenData', () => {
|
||||
describe('getTransactionData', () => {
|
||||
it('should return token data', () => {
|
||||
const tokenData = utils.getTokenData(
|
||||
const tokenData = utils.getTransactionData(
|
||||
'0xa9059cbb00000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000004e20',
|
||||
);
|
||||
expect(tokenData).toStrictEqual(expect.anything());
|
||||
@ -22,7 +22,7 @@ describe('Transactions utils', () => {
|
||||
});
|
||||
|
||||
it('should not throw errors when called without arguments', () => {
|
||||
expect(() => utils.getTokenData()).not.toThrow();
|
||||
expect(() => utils.getTransactionData()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getTokenData } from '../helpers/utils/transactions.util';
|
||||
import { getTransactionData } from '../helpers/utils/transactions.util';
|
||||
|
||||
/**
|
||||
* useTokenData
|
||||
@ -19,6 +19,6 @@ export function useTokenData(transactionData, isTokenTransaction = true) {
|
||||
if (!isTokenTransaction || !transactionData) {
|
||||
return null;
|
||||
}
|
||||
return getTokenData(transactionData);
|
||||
return getTransactionData(transactionData);
|
||||
}, [isTokenTransaction, transactionData]);
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ describe('useTokenDisplayValue', () => {
|
||||
describe(`when input is decimals: ${token.decimals} and value: ${tokenValue}`, () => {
|
||||
it(`should return ${displayValue} as displayValue`, () => {
|
||||
const getTokenValueStub = sinon.stub(tokenUtil, 'getTokenValueParam');
|
||||
const getTokenDataStub = sinon.stub(txUtil, 'getTokenData');
|
||||
const getTokenDataStub = sinon.stub(txUtil, 'getTransactionData');
|
||||
|
||||
getTokenDataStub.callsFake(() => tokenData);
|
||||
getTokenValueStub.callsFake(() => tokenValue);
|
||||
|
@ -6,7 +6,10 @@ import {
|
||||
} from '../helpers/utils/transactions.util';
|
||||
import { camelCaseToCapitalize } from '../helpers/utils/common.util';
|
||||
import { PRIMARY, SECONDARY } from '../helpers/constants/common';
|
||||
import { getTokenAddressParam } from '../helpers/utils/token-util';
|
||||
import {
|
||||
getTokenAddressParam,
|
||||
getTokenValueParam,
|
||||
} from '../helpers/utils/token-util';
|
||||
import {
|
||||
isEqualCaseInsensitive,
|
||||
formatDateWithYearContext,
|
||||
@ -18,7 +21,7 @@ import {
|
||||
PENDING_STATUS_HASH,
|
||||
TOKEN_CATEGORY_HASH,
|
||||
} from '../helpers/constants/transactions';
|
||||
import { getTokens } from '../ducks/metamask/metamask';
|
||||
import { getCollectibles, getTokens } from '../ducks/metamask/metamask';
|
||||
import {
|
||||
TRANSACTION_TYPES,
|
||||
TRANSACTION_GROUP_CATEGORIES,
|
||||
@ -64,6 +67,7 @@ export function useTransactionDisplayData(transactionGroup) {
|
||||
const dispatch = useDispatch();
|
||||
const currentAsset = useCurrentAsset();
|
||||
const knownTokens = useSelector(getTokens);
|
||||
const knownCollectibles = useSelector(getCollectibles);
|
||||
const t = useI18nContext();
|
||||
const { initialTransaction, primaryTransaction } = transactionGroup;
|
||||
// initialTransaction contains the data we need to derive the primary purpose of this transaction group
|
||||
@ -103,10 +107,24 @@ export function useTransactionDisplayData(transactionGroup) {
|
||||
knownTokens.find(({ address }) =>
|
||||
isEqualCaseInsensitive(address, recipientAddress),
|
||||
);
|
||||
|
||||
const tokenData = useTokenData(
|
||||
initialTransaction?.txParams?.data,
|
||||
isTokenCategory,
|
||||
);
|
||||
|
||||
// If this is an ERC20 token transaction this value is equal to the amount sent
|
||||
// If it is an ERC721 token transaction it is the tokenId being sent
|
||||
const tokenAmountOrTokenId = getTokenValueParam(tokenData);
|
||||
|
||||
const collectible =
|
||||
isTokenCategory &&
|
||||
knownCollectibles.find(
|
||||
({ address, tokenId }) =>
|
||||
isEqualCaseInsensitive(address, recipientAddress) &&
|
||||
tokenId === tokenAmountOrTokenId,
|
||||
);
|
||||
|
||||
const tokenDisplayValue = useTokenDisplayValue(
|
||||
initialTransaction?.txParams?.data,
|
||||
token,
|
||||
@ -220,7 +238,9 @@ export function useTransactionDisplayData(transactionGroup) {
|
||||
type === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER
|
||||
) {
|
||||
category = TRANSACTION_GROUP_CATEGORIES.SEND;
|
||||
title = t('sendSpecifiedTokens', [token?.symbol || t('token')]);
|
||||
title = t('sendSpecifiedTokens', [
|
||||
token?.symbol || collectible?.name || t('token'),
|
||||
]);
|
||||
recipientAddress = getTokenAddressParam(tokenData);
|
||||
subtitle = t('toAddress', [shortenAddress(recipientAddress)]);
|
||||
} else if (type === TRANSACTION_TYPES.SIMPLE_SEND) {
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
updateCustomNonce,
|
||||
getNextNonce,
|
||||
} from '../../store/actions';
|
||||
import { getTokenData } from '../../helpers/utils/transactions.util';
|
||||
import { getTransactionData } from '../../helpers/utils/transactions.util';
|
||||
import {
|
||||
calcTokenAmount,
|
||||
getTokenAddressParam,
|
||||
@ -105,7 +105,7 @@ export default function ConfirmApprove() {
|
||||
const tokenSymbol = currentToken?.symbol;
|
||||
const decimals = Number(currentToken?.decimals);
|
||||
const tokenImage = currentToken?.image;
|
||||
const tokenData = getTokenData(data);
|
||||
const tokenData = getTransactionData(data);
|
||||
const tokenValue = getTokenValueParam(tokenData);
|
||||
const toAddress = getTokenAddressParam(tokenData);
|
||||
const tokenAmount =
|
||||
|
@ -4,13 +4,13 @@ import {
|
||||
calcTokenValue,
|
||||
getTokenAddressParam,
|
||||
} from '../../helpers/utils/token-util';
|
||||
import { getTokenData } from '../../helpers/utils/transactions.util';
|
||||
import { getTransactionData } from '../../helpers/utils/transactions.util';
|
||||
|
||||
export function getCustomTxParamsData(
|
||||
data,
|
||||
{ customPermissionAmount, decimals },
|
||||
) {
|
||||
const tokenData = getTokenData(data);
|
||||
const tokenData = getTransactionData(data);
|
||||
|
||||
if (!tokenData) {
|
||||
throw new Error('Invalid data');
|
||||
|
@ -14,10 +14,12 @@ import { getWeiHexFromDecimalValue } from '../../helpers/utils/conversions.util'
|
||||
import { ETH, PRIMARY } from '../../helpers/constants/common';
|
||||
|
||||
export default function ConfirmTokenTransactionBase({
|
||||
image,
|
||||
title,
|
||||
subtitle,
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
tokenAmount = '0',
|
||||
tokenSymbol,
|
||||
fiatTransactionTotal,
|
||||
ethTransactionTotal,
|
||||
ethTransactionTotalMaxAmount,
|
||||
@ -69,38 +71,45 @@ export default function ConfirmTokenTransactionBase({
|
||||
tokenAmount,
|
||||
]);
|
||||
|
||||
const tokensText = `${tokenAmount} ${tokenSymbol}`;
|
||||
const subtitleComponent = () => {
|
||||
if (contractExchangeRate === undefined && subtitle === undefined) {
|
||||
return <span>{t('noConversionRateAvailable')}</span>;
|
||||
}
|
||||
if (subtitle) {
|
||||
return <span>{subtitle}</span>;
|
||||
}
|
||||
return (
|
||||
<UserPreferencedCurrencyDisplay
|
||||
value={hexWeiValue}
|
||||
type={PRIMARY}
|
||||
showEthLogo
|
||||
hideLabel
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmTransactionBase
|
||||
toAddress={toAddress}
|
||||
image={image}
|
||||
onEdit={onEdit}
|
||||
identiconAddress={tokenAddress}
|
||||
title={tokensText}
|
||||
subtitleComponent={
|
||||
contractExchangeRate === undefined ? (
|
||||
<span>{t('noConversionRateAvailable')}</span>
|
||||
) : (
|
||||
<UserPreferencedCurrencyDisplay
|
||||
value={hexWeiValue}
|
||||
type={PRIMARY}
|
||||
showEthLogo
|
||||
hideLabel
|
||||
/>
|
||||
)
|
||||
}
|
||||
primaryTotalTextOverride={`${tokensText} + ${ethTransactionTotal} ${nativeCurrency}`}
|
||||
primaryTotalTextOverrideMaxAmount={`${tokensText} + ${ethTransactionTotalMaxAmount} ${nativeCurrency}`}
|
||||
title={title}
|
||||
subtitleComponent={subtitleComponent()}
|
||||
primaryTotalTextOverride={`${title} + ${ethTransactionTotal} ${nativeCurrency}`}
|
||||
primaryTotalTextOverrideMaxAmount={`${title} + ${ethTransactionTotalMaxAmount} ${nativeCurrency}`}
|
||||
secondaryTotalTextOverride={secondaryTotalTextOverride}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ConfirmTokenTransactionBase.propTypes = {
|
||||
image: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
tokenAddress: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
tokenAmount: PropTypes.string,
|
||||
tokenSymbol: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
contractExchangeRate: PropTypes.number,
|
||||
|
@ -5,8 +5,8 @@ import {
|
||||
contractExchangeRateSelector,
|
||||
transactionFeeSelector,
|
||||
} from '../../selectors';
|
||||
import { getTokens } from '../../ducks/metamask/metamask';
|
||||
import { getTokenData } from '../../helpers/utils/transactions.util';
|
||||
import { getCollectibles, getTokens } from '../../ducks/metamask/metamask';
|
||||
import { getTransactionData } from '../../helpers/utils/transactions.util';
|
||||
import {
|
||||
calcTokenAmount,
|
||||
getTokenAddressParam,
|
||||
@ -49,27 +49,53 @@ const mapStateToProps = (state, ownProps) => {
|
||||
hexMaximumTransactionFee,
|
||||
} = transactionFeeSelector(state, transaction);
|
||||
const tokens = getTokens(state);
|
||||
const currentToken = tokens?.find(({ address }) =>
|
||||
isEqualCaseInsensitive(tokenAddress, address),
|
||||
);
|
||||
const { decimals, symbol: tokenSymbol } = currentToken || {};
|
||||
const collectibles = getCollectibles(state);
|
||||
|
||||
const transactionData = getTransactionData(data);
|
||||
const toAddress = getTokenAddressParam(transactionData);
|
||||
const tokenAmountOrTokenId = getTokenValueParam(transactionData);
|
||||
const ethTransactionTotalMaxAmount = Number(
|
||||
hexWEIToDecETH(hexMaximumTransactionFee),
|
||||
).toFixed(6);
|
||||
|
||||
const tokenData = getTokenData(data);
|
||||
const tokenValue = getTokenValueParam(tokenData);
|
||||
const toAddress = getTokenAddressParam(tokenData);
|
||||
const tokenAmount =
|
||||
tokenData && calcTokenAmount(tokenValue, decimals).toFixed();
|
||||
const contractExchangeRate = contractExchangeRateSelector(state);
|
||||
const currentToken = tokens?.find(({ address }) =>
|
||||
isEqualCaseInsensitive(tokenAddress, address),
|
||||
);
|
||||
const currentCollectible = collectibles?.find(
|
||||
({ address, tokenId }) =>
|
||||
isEqualCaseInsensitive(tokenAddress, address) &&
|
||||
tokenId === tokenAmountOrTokenId,
|
||||
);
|
||||
|
||||
let image,
|
||||
tokenId,
|
||||
collectibleName,
|
||||
tokenAmount,
|
||||
contractExchangeRate,
|
||||
title,
|
||||
subtitle;
|
||||
|
||||
if (currentCollectible) {
|
||||
({ image, tokenId, name: collectibleName } = currentCollectible || {});
|
||||
|
||||
title = collectibleName;
|
||||
subtitle = `#${tokenId}`;
|
||||
} else if (currentToken) {
|
||||
const { decimals, symbol: tokenSymbol } = currentToken || {};
|
||||
tokenAmount =
|
||||
transactionData &&
|
||||
calcTokenAmount(tokenAmountOrTokenId, decimals).toFixed();
|
||||
contractExchangeRate = contractExchangeRateSelector(state);
|
||||
title = `${tokenAmount} ${tokenSymbol}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
subtitle,
|
||||
image,
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
currentCurrency,
|
||||
conversionRate,
|
||||
contractExchangeRate,
|
||||
|
@ -121,6 +121,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
onEdit: PropTypes.func,
|
||||
subtitleComponent: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
getNextNonce: PropTypes.func,
|
||||
nextNonce: PropTypes.number,
|
||||
@ -613,7 +614,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
<LoadingHeartBeat />
|
||||
<strong key="editGasSubTextAmountLabel">
|
||||
{t('editGasSubTextAmountLabel')}
|
||||
</strong>
|
||||
</strong>{' '}
|
||||
{renderTotalMaxAmount()}
|
||||
</div>
|
||||
}
|
||||
@ -863,7 +864,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
value={hexTransactionAmount}
|
||||
type={PRIMARY}
|
||||
showEthLogo
|
||||
ethLogoHeight="26"
|
||||
ethLogoHeight="36"
|
||||
hideLabel
|
||||
/>
|
||||
);
|
||||
@ -1004,6 +1005,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
gasFeeIsCustom,
|
||||
nativeCurrency,
|
||||
hardwareWalletRequiresConnection,
|
||||
image,
|
||||
} = this.props;
|
||||
const {
|
||||
submitting,
|
||||
@ -1060,6 +1062,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
showEdit={Boolean(onEdit)}
|
||||
action={functionType}
|
||||
title={title}
|
||||
image={image}
|
||||
titleComponent={this.renderTitleComponent()}
|
||||
subtitleComponent={this.renderSubtitleComponent()}
|
||||
hideSubtitle={hideSubtitle}
|
||||
|
@ -14,7 +14,11 @@ import {
|
||||
setDefaultHomeActiveTabName,
|
||||
} from '../../store/actions';
|
||||
import { isBalanceSufficient, calcGasTotal } from '../send/send.utils';
|
||||
import { shortenAddress, valuesFor } from '../../helpers/utils/util';
|
||||
import {
|
||||
isEqualCaseInsensitive,
|
||||
shortenAddress,
|
||||
valuesFor,
|
||||
} from '../../helpers/utils/util';
|
||||
import {
|
||||
getAdvancedInlineGasShown,
|
||||
getCustomNonceValue,
|
||||
@ -82,6 +86,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
network,
|
||||
unapprovedTxs,
|
||||
nextNonce,
|
||||
allCollectibleContracts,
|
||||
selectedAddress,
|
||||
provider: { chainId },
|
||||
} = metamask;
|
||||
const { tokenData, txData, tokenProps, nonce } = confirmTransaction;
|
||||
@ -168,6 +174,13 @@ const mapStateToProps = (state, ownProps) => {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isCollectibleTransfer = Boolean(
|
||||
allCollectibleContracts?.[selectedAddress]?.[chainId].find((contract) => {
|
||||
return isEqualCaseInsensitive(contract.address, fullTxData.txParams.to);
|
||||
}),
|
||||
);
|
||||
|
||||
customNonceValue = getCustomNonceValue(state);
|
||||
const isEthGasPrice = getIsEthGasPriceFetched(state);
|
||||
const noGasPrice = !supportsEIP1559 && getNoGasPriceFetched(state);
|
||||
@ -215,7 +228,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
useNonceField: getUseNonceField(state),
|
||||
customNonceValue,
|
||||
insufficientBalance,
|
||||
hideSubtitle: !getShouldShowFiat(state),
|
||||
hideSubtitle: !getShouldShowFiat(state) && !isCollectibleTransfer,
|
||||
hideFiatConversion: !getShouldShowFiat(state),
|
||||
type,
|
||||
nextNonce,
|
||||
|
@ -42,7 +42,11 @@ export default class SendAmountRow extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inError } = this.props;
|
||||
const { inError, asset } = this.props;
|
||||
|
||||
if (asset.type === ASSET_TYPES.COLLECTIBLE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SendRowWrapper
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SendRowWrapper from '../send-row-wrapper';
|
||||
import Identicon from '../../../../components/ui/identicon/identicon.component';
|
||||
import Identicon from '../../../../components/ui/identicon';
|
||||
import TokenBalance from '../../../../components/ui/token-balance';
|
||||
import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display';
|
||||
import { ERC20, PRIMARY } from '../../../../helpers/constants/common';
|
||||
@ -20,10 +20,27 @@ export default class SendAssetRow extends Component {
|
||||
).isRequired,
|
||||
accounts: PropTypes.object.isRequired,
|
||||
selectedAddress: PropTypes.string.isRequired,
|
||||
sendAssetAddress: PropTypes.string,
|
||||
sendAsset: PropTypes.object,
|
||||
updateSendAsset: PropTypes.func.isRequired,
|
||||
nativeCurrency: PropTypes.string,
|
||||
nativeCurrencyImage: PropTypes.string,
|
||||
collectibles: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
address: PropTypes.string.isRequired,
|
||||
tokenId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
standard: PropTypes.string,
|
||||
imageThumbnail: PropTypes.string,
|
||||
imagePreview: PropTypes.string,
|
||||
creator: PropTypes.shape({
|
||||
address: PropTypes.string,
|
||||
config: PropTypes.string,
|
||||
profile_img_url: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@ -34,17 +51,34 @@ export default class SendAssetRow extends Component {
|
||||
state = {
|
||||
isShowingDropdown: false,
|
||||
sendableTokens: [],
|
||||
sendableCollectibles: [],
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const sendableTokens = this.props.tokens.filter((token) => !token.isERC721);
|
||||
this.setState({ sendableTokens });
|
||||
const sendableCollectibles = this.props.collectibles.filter(
|
||||
(collectible) => collectible.isCurrentlyOwned,
|
||||
);
|
||||
this.setState({ sendableTokens, sendableCollectibles });
|
||||
}
|
||||
|
||||
openDropdown = () => this.setState({ isShowingDropdown: true });
|
||||
|
||||
closeDropdown = () => this.setState({ isShowingDropdown: false });
|
||||
|
||||
getAssetSelected = (type, token) => {
|
||||
switch (type) {
|
||||
case ASSET_TYPES.NATIVE:
|
||||
return this.props.nativeCurrency;
|
||||
case ASSET_TYPES.TOKEN:
|
||||
return ERC20;
|
||||
case ASSET_TYPES.COLLECTIBLE:
|
||||
return token?.standard;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
selectToken = (type, token) => {
|
||||
this.setState(
|
||||
{
|
||||
@ -58,7 +92,7 @@ export default class SendAssetRow extends Component {
|
||||
name: 'User clicks "Assets" dropdown',
|
||||
},
|
||||
customVariables: {
|
||||
assetSelected: token ? ERC20 : this.props.nativeCurrency,
|
||||
assetSelected: this.getAssetSelected(type, token),
|
||||
},
|
||||
});
|
||||
this.props.updateSendAsset({
|
||||
@ -75,8 +109,14 @@ export default class SendAssetRow extends Component {
|
||||
return (
|
||||
<SendRowWrapper label={`${t('asset')}:`}>
|
||||
<div className="send-v2__asset-dropdown">
|
||||
{this.renderSendToken()}
|
||||
{this.state.sendableTokens.length > 0
|
||||
<div
|
||||
className="send-v2__asset-dropdown__input-wrapper"
|
||||
onClick={this.openDropdown}
|
||||
>
|
||||
{this.renderSendAsset()}
|
||||
</div>
|
||||
{[...this.state.sendableTokens, ...this.state.sendableCollectibles]
|
||||
.length > 0
|
||||
? this.renderAssetDropdown()
|
||||
: null}
|
||||
</div>
|
||||
@ -84,19 +124,31 @@ export default class SendAssetRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderSendToken() {
|
||||
const { sendAssetAddress } = this.props;
|
||||
const token = this.props.tokens.find(({ address }) =>
|
||||
isEqualCaseInsensitive(address, sendAssetAddress),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="send-v2__asset-dropdown__input-wrapper"
|
||||
onClick={this.openDropdown}
|
||||
>
|
||||
{token ? this.renderAsset(token) : this.renderNativeCurrency()}
|
||||
</div>
|
||||
);
|
||||
renderSendAsset() {
|
||||
const {
|
||||
sendAsset: { details, type },
|
||||
tokens,
|
||||
collectibles,
|
||||
} = this.props;
|
||||
|
||||
if (type === ASSET_TYPES.TOKEN) {
|
||||
const token = tokens.find(({ address }) =>
|
||||
isEqualCaseInsensitive(address, details.address),
|
||||
);
|
||||
if (token) {
|
||||
return this.renderToken(token);
|
||||
}
|
||||
} else if (type === ASSET_TYPES.COLLECTIBLE) {
|
||||
const collectible = collectibles.find(
|
||||
({ address, tokenId }) =>
|
||||
isEqualCaseInsensitive(address, details.address) &&
|
||||
tokenId === details.tokenId,
|
||||
);
|
||||
if (collectible) {
|
||||
return this.renderCollectible(collectible);
|
||||
}
|
||||
}
|
||||
return this.renderNativeCurrency();
|
||||
}
|
||||
|
||||
renderAssetDropdown() {
|
||||
@ -110,7 +162,10 @@ export default class SendAssetRow extends Component {
|
||||
<div className="send-v2__asset-dropdown__list">
|
||||
{this.renderNativeCurrency(true)}
|
||||
{this.state.sendableTokens.map((token) =>
|
||||
this.renderAsset(token, true),
|
||||
this.renderToken(token, true),
|
||||
)}
|
||||
{this.state.sendableCollectibles.map((collectible) =>
|
||||
this.renderCollectible(collectible, true),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -127,14 +182,17 @@ export default class SendAssetRow extends Component {
|
||||
nativeCurrencyImage,
|
||||
} = this.props;
|
||||
|
||||
const { sendableTokens, sendableCollectibles } = this.state;
|
||||
|
||||
const balanceValue = accounts[selectedAddress]
|
||||
? accounts[selectedAddress].balance
|
||||
: '';
|
||||
|
||||
const sendableAssets = [...sendableTokens, ...sendableCollectibles];
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
this.state.sendableTokens.length > 0
|
||||
sendableAssets.length > 0
|
||||
? 'send-v2__asset-dropdown__asset'
|
||||
: 'send-v2__asset-dropdown__single-asset'
|
||||
}
|
||||
@ -161,14 +219,14 @@ export default class SendAssetRow extends Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!insideDropdown && this.state.sendableTokens.length > 0 && (
|
||||
{!insideDropdown && sendableAssets.length > 0 && (
|
||||
<i className="fa fa-caret-down fa-lg send-v2__asset-dropdown__caret" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAsset(token, insideDropdown = false) {
|
||||
renderToken(token, insideDropdown = false) {
|
||||
const { address, symbol, image } = token;
|
||||
const { t } = this.context;
|
||||
|
||||
@ -196,4 +254,33 @@ export default class SendAssetRow extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCollectible(collectible, insideDropdown = false) {
|
||||
const { address, name, image, tokenId } = collectible;
|
||||
const { t } = this.context;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={address}
|
||||
className="send-v2__asset-dropdown__asset"
|
||||
onClick={() => this.selectToken(ASSET_TYPES.COLLECTIBLE, collectible)}
|
||||
>
|
||||
<div className="send-v2__asset-dropdown__asset-icon">
|
||||
<Identicon address={address} diameter={36} image={image} />
|
||||
</div>
|
||||
<div className="send-v2__asset-dropdown__asset-data">
|
||||
<div className="send-v2__asset-dropdown__symbol">{name}</div>
|
||||
<div className="send-v2__asset-dropdown__name">
|
||||
<span className="send-v2__asset-dropdown__name__label">
|
||||
{`${t('tokenId')}:`}
|
||||
</span>
|
||||
{tokenId}
|
||||
</div>
|
||||
</div>
|
||||
{!insideDropdown && (
|
||||
<i className="fa fa-caret-down fa-lg send-v2__asset-dropdown__caret" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,21 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getNativeCurrency } from '../../../../ducks/metamask/metamask';
|
||||
import {
|
||||
getCollectibles,
|
||||
getNativeCurrency,
|
||||
} from '../../../../ducks/metamask/metamask';
|
||||
import {
|
||||
getMetaMaskAccounts,
|
||||
getNativeCurrencyImage,
|
||||
} from '../../../../selectors';
|
||||
import { updateSendAsset, getSendAssetAddress } from '../../../../ducks/send';
|
||||
import { updateSendAsset, getSendAsset } from '../../../../ducks/send';
|
||||
import SendAssetRow from './send-asset-row.component';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tokens: state.metamask.tokens,
|
||||
selectedAddress: state.metamask.selectedAddress,
|
||||
sendAssetAddress: getSendAssetAddress(state),
|
||||
collectibles: getCollectibles(state),
|
||||
sendAsset: getSendAsset(state),
|
||||
accounts: getMetaMaskAccounts(state),
|
||||
nativeCurrency: getNativeCurrency(state),
|
||||
nativeCurrencyImage: getNativeCurrencyImage(state),
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
ETH_GAS_PRICE_FETCH_WARNING_KEY,
|
||||
GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
|
||||
GAS_PRICE_EXCESSIVE_ERROR_KEY,
|
||||
UNSENDABLE_ASSET_ERROR_KEY,
|
||||
INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY,
|
||||
} from '../../../helpers/constants/error-keys';
|
||||
import { ASSET_TYPES } from '../../../ducks/send';
|
||||
@ -26,7 +25,6 @@ export default class SendContent extends Component {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
isAssetSendable: PropTypes.bool,
|
||||
showHexData: PropTypes.bool,
|
||||
contact: PropTypes.object,
|
||||
isOwnedAccount: PropTypes.bool,
|
||||
@ -48,7 +46,6 @@ export default class SendContent extends Component {
|
||||
gasIsExcessive,
|
||||
isEthGasPrice,
|
||||
noGasPrice,
|
||||
isAssetSendable,
|
||||
networkOrAccountNotSupports1559,
|
||||
getIsBalanceInsufficient,
|
||||
asset,
|
||||
@ -63,7 +60,9 @@ export default class SendContent extends Component {
|
||||
gasError = INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY;
|
||||
}
|
||||
const showHexData =
|
||||
this.props.showHexData && asset.type !== ASSET_TYPES.TOKEN;
|
||||
this.props.showHexData &&
|
||||
asset.type !== ASSET_TYPES.TOKEN &&
|
||||
asset.type !== ASSET_TYPES.COLLECTIBLE;
|
||||
|
||||
return (
|
||||
<PageContainerContent>
|
||||
@ -72,9 +71,6 @@ export default class SendContent extends Component {
|
||||
{isEthGasPrice
|
||||
? this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)
|
||||
: null}
|
||||
{isAssetSendable === false
|
||||
? this.renderError(UNSENDABLE_ASSET_ERROR_KEY)
|
||||
: null}
|
||||
{error ? this.renderError(error) : null}
|
||||
{warning ? this.renderWarning() : null}
|
||||
{this.maybeRenderAddContact()}
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
checkNetworkOrAccountNotSupports1559,
|
||||
} from '../../../selectors';
|
||||
import {
|
||||
getIsAssetSendable,
|
||||
getIsBalanceInsufficient,
|
||||
getSendTo,
|
||||
getSendAsset,
|
||||
@ -19,7 +18,6 @@ function mapStateToProps(state) {
|
||||
const ownedAccounts = accountsWithSendEtherInfoSelector(state);
|
||||
const to = getSendTo(state);
|
||||
return {
|
||||
isAssetSendable: getIsAssetSendable(state),
|
||||
isOwnedAccount: Boolean(
|
||||
ownedAccounts.find(
|
||||
({ address }) => address.toLowerCase() === to.toLowerCase(),
|
||||
|
@ -28,6 +28,7 @@ const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, {
|
||||
});
|
||||
|
||||
const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb';
|
||||
const COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE = '0x23b872dd';
|
||||
|
||||
const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds';
|
||||
const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens';
|
||||
@ -71,4 +72,5 @@ export {
|
||||
REQUIRED_ERROR,
|
||||
CONFUSING_ENS_ERROR,
|
||||
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
|
||||
COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE,
|
||||
};
|
||||
|
@ -10,13 +10,18 @@ import {
|
||||
|
||||
import { calcTokenAmount } from '../../helpers/utils/token-util';
|
||||
import { addHexPrefix } from '../../../app/scripts/lib/util';
|
||||
|
||||
import { TOKEN_TRANSFER_FUNCTION_SIGNATURE } from './send.constants';
|
||||
import { ERC20, ERC721 } from '../../helpers/constants/common';
|
||||
import {
|
||||
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
|
||||
COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE,
|
||||
} from './send.constants';
|
||||
|
||||
export {
|
||||
addGasBuffer,
|
||||
calcGasTotal,
|
||||
generateTokenTransferData,
|
||||
getAssetTransferData,
|
||||
generateERC20TransferData,
|
||||
generateERC721TransferData,
|
||||
isBalanceSufficient,
|
||||
isTokenBalanceSufficient,
|
||||
ellipsify,
|
||||
@ -123,7 +128,7 @@ function addGasBuffer(
|
||||
return upperGasLimit;
|
||||
}
|
||||
|
||||
function generateTokenTransferData({
|
||||
function generateERC20TransferData({
|
||||
toAddress = '0x0',
|
||||
amount = '0x0',
|
||||
sendToken,
|
||||
@ -145,6 +150,46 @@ function generateTokenTransferData({
|
||||
);
|
||||
}
|
||||
|
||||
function generateERC721TransferData({
|
||||
toAddress = '0x0',
|
||||
fromAddress = '0x0',
|
||||
tokenId,
|
||||
}) {
|
||||
if (!tokenId) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE +
|
||||
Array.prototype.map
|
||||
.call(
|
||||
abi.rawEncode(
|
||||
['address', 'address', 'uint256'],
|
||||
[fromAddress, toAddress, tokenId],
|
||||
),
|
||||
(x) => `00${x.toString(16)}`.slice(-2),
|
||||
)
|
||||
.join('')
|
||||
);
|
||||
}
|
||||
|
||||
function getAssetTransferData({ sendToken, fromAddress, toAddress, amount }) {
|
||||
switch (sendToken.standard) {
|
||||
case ERC721:
|
||||
return generateERC721TransferData({
|
||||
toAddress,
|
||||
fromAddress,
|
||||
tokenId: sendToken.tokenId,
|
||||
});
|
||||
case ERC20:
|
||||
default:
|
||||
return generateERC20TransferData({
|
||||
toAddress,
|
||||
amount,
|
||||
sendToken,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ellipsify(text, first = 6, last = 4) {
|
||||
return `${text.slice(0, first)}...${text.slice(-last)}`;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
|
||||
import {
|
||||
calcGasTotal,
|
||||
generateTokenTransferData,
|
||||
generateERC20TransferData,
|
||||
isBalanceSufficient,
|
||||
isTokenBalanceSufficient,
|
||||
} from './send.utils';
|
||||
@ -53,10 +53,10 @@ describe('send utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTokenTransferData()', () => {
|
||||
describe('generateERC20TransferData()', () => {
|
||||
it('should return undefined if not passed a send token', () => {
|
||||
expect(
|
||||
generateTokenTransferData({
|
||||
generateERC20TransferData({
|
||||
toAddress: 'mockAddress',
|
||||
amount: '0xa',
|
||||
sendToken: undefined,
|
||||
@ -65,7 +65,7 @@ describe('send utils', () => {
|
||||
});
|
||||
|
||||
it('should call abi.rawEncode with the correct params', () => {
|
||||
generateTokenTransferData({
|
||||
generateERC20TransferData({
|
||||
toAddress: 'mockAddress',
|
||||
amount: 'ab',
|
||||
sendToken: { address: '0x0' },
|
||||
@ -80,7 +80,7 @@ describe('send utils', () => {
|
||||
|
||||
it('should return encoded token transfer data', () => {
|
||||
expect(
|
||||
generateTokenTransferData({
|
||||
generateERC20TransferData({
|
||||
toAddress: 'mockAddress',
|
||||
amount: '0xa',
|
||||
sendToken: { address: '0x0' },
|
||||
|
@ -68,7 +68,7 @@ import {
|
||||
SWAPS_ERROR_ROUTE,
|
||||
AWAITING_SWAP_ROUTE,
|
||||
} from '../../../helpers/constants/routes';
|
||||
import { getTokenData } from '../../../helpers/utils/transactions.util';
|
||||
import { getTransactionData } from '../../../helpers/utils/transactions.util';
|
||||
import {
|
||||
calcTokenAmount,
|
||||
calcTokenValue,
|
||||
@ -243,7 +243,7 @@ export default function ViewQuote() {
|
||||
const tokenBalanceUnavailable =
|
||||
tokensWithBalances && balanceToken === undefined;
|
||||
|
||||
const approveData = getTokenData(approveTxParams?.data);
|
||||
const approveData = getTransactionData(approveTxParams?.data);
|
||||
const approveValue = approveData && getTokenValueParam(approveData);
|
||||
const approveAmount =
|
||||
approveValue &&
|
||||
|
@ -1393,8 +1393,29 @@ export function removeCollectible(address, tokenID, dontShowLoadingIndicator) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkAndUpdateCollectiblesOwnershipStatus() {
|
||||
await promisifiedBackground.checkAndUpdateCollectiblesOwnershipStatus();
|
||||
export async function checkAndUpdateAllCollectiblesOwnershipStatus() {
|
||||
await promisifiedBackground.checkAndUpdateAllCollectiblesOwnershipStatus();
|
||||
}
|
||||
|
||||
export async function isCollectibleOwner(
|
||||
ownerAddress,
|
||||
collectibleAddress,
|
||||
collectibleId,
|
||||
) {
|
||||
return await promisifiedBackground.isCollectibleOwner(
|
||||
ownerAddress,
|
||||
collectibleAddress,
|
||||
collectibleId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkAndUpdateSingleCollectibleOwnershipStatus(
|
||||
collectible,
|
||||
) {
|
||||
await promisifiedBackground.checkAndUpdateSingleCollectibleOwnershipStatus(
|
||||
collectible,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
export function removeToken(address) {
|
||||
|
22
yarn.lock
22
yarn.lock
@ -2652,14 +2652,15 @@
|
||||
web3 "^0.20.7"
|
||||
web3-provider-engine "^16.0.3"
|
||||
|
||||
"@metamask/controllers@^23.0.0":
|
||||
version "23.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-23.0.0.tgz#81ea9fa37924a14b08668f37e7e31f46091aa610"
|
||||
integrity sha512-6hKh5H0HM1YLTOdfuD8gRXGCG3brEhTagup204SrlmwabTReaIIb/zXTat7jCs6ZvN362a44GO5mboZ6W9MhIA==
|
||||
"@metamask/controllers@^24.0.0":
|
||||
version "24.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-24.0.0.tgz#380d4e10bd9b903afc38b041ea7e64b30ae34dab"
|
||||
integrity sha512-gOTpvxQTNTrXOUpGOiRKcqHUgnjSiTgJcwNLxenZWqPIixz7eI2qHnjO7ZaGbV/baK7SOa4rRGokvsCNV0mafA==
|
||||
dependencies:
|
||||
"@ethereumjs/common" "^2.3.1"
|
||||
"@ethereumjs/tx" "^3.2.1"
|
||||
"@metamask/contract-metadata" "^1.31.0"
|
||||
"@metamask/metamask-eth-abis" "^2.1.0"
|
||||
"@types/uuid" "^8.3.0"
|
||||
abort-controller "^3.0.0"
|
||||
async-mutex "^0.2.6"
|
||||
@ -2676,9 +2677,6 @@
|
||||
ethereumjs-wallet "^1.0.1"
|
||||
ethers "^5.4.1"
|
||||
ethjs-unit "^0.1.6"
|
||||
human-standard-collectible-abi "^1.0.2"
|
||||
human-standard-multi-collectible-abi "^1.0.4"
|
||||
human-standard-token-abi "^2.0.0"
|
||||
immer "^9.0.6"
|
||||
isomorphic-fetch "^3.0.0"
|
||||
jsonschema "^1.2.4"
|
||||
@ -2768,6 +2766,11 @@
|
||||
gl-mat4 "1.1.4"
|
||||
gl-vec3 "1.0.3"
|
||||
|
||||
"@metamask/metamask-eth-abis@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/metamask-eth-abis/-/metamask-eth-abis-2.1.0.tgz#316c2e72373506f1a0120b76e432760a27eb6806"
|
||||
integrity sha512-T8LBEB0PQo0N1tZQKZ2K8BGmv+IDLcXkzt8Pn7x0YnwZD6YpCIvKqYM3iy2fJ6wFXeCvRKqpn4K6EqwnkSJAbQ==
|
||||
|
||||
"@metamask/object-multiplex@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-1.1.0.tgz#6b1507c4d10caafd2ea82dd2a5360b91631e036e"
|
||||
@ -14761,11 +14764,6 @@ human-standard-collectible-abi@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/human-standard-collectible-abi/-/human-standard-collectible-abi-1.0.2.tgz#077bae9ed1b0b0b82bc46932104b4b499c941aa0"
|
||||
integrity sha512-nD3ITUuSAIBgkaCm9J2BGwlHL8iEzFjJfTleDAC5Wi8RBJEXXhxV0JeJjd95o+rTwf98uTE5MW+VoBKOIYQh0g==
|
||||
|
||||
human-standard-multi-collectible-abi@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/human-standard-multi-collectible-abi/-/human-standard-multi-collectible-abi-1.0.4.tgz#981625bc1a6bea5fef90567f9e12c11581fac497"
|
||||
integrity sha512-ylR9JDXClDJAxWD/QJxsjXJJdLTUmhipTquMAgrfybXL3qX3x3P/vmKg92A7qFu7SqVOf2hyv5dA8vX0j+0Thg==
|
||||
|
||||
human-standard-token-abi@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/human-standard-token-abi/-/human-standard-token-abi-1.0.2.tgz#207d7846796ee5bb85fdd336e769cb38045b2ae0"
|
||||
|
Loading…
Reference in New Issue
Block a user