mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Refactor token send/method confirmation flow (trimmed down) (#13788)
* make use of getTokenStandardAndDetails method exposed on assetsContractController to determine how to represent the contract being interacted with in token contract method calls
This commit is contained in:
parent
bc62e77cc3
commit
3747ace06b
7
app/_locales/en/messages.json
generated
7
app/_locales/en/messages.json
generated
@ -58,6 +58,10 @@
|
||||
"message": "$1 may access and spend up to this max amount",
|
||||
"description": "$1 is the url of the site requesting ability to spend"
|
||||
},
|
||||
"accessAndSpendNoticeNFT": {
|
||||
"message": "$1 may access and spend this asset",
|
||||
"description": "$1 is the url of the site requesting ability to spend"
|
||||
},
|
||||
"accessingYourCamera": {
|
||||
"message": "Accessing your camera..."
|
||||
},
|
||||
@ -279,6 +283,9 @@
|
||||
"approvedAmountWithColon": {
|
||||
"message": "Approved amount:"
|
||||
},
|
||||
"approvedAsset": {
|
||||
"message": "Approved asset"
|
||||
},
|
||||
"areYouDeveloper": {
|
||||
"message": "Are you a developer?"
|
||||
},
|
||||
|
@ -1271,7 +1271,6 @@ export default class MetamaskController extends EventEmitter {
|
||||
appStateController,
|
||||
collectiblesController,
|
||||
collectibleDetectionController,
|
||||
assetsContractController,
|
||||
currencyRateController,
|
||||
detectTokensController,
|
||||
ensController,
|
||||
@ -1427,9 +1426,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
setTheme: preferencesController.setTheme.bind(preferencesController),
|
||||
|
||||
// AssetsContractController
|
||||
getTokenStandardAndDetails: assetsContractController.getTokenStandardAndDetails.bind(
|
||||
assetsContractController,
|
||||
),
|
||||
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
|
||||
|
||||
// CollectiblesController
|
||||
addCollectible: collectiblesController.addCollectible.bind(
|
||||
@ -1758,6 +1755,19 @@ export default class MetamaskController extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
async getTokenStandardAndDetails(address, userAddress, tokenId) {
|
||||
const details = await this.assetsContractController.getTokenStandardAndDetails(
|
||||
address,
|
||||
userAddress,
|
||||
tokenId,
|
||||
);
|
||||
return {
|
||||
...details,
|
||||
decimals: details?.decimals?.toString(10),
|
||||
balance: details?.balance?.toString(10),
|
||||
};
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// VAULT / KEYRING RELATED METHODS
|
||||
//=============================================================================
|
||||
|
@ -16,6 +16,7 @@ const { ensureXServerIsRunning } = require('./x-server');
|
||||
const tinyDelayMs = 200;
|
||||
const regularDelayMs = tinyDelayMs * 2;
|
||||
const largeDelayMs = regularDelayMs * 2;
|
||||
const veryLargeDelayMs = largeDelayMs * 2;
|
||||
const dappPort = 8080;
|
||||
|
||||
const convertToHexValue = (val) => `0x${new BigNumber(val, 10).toString(16)}`;
|
||||
@ -276,6 +277,7 @@ module.exports = {
|
||||
tinyDelayMs,
|
||||
regularDelayMs,
|
||||
largeDelayMs,
|
||||
veryLargeDelayMs,
|
||||
withFixtures,
|
||||
connectDappWithExtensionPopup,
|
||||
completeImportSRPOnboardingFlow,
|
||||
|
@ -3,7 +3,12 @@ const path = require('path');
|
||||
|
||||
const enLocaleMessages = require('../../app/_locales/en/messages.json');
|
||||
const createStaticServer = require('../../development/create-static-server');
|
||||
const { tinyDelayMs, regularDelayMs, largeDelayMs } = require('./helpers');
|
||||
const {
|
||||
tinyDelayMs,
|
||||
regularDelayMs,
|
||||
largeDelayMs,
|
||||
veryLargeDelayMs,
|
||||
} = require('./helpers');
|
||||
const { buildWebDriver } = require('./webdriver');
|
||||
const Ganache = require('./ganache');
|
||||
const { ensureXServerIsRunning } = require('./x-server');
|
||||
@ -272,7 +277,7 @@ describe('MetaMask', function () {
|
||||
const gasPriceInput = inputs[1];
|
||||
await gasLimitInput.fill('4700000');
|
||||
await gasPriceInput.fill('20');
|
||||
await driver.delay(1000);
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement({ text: 'Save', tag: 'button' });
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
||||
|
||||
@ -343,10 +348,11 @@ describe('MetaMask', function () {
|
||||
// Continue to next screen
|
||||
await driver.delay(largeDelayMs);
|
||||
await driver.clickElement({ text: 'Next', tag: 'button' });
|
||||
await driver.delay(regularDelayMs);
|
||||
await driver.delay(largeDelayMs);
|
||||
});
|
||||
|
||||
it('displays the token transfer data', async function () {
|
||||
await driver.delay(largeDelayMs);
|
||||
await driver.clickElement({ text: 'Hex', tag: 'button' });
|
||||
await driver.delay(regularDelayMs);
|
||||
|
||||
@ -386,7 +392,7 @@ describe('MetaMask', function () {
|
||||
const gasPriceInput = inputs[1];
|
||||
await gasLimitInput.fill('100000');
|
||||
await gasPriceInput.fill('100');
|
||||
await driver.delay(1000);
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement({ text: 'Save', tag: 'button' });
|
||||
});
|
||||
|
||||
@ -449,19 +455,20 @@ describe('MetaMask', function () {
|
||||
});
|
||||
|
||||
it('customizes gas', async function () {
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement({ text: 'Edit', tag: 'button' });
|
||||
await driver.delay(largeDelayMs);
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement(
|
||||
{ text: 'Edit suggested gas fee', tag: 'button' },
|
||||
10000,
|
||||
);
|
||||
await driver.delay(1000);
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
const inputs = await driver.findElements('input[type="number"]');
|
||||
const gasLimitInput = inputs[0];
|
||||
const gasPriceInput = inputs[1];
|
||||
await gasLimitInput.fill('60000');
|
||||
await gasPriceInput.fill('10');
|
||||
await driver.delay(1000);
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement({ text: 'Save', tag: 'button' });
|
||||
await driver.findElement({ tag: 'span', text: '0.0006' });
|
||||
});
|
||||
@ -586,7 +593,7 @@ describe('MetaMask', function () {
|
||||
|
||||
await gasLimitInput.fill('60001');
|
||||
|
||||
await driver.delay(1000);
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
|
||||
await driver.clickElement({ text: 'Save', tag: 'button' });
|
||||
|
||||
@ -752,7 +759,7 @@ describe('MetaMask', function () {
|
||||
});
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
await driver.delay(1000);
|
||||
await driver.delay(veryLargeDelayMs);
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
||||
await driver.delay(regularDelayMs);
|
||||
});
|
||||
|
@ -11,6 +11,8 @@ import useAddressDetails from '../../../../../hooks/useAddressDetails';
|
||||
import Identicon from '../../../../ui/identicon';
|
||||
import InfoTooltip from '../../../../ui/info-tooltip';
|
||||
import NicknamePopovers from '../../../modals/nickname-popovers';
|
||||
import Typography from '../../../../ui/typography';
|
||||
import { TYPOGRAPHY } from '../../../../../helpers/constants/design-system';
|
||||
|
||||
const ConfirmPageContainerSummary = (props) => {
|
||||
const {
|
||||
@ -116,9 +118,15 @@ const ConfirmPageContainerSummary = (props) => {
|
||||
<div className="confirm-page-container-summary__title">
|
||||
{renderImage()}
|
||||
{!hideTitle ? (
|
||||
<div className="confirm-page-container-summary__title-text">
|
||||
<Typography
|
||||
className="confirm-page-container-summary__title-text"
|
||||
variant={
|
||||
title && title.length < 10 ? TYPOGRAPHY.H1 : TYPOGRAPHY.H3
|
||||
}
|
||||
title={title}
|
||||
>
|
||||
{titleComponent || title}
|
||||
</div>
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
{hideSubtitle ? null : (
|
||||
|
@ -77,6 +77,14 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__title-text-long {
|
||||
@include H3;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
@include H5;
|
||||
|
||||
|
@ -82,6 +82,7 @@ export default function Typography({
|
||||
fontStyle = 'normal',
|
||||
align,
|
||||
overflowWrap,
|
||||
title,
|
||||
tag,
|
||||
margin = [1, 0],
|
||||
boxProps = {},
|
||||
@ -117,7 +118,10 @@ export default function Typography({
|
||||
return (
|
||||
<Box margin={margin} {...boxProps}>
|
||||
{(boxClassName) => (
|
||||
<Tag className={classnames(boxClassName, computedClassName)}>
|
||||
<Tag
|
||||
className={classnames(boxClassName, computedClassName)}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
)}
|
||||
@ -175,6 +179,10 @@ Typography.propTypes = {
|
||||
* Additional className to assign the Typography component
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Title attribute to include on the element. Will show as tooltip on hover.
|
||||
*/
|
||||
title: PropTypes.string,
|
||||
/**
|
||||
* The text content of the Typography component
|
||||
*/
|
||||
|
@ -4,8 +4,12 @@ import {
|
||||
conversionUtil,
|
||||
multiplyCurrencies,
|
||||
} from '../../../shared/modules/conversion.utils';
|
||||
import { getTokenStandardAndDetails } from '../../store/actions';
|
||||
import { ERC1155, ERC721 } from '../constants/common';
|
||||
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
|
||||
import * as util from './util';
|
||||
import { formatCurrency } from './confirm-tx.util';
|
||||
import { getTransactionData } from './transactions.util';
|
||||
|
||||
const DEFAULT_SYMBOL = '';
|
||||
|
||||
@ -212,3 +216,48 @@ export function getTokenFiatAmount(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getAssetDetails(
|
||||
tokenAddress,
|
||||
currentUserAddress,
|
||||
transactionData,
|
||||
existingCollectibles,
|
||||
) {
|
||||
const tokenData = getTransactionData(transactionData);
|
||||
if (!tokenData) {
|
||||
throw new Error('Unable to detect valid token data');
|
||||
}
|
||||
|
||||
const tokenId = getTokenValueParam(tokenData);
|
||||
let tokenDetails;
|
||||
try {
|
||||
tokenDetails = await getTokenStandardAndDetails(
|
||||
tokenAddress,
|
||||
currentUserAddress,
|
||||
tokenId,
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(error);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (tokenDetails?.standard) {
|
||||
const { standard } = tokenDetails;
|
||||
if (standard === ERC721 || standard === ERC1155) {
|
||||
const existingCollectible = existingCollectibles.find(({ address }) =>
|
||||
isEqualCaseInsensitive(tokenAddress, address),
|
||||
);
|
||||
|
||||
if (existingCollectible) {
|
||||
return {
|
||||
...existingCollectible,
|
||||
standard,
|
||||
};
|
||||
}
|
||||
}
|
||||
// else if not a collectible already in state or standard === ERC20 just return tokenDetails as it contains all required data
|
||||
return tokenDetails;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
118
ui/hooks/useAssetDetails.js
Normal file
118
ui/hooks/useAssetDetails.js
Normal file
@ -0,0 +1,118 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { getCollectibles, getTokens } from '../ducks/metamask/metamask';
|
||||
import { ERC1155, ERC721, ERC20 } from '../helpers/constants/common';
|
||||
import {
|
||||
calcTokenAmount,
|
||||
getAssetDetails,
|
||||
getTokenAddressParam,
|
||||
getTokenValueParam,
|
||||
} from '../helpers/utils/token-util';
|
||||
import { getTransactionData } from '../helpers/utils/transactions.util';
|
||||
import { getTokenList } from '../selectors';
|
||||
import { hideLoadingIndication, showLoadingIndication } from '../store/actions';
|
||||
import { usePrevious } from './usePrevious';
|
||||
|
||||
export function useAssetDetails(tokenAddress, userAddress, transactionData) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// state selectors
|
||||
const tokens = useSelector(getTokens);
|
||||
const collectibles = useSelector(getCollectibles);
|
||||
const tokenList = useSelector(getTokenList);
|
||||
|
||||
// in-hook state
|
||||
const [currentAsset, setCurrentAsset] = useState(null);
|
||||
|
||||
// previous state checkers
|
||||
const prevTokenAddress = usePrevious(tokenAddress);
|
||||
const prevUserAddress = usePrevious(userAddress);
|
||||
const prevTransactionData = usePrevious(transactionData);
|
||||
|
||||
useEffect(() => {
|
||||
async function getAndSetAssetDetails() {
|
||||
dispatch(showLoadingIndication());
|
||||
const assetDetails = await getAssetDetails(
|
||||
tokenAddress,
|
||||
userAddress,
|
||||
transactionData,
|
||||
collectibles,
|
||||
tokens,
|
||||
tokenList,
|
||||
);
|
||||
setCurrentAsset(assetDetails);
|
||||
dispatch(hideLoadingIndication());
|
||||
}
|
||||
if (
|
||||
tokenAddress !== prevTokenAddress ||
|
||||
userAddress !== prevUserAddress ||
|
||||
transactionData !== prevTransactionData
|
||||
) {
|
||||
getAndSetAssetDetails();
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
prevTokenAddress,
|
||||
prevTransactionData,
|
||||
prevUserAddress,
|
||||
tokenAddress,
|
||||
userAddress,
|
||||
transactionData,
|
||||
collectibles,
|
||||
tokens,
|
||||
tokenList,
|
||||
]);
|
||||
|
||||
let assetStandard,
|
||||
assetName,
|
||||
assetAddress,
|
||||
tokenSymbol,
|
||||
decimals,
|
||||
tokenImage,
|
||||
userBalance,
|
||||
tokenValue,
|
||||
toAddress,
|
||||
tokenAmount,
|
||||
tokenId;
|
||||
|
||||
if (currentAsset) {
|
||||
const {
|
||||
standard,
|
||||
symbol,
|
||||
image,
|
||||
name,
|
||||
balance,
|
||||
decimals: currentAssetDecimals,
|
||||
} = currentAsset;
|
||||
const tokenData = getTransactionData(transactionData);
|
||||
assetStandard = standard;
|
||||
assetAddress = tokenAddress;
|
||||
tokenSymbol = symbol;
|
||||
tokenImage = image;
|
||||
toAddress = getTokenAddressParam(tokenData);
|
||||
if (assetStandard === ERC721 || assetStandard === ERC1155) {
|
||||
assetName = name;
|
||||
tokenId = getTokenValueParam(tokenData);
|
||||
}
|
||||
if (assetStandard === ERC20) {
|
||||
userBalance = balance;
|
||||
decimals = Number(currentAssetDecimals?.toString(10));
|
||||
tokenAmount =
|
||||
tokenData &&
|
||||
calcTokenAmount(getTokenValueParam(tokenData), decimals).toString(10);
|
||||
}
|
||||
}
|
||||
return {
|
||||
assetStandard,
|
||||
assetName,
|
||||
assetAddress,
|
||||
userBalance,
|
||||
tokenSymbol,
|
||||
decimals,
|
||||
tokenImage,
|
||||
tokenValue,
|
||||
toAddress,
|
||||
tokenAmount,
|
||||
tokenId,
|
||||
};
|
||||
}
|
@ -28,6 +28,7 @@ import { SECOND } from '../../../../shared/constants/time';
|
||||
import { ConfirmPageContainerWarning } from '../../../components/app/confirm-page-container/confirm-page-container-content';
|
||||
import GasDetailsItem from '../../../components/app/gas-details-item';
|
||||
import LedgerInstructionField from '../../../components/app/ledger-instruction-field';
|
||||
import { ERC1155, ERC20, ERC721 } from '../../../helpers/constants/common';
|
||||
|
||||
export default class ConfirmApproveContent extends Component {
|
||||
static contextTypes = {
|
||||
@ -60,13 +61,15 @@ export default class ConfirmApproveContent extends Component {
|
||||
warning: PropTypes.string,
|
||||
txData: PropTypes.object,
|
||||
fromAddressIsLedger: PropTypes.bool,
|
||||
tokenImage: PropTypes.string,
|
||||
chainId: PropTypes.string,
|
||||
rpcPrefs: PropTypes.object,
|
||||
isContract: PropTypes.bool,
|
||||
hexTransactionTotal: PropTypes.string,
|
||||
isMultiLayerFeeNetwork: PropTypes.bool,
|
||||
supportsEIP1559V2: PropTypes.bool,
|
||||
assetName: PropTypes.string,
|
||||
tokenId: PropTypes.string,
|
||||
assetStandard: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -178,7 +181,60 @@ export default class ConfirmApproveContent extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPermissionContent() {
|
||||
renderERC721OrERC1155PermissionContent() {
|
||||
const { t } = this.context;
|
||||
const { origin, toAddress, isContract, assetName, tokenId } = this.props;
|
||||
|
||||
const displayedAddress = isContract
|
||||
? `${t('contract')} (${addressSummary(toAddress)})`
|
||||
: addressSummary(toAddress);
|
||||
return (
|
||||
<div className="flex-column">
|
||||
<div className="confirm-approve-content__small-text">
|
||||
{t('accessAndSpendNoticeNFT', [origin])}
|
||||
</div>
|
||||
<div className="flex-row">
|
||||
<div className="confirm-approve-content__label">
|
||||
{t('approvedAsset')}:
|
||||
</div>
|
||||
<div className="confirm-approve-content__medium-text">
|
||||
{`${assetName} #${tokenId}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-row">
|
||||
<div className="confirm-approve-content__label">
|
||||
{t('grantedToWithColon')}
|
||||
</div>
|
||||
<div className="confirm-approve-content__medium-text">
|
||||
{displayedAddress}
|
||||
</div>
|
||||
<div className="confirm-approve-content__medium-text">
|
||||
<Button
|
||||
type="link"
|
||||
className="confirm-approve-content__copy-address"
|
||||
onClick={() => {
|
||||
this.setState({ copied: true });
|
||||
this.copyTimeout = setTimeout(
|
||||
() => this.setState({ copied: false }),
|
||||
SECOND * 3,
|
||||
);
|
||||
copyToClipboard(toAddress);
|
||||
}}
|
||||
title={
|
||||
this.state.copied
|
||||
? t('copiedExclamation')
|
||||
: t('copyToClipboard')
|
||||
}
|
||||
>
|
||||
<CopyIcon size={14} color="#6a737d" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderERC20PermissionContent() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
customTokenAmount,
|
||||
@ -188,6 +244,7 @@ export default class ConfirmApproveContent extends Component {
|
||||
toAddress,
|
||||
isContract,
|
||||
} = this.props;
|
||||
|
||||
const displayedAddress = isContract
|
||||
? `${t('contract')} (${addressSummary(toAddress)})`
|
||||
: addressSummary(toAddress);
|
||||
@ -252,6 +309,75 @@ export default class ConfirmApproveContent extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderFullDetails() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
assetStandard,
|
||||
showEditApprovalPermissionModal,
|
||||
customTokenAmount,
|
||||
tokenAmount,
|
||||
decimals,
|
||||
origin,
|
||||
setCustomAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
} = this.props;
|
||||
if (assetStandard === ERC20) {
|
||||
return (
|
||||
<div className="confirm-approve-content__full-tx-content">
|
||||
<div className="confirm-approve-content__permission">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <img src="./images/user-check.svg" alt="" />,
|
||||
title: t('permissionRequest'),
|
||||
content: this.renderERC20PermissionContent(),
|
||||
showEdit: true,
|
||||
onEditClick: () =>
|
||||
showEditApprovalPermissionModal({
|
||||
customTokenAmount,
|
||||
decimals,
|
||||
origin,
|
||||
setCustomAmount,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
<div className="confirm-approve-content__data">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <i className="fa fa-file" />,
|
||||
title: 'Data',
|
||||
content: this.renderDataContent(),
|
||||
noBorder: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (assetStandard === ERC721 || assetStandard === ERC1155) {
|
||||
return (
|
||||
<div className="confirm-approve-content__full-tx-content">
|
||||
<div className="confirm-approve-content__permission">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <img src="./images/user-check.svg" alt="" />,
|
||||
title: t('permissionRequest'),
|
||||
content: this.renderERC721OrERC1155PermissionContent(),
|
||||
showEdit: false,
|
||||
})}
|
||||
</div>
|
||||
<div className="confirm-approve-content__data">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <i className="fa fa-file" />,
|
||||
title: t('data'),
|
||||
content: this.renderDataContent(),
|
||||
noBorder: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderCustomNonceContent() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
@ -321,11 +447,13 @@ export default class ConfirmApproveContent extends Component {
|
||||
warning,
|
||||
txData,
|
||||
fromAddressIsLedger,
|
||||
tokenImage,
|
||||
toAddress,
|
||||
chainId,
|
||||
rpcPrefs,
|
||||
isContract,
|
||||
assetStandard,
|
||||
tokenId,
|
||||
assetName,
|
||||
} = this.props;
|
||||
const { showFullTxDetails } = this.state;
|
||||
|
||||
@ -368,7 +496,11 @@ export default class ConfirmApproveContent extends Component {
|
||||
</Box>
|
||||
</Box>
|
||||
<div className="confirm-approve-content__title">
|
||||
{t('allowSpendToken', [tokenSymbol])}
|
||||
{t('allowSpendToken', [
|
||||
assetStandard === ERC20
|
||||
? tokenSymbol
|
||||
: `${assetName} (#${tokenId})`,
|
||||
])}
|
||||
</div>
|
||||
<div className="confirm-approve-content__description">
|
||||
{t('trustSiteApprovePermission', [
|
||||
@ -383,7 +515,6 @@ export default class ConfirmApproveContent extends Component {
|
||||
className="confirm-approve-content__address-identicon"
|
||||
diameter={20}
|
||||
address={toAddress}
|
||||
image={tokenImage}
|
||||
/>
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H6}
|
||||
@ -438,24 +569,26 @@ export default class ConfirmApproveContent extends Component {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<div className="confirm-approve-content__edit-submission-button-container">
|
||||
<div
|
||||
className="confirm-approve-content__medium-link-text cursor-pointer"
|
||||
onClick={() =>
|
||||
showEditApprovalPermissionModal({
|
||||
customTokenAmount,
|
||||
decimals,
|
||||
origin,
|
||||
setCustomAmount,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('editPermission')}
|
||||
{assetStandard === ERC20 ? (
|
||||
<div className="confirm-approve-content__edit-submission-button-container">
|
||||
<div
|
||||
className="confirm-approve-content__medium-link-text cursor-pointer"
|
||||
onClick={() =>
|
||||
showEditApprovalPermissionModal({
|
||||
customTokenAmount,
|
||||
decimals,
|
||||
origin,
|
||||
setCustomAmount,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('editPermission')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="confirm-approve-content__card-wrapper">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <i className="fa fa-tag" />,
|
||||
@ -527,36 +660,7 @@ export default class ConfirmApproveContent extends Component {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showFullTxDetails ? (
|
||||
<div className="confirm-approve-content__full-tx-content">
|
||||
<div className="confirm-approve-content__permission">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <img src="./images/user-check.svg" alt="" />,
|
||||
title: t('permissionRequest'),
|
||||
content: this.renderPermissionContent(),
|
||||
showEdit: true,
|
||||
onEditClick: () =>
|
||||
showEditApprovalPermissionModal({
|
||||
customTokenAmount,
|
||||
decimals,
|
||||
origin,
|
||||
setCustomAmount,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
tokenBalance,
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
<div className="confirm-approve-content__data">
|
||||
{this.renderApproveContentCard({
|
||||
symbol: <i className="fa fa-file" />,
|
||||
title: 'Data',
|
||||
content: this.renderDataContent(),
|
||||
noBorder: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showFullTxDetails ? this.renderFullDetails() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { renderWithProvider } from '../../../../test/jest/rendering';
|
||||
import { ERC20 } from '../../../helpers/constants/common';
|
||||
import ConfirmApproveContent from '.';
|
||||
|
||||
const renderComponent = (props) => {
|
||||
@ -16,6 +17,7 @@ const props = {
|
||||
tokenAmount: '10',
|
||||
origin: 'https://metamask.github.io/test-dapp/',
|
||||
tokenSymbol: 'TST',
|
||||
assetStandard: ERC20,
|
||||
tokenImage: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
|
||||
tokenBalance: '15',
|
||||
showCustomizeGasModal: jest.fn(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base';
|
||||
import { EDIT_GAS_MODES } from '../../../shared/constants/gas';
|
||||
import {
|
||||
@ -8,24 +8,15 @@ import {
|
||||
updateCustomNonce,
|
||||
getNextNonce,
|
||||
} from '../../store/actions';
|
||||
import { getTransactionData } from '../../helpers/utils/transactions.util';
|
||||
import {
|
||||
calcTokenAmount,
|
||||
getTokenAddressParam,
|
||||
getTokenValueParam,
|
||||
} from '../../helpers/utils/token-util';
|
||||
import { calcTokenAmount } from '../../helpers/utils/token-util';
|
||||
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
|
||||
import { GasFeeContextProvider } from '../../contexts/gasFee';
|
||||
import { TransactionModalContextProvider } from '../../contexts/transaction-modal';
|
||||
import { useTokenTracker } from '../../hooks/useTokenTracker';
|
||||
import {
|
||||
getTokens,
|
||||
getNativeCurrency,
|
||||
isAddressLedger,
|
||||
} from '../../ducks/metamask/metamask';
|
||||
import {
|
||||
transactionFeeSelector,
|
||||
txDataSelector,
|
||||
getCurrentCurrency,
|
||||
getSubjectMetadata,
|
||||
getUseNonceField,
|
||||
@ -38,12 +29,11 @@ import {
|
||||
getEIP1559V2Enabled,
|
||||
} from '../../selectors';
|
||||
import { useApproveTransaction } from '../../hooks/useApproveTransaction';
|
||||
import { currentNetworkTxListSelector } from '../../selectors/transactions';
|
||||
import AdvancedGasFeePopover from '../../components/app/advanced-gas-fee-popover';
|
||||
import EditGasFeePopover from '../../components/app/edit-gas-fee-popover';
|
||||
import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component';
|
||||
import Loading from '../../components/ui/loading-screen';
|
||||
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
|
||||
import { ERC20, ERC1155, ERC721 } from '../../helpers/constants/common';
|
||||
import { getCustomTxParamsData } from './confirm-approve.util';
|
||||
import ConfirmApproveContent from './confirm-approve-content';
|
||||
|
||||
@ -51,19 +41,28 @@ const isAddressLedgerByFromAddress = (address) => (state) => {
|
||||
return isAddressLedger(state, address);
|
||||
};
|
||||
|
||||
export default function ConfirmApprove() {
|
||||
export default function ConfirmApprove({
|
||||
assetStandard,
|
||||
assetName,
|
||||
userBalance,
|
||||
tokenSymbol,
|
||||
decimals,
|
||||
tokenImage,
|
||||
tokenAmount,
|
||||
tokenId,
|
||||
userAddress,
|
||||
toAddress,
|
||||
transaction,
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
hexTransactionTotal,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const { id: paramsTransactionId } = useParams();
|
||||
const {
|
||||
id: transactionId,
|
||||
txParams: { to: tokenAddress, data, from } = {},
|
||||
} = useSelector(txDataSelector);
|
||||
const { txParams: { data: transactionData } = {} } = transaction;
|
||||
|
||||
const currentCurrency = useSelector(getCurrentCurrency);
|
||||
const nativeCurrency = useSelector(getNativeCurrency);
|
||||
const currentNetworkTxList = useSelector(currentNetworkTxListSelector);
|
||||
const subjectMetadata = useSelector(getSubjectMetadata);
|
||||
const tokens = useSelector(getTokens);
|
||||
const useNonceField = useSelector(getUseNonceField);
|
||||
const nextNonce = useSelector(getNextSuggestedNonce);
|
||||
const customNonceValue = useSelector(getCustomNonceValue);
|
||||
@ -73,45 +72,17 @@ export default function ConfirmApprove() {
|
||||
const networkAndAccountSupports1559 = useSelector(
|
||||
checkNetworkAndAccountSupports1559,
|
||||
);
|
||||
|
||||
const fromAddressIsLedger = useSelector(isAddressLedgerByFromAddress(from));
|
||||
|
||||
const transaction =
|
||||
currentNetworkTxList.find(
|
||||
({ id }) => id === (Number(paramsTransactionId) || transactionId),
|
||||
) || {};
|
||||
const {
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
hexTransactionTotal,
|
||||
} = useSelector((state) => transactionFeeSelector(state, transaction));
|
||||
const fromAddressIsLedger = useSelector(
|
||||
isAddressLedgerByFromAddress(userAddress),
|
||||
);
|
||||
const [customPermissionAmount, setCustomPermissionAmount] = useState('');
|
||||
const [submitWarning, setSubmitWarning] = useState('');
|
||||
const [isContract, setIsContract] = useState(false);
|
||||
|
||||
const eip1559V2Enabled = useSelector(getEIP1559V2Enabled);
|
||||
const supportsEIP1559V2 = eip1559V2Enabled && networkAndAccountSupports1559;
|
||||
|
||||
const currentToken = (tokens &&
|
||||
tokens.find(({ address }) =>
|
||||
isEqualCaseInsensitive(tokenAddress, address),
|
||||
)) || {
|
||||
address: tokenAddress,
|
||||
};
|
||||
|
||||
const { tokensWithBalances } = useTokenTracker([currentToken]);
|
||||
const tokenTrackerBalance = tokensWithBalances[0]?.balance || '';
|
||||
|
||||
const tokenSymbol = currentToken?.symbol;
|
||||
const decimals = Number(currentToken?.decimals);
|
||||
const tokenImage = currentToken?.image;
|
||||
const tokenData = getTransactionData(data);
|
||||
const tokenValue = getTokenValueParam(tokenData);
|
||||
const toAddress = getTokenAddressParam(tokenData);
|
||||
const tokenAmount =
|
||||
tokenData && calcTokenAmount(tokenValue, decimals).toString(10);
|
||||
|
||||
const [customPermissionAmount, setCustomPermissionAmount] = useState('');
|
||||
|
||||
const previousTokenAmount = useRef(tokenAmount);
|
||||
|
||||
const {
|
||||
approveTransaction,
|
||||
showCustomizeGasPopover,
|
||||
@ -125,7 +96,6 @@ export default function ConfirmApprove() {
|
||||
previousTokenAmount.current = tokenAmount;
|
||||
}, [customPermissionAmount, tokenAmount]);
|
||||
|
||||
const [submitWarning, setSubmitWarning] = useState('');
|
||||
const prevNonce = useRef(nextNonce);
|
||||
const prevCustomNonce = useRef(customNonceValue);
|
||||
useEffect(() => {
|
||||
@ -145,7 +115,6 @@ export default function ConfirmApprove() {
|
||||
prevNonce.current = nextNonce;
|
||||
}, [customNonceValue, nextNonce]);
|
||||
|
||||
const [isContract, setIsContract] = useState(false);
|
||||
const checkIfContract = useCallback(async () => {
|
||||
const { isContractAddress } = await readAddressAsContract(
|
||||
global.eth,
|
||||
@ -153,6 +122,7 @@ export default function ConfirmApprove() {
|
||||
);
|
||||
setIsContract(isContractAddress);
|
||||
}, [setIsContract, toAddress]);
|
||||
|
||||
useEffect(() => {
|
||||
checkIfContract();
|
||||
}, [checkIfContract]);
|
||||
@ -162,21 +132,30 @@ export default function ConfirmApprove() {
|
||||
|
||||
const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {};
|
||||
|
||||
const tokensText = `${Number(tokenAmount)} ${tokenSymbol}`;
|
||||
const tokenBalance = tokenTrackerBalance
|
||||
? calcTokenAmount(tokenTrackerBalance, decimals).toString(10)
|
||||
let tokensText;
|
||||
if (assetStandard === ERC20) {
|
||||
tokensText = `${Number(tokenAmount)} ${tokenSymbol}`;
|
||||
} else if (assetStandard === ERC721 || assetStandard === ERC1155) {
|
||||
tokensText = assetName;
|
||||
}
|
||||
|
||||
const tokenBalance = userBalance
|
||||
? calcTokenAmount(userBalance, decimals).toString(10)
|
||||
: '';
|
||||
const customData = customPermissionAmount
|
||||
? getCustomTxParamsData(data, { customPermissionAmount, decimals })
|
||||
? getCustomTxParamsData(transactionData, {
|
||||
customPermissionAmount,
|
||||
decimals,
|
||||
})
|
||||
: null;
|
||||
|
||||
return tokenSymbol === undefined ? (
|
||||
return tokenSymbol === undefined && assetName === undefined ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<GasFeeContextProvider transaction={transaction}>
|
||||
<ConfirmTransactionBase
|
||||
toAddress={toAddress}
|
||||
identiconAddress={tokenAddress}
|
||||
identiconAddress={toAddress}
|
||||
showAccountInHeader
|
||||
title={tokensText}
|
||||
contentComponent={
|
||||
@ -191,6 +170,9 @@ export default function ConfirmApprove() {
|
||||
tokenSymbol={tokenSymbol}
|
||||
tokenImage={tokenImage}
|
||||
tokenBalance={tokenBalance}
|
||||
tokenId={tokenId}
|
||||
assetName={assetName}
|
||||
assetStandard={assetStandard}
|
||||
showCustomizeGasModal={approveTransaction}
|
||||
showEditApprovalPermissionModal={({
|
||||
/* eslint-disable no-shadow */
|
||||
@ -213,10 +195,12 @@ export default function ConfirmApprove() {
|
||||
tokenAmount,
|
||||
tokenBalance,
|
||||
tokenSymbol,
|
||||
tokenId,
|
||||
assetStandard,
|
||||
}),
|
||||
)
|
||||
}
|
||||
data={customData || data}
|
||||
data={customData || transactionData}
|
||||
toAddress={toAddress}
|
||||
currentCurrency={currentCurrency}
|
||||
nativeCurrency={nativeCurrency}
|
||||
@ -280,3 +264,27 @@ export default function ConfirmApprove() {
|
||||
</GasFeeContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ConfirmApprove.propTypes = {
|
||||
assetStandard: PropTypes.string,
|
||||
assetName: PropTypes.string,
|
||||
userBalance: PropTypes.string,
|
||||
tokenSymbol: PropTypes.string,
|
||||
decimals: PropTypes.string,
|
||||
tokenImage: PropTypes.string,
|
||||
tokenAmount: PropTypes.string,
|
||||
tokenId: PropTypes.string,
|
||||
userAddress: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
transaction: PropTypes.shape({
|
||||
origin: PropTypes.string,
|
||||
txParams: PropTypes.shape({
|
||||
data: PropTypes.string,
|
||||
to: PropTypes.string,
|
||||
from: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
hexTransactionTotal: PropTypes.string,
|
||||
};
|
||||
|
118
ui/pages/confirm-send-token/confirm-send-token.js
Normal file
118
ui/pages/confirm-send-token/confirm-send-token.js
Normal file
@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base/confirm-token-transaction-base';
|
||||
import { SEND_ROUTE } from '../../helpers/constants/routes';
|
||||
import { ASSET_TYPES, editTransaction } from '../../ducks/send';
|
||||
import {
|
||||
contractExchangeRateSelector,
|
||||
getCurrentCurrency,
|
||||
} from '../../selectors';
|
||||
import {
|
||||
getConversionRate,
|
||||
getNativeCurrency,
|
||||
} from '../../ducks/metamask/metamask';
|
||||
import { ERC20, ERC721 } from '../../helpers/constants/common';
|
||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import { showSendTokenPage } from '../../store/actions';
|
||||
|
||||
export default function ConfirmSendToken({
|
||||
assetStandard,
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
assetName,
|
||||
tokenSymbol,
|
||||
tokenAmount,
|
||||
tokenId,
|
||||
transaction,
|
||||
image,
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
hexMaximumTransactionFee,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const handleEditTransaction = ({
|
||||
txData,
|
||||
tokenData,
|
||||
tokenProps: assetDetails,
|
||||
}) => {
|
||||
const { id } = txData;
|
||||
dispatch(
|
||||
editTransaction(
|
||||
ASSET_TYPES.TOKEN,
|
||||
id.toString(),
|
||||
tokenData,
|
||||
assetDetails,
|
||||
),
|
||||
);
|
||||
dispatch(clearConfirmTransaction());
|
||||
dispatch(showSendTokenPage());
|
||||
};
|
||||
|
||||
const handleEdit = (confirmTransactionData) => {
|
||||
handleEditTransaction(confirmTransactionData);
|
||||
history.push(SEND_ROUTE);
|
||||
};
|
||||
const conversionRate = useSelector(getConversionRate);
|
||||
const nativeCurrency = useSelector(getNativeCurrency);
|
||||
const currentCurrency = useSelector(getCurrentCurrency);
|
||||
const contractExchangeRate = useSelector(contractExchangeRateSelector);
|
||||
|
||||
let title, subtitle;
|
||||
|
||||
if (assetStandard === ERC721) {
|
||||
title = assetName;
|
||||
subtitle = `#${tokenId}`;
|
||||
} else if (assetStandard === ERC20) {
|
||||
title = `${tokenAmount} ${tokenSymbol}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmTokenTransactionBase
|
||||
onEdit={handleEdit}
|
||||
conversionRate={conversionRate}
|
||||
currentCurrency={currentCurrency}
|
||||
nativeCurrency={nativeCurrency}
|
||||
contractExchangeRate={contractExchangeRate}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
assetStandard={assetStandard}
|
||||
assetName={assetName}
|
||||
tokenSymbol={tokenSymbol}
|
||||
tokenAmount={tokenAmount}
|
||||
tokenId={tokenId}
|
||||
transaction={transaction}
|
||||
image={image}
|
||||
toAddress={toAddress}
|
||||
tokenAddress={tokenAddress}
|
||||
ethTransactionTotal={ethTransactionTotal}
|
||||
fiatTransactionTotal={fiatTransactionTotal}
|
||||
hexMaximumTransactionFee={hexMaximumTransactionFee}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ConfirmSendToken.propTypes = {
|
||||
tokenAmount: PropTypes.string,
|
||||
assetStandard: PropTypes.string,
|
||||
assetName: PropTypes.string,
|
||||
tokenSymbol: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
tokenId: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
tokenAddress: PropTypes.string,
|
||||
transaction: PropTypes.shape({
|
||||
origin: PropTypes.string,
|
||||
txParams: PropTypes.shape({
|
||||
data: PropTypes.string,
|
||||
to: PropTypes.string,
|
||||
from: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
hexMaximumTransactionFee: PropTypes.string,
|
||||
};
|
@ -1 +1 @@
|
||||
export { default } from './confirm-send-token.container';
|
||||
export { default } from './confirm-send-token';
|
||||
|
@ -1,112 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {
|
||||
contractExchangeRateSelector,
|
||||
transactionFeeSelector,
|
||||
} from '../../selectors';
|
||||
import { getCollectibles, getTokens } from '../../ducks/metamask/metamask';
|
||||
import { getTransactionData } from '../../helpers/utils/transactions.util';
|
||||
import {
|
||||
calcTokenAmount,
|
||||
getTokenAddressParam,
|
||||
getTokenValueParam,
|
||||
} from '../../helpers/utils/token-util';
|
||||
import { hexWEIToDecETH } from '../../helpers/utils/conversions.util';
|
||||
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
|
||||
import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
match: { params = {} },
|
||||
} = ownProps;
|
||||
const { id: paramsTransactionId } = params;
|
||||
const {
|
||||
confirmTransaction,
|
||||
metamask: {
|
||||
currentCurrency,
|
||||
conversionRate,
|
||||
currentNetworkTxList,
|
||||
nativeCurrency,
|
||||
},
|
||||
} = state;
|
||||
|
||||
const {
|
||||
txData: {
|
||||
id: transactionId,
|
||||
txParams: { to: tokenAddress, data } = {},
|
||||
} = {},
|
||||
} = confirmTransaction;
|
||||
|
||||
const transaction =
|
||||
currentNetworkTxList.find(
|
||||
({ id }) => id === (Number(paramsTransactionId) || transactionId),
|
||||
) || {};
|
||||
|
||||
const {
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
hexMaximumTransactionFee,
|
||||
} = transactionFeeSelector(state, transaction);
|
||||
const tokens = getTokens(state);
|
||||
const collectibles = getCollectibles(state);
|
||||
|
||||
const transactionData = getTransactionData(data);
|
||||
const toAddress = getTokenAddressParam(transactionData);
|
||||
const tokenAmountOrTokenId = getTokenValueParam(transactionData);
|
||||
const ethTransactionTotalMaxAmount = Number(
|
||||
hexWEIToDecETH(hexMaximumTransactionFee),
|
||||
).toFixed(6);
|
||||
|
||||
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,
|
||||
currentCurrency,
|
||||
conversionRate,
|
||||
contractExchangeRate,
|
||||
fiatTransactionTotal,
|
||||
ethTransactionTotal,
|
||||
ethTransactionTotalMaxAmount,
|
||||
nativeCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps),
|
||||
)(ConfirmTokenTransactionBase);
|
@ -1,6 +1,7 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { I18nContext } from '../../contexts/i18n';
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base';
|
||||
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display';
|
||||
@ -10,26 +11,57 @@ import {
|
||||
addFiat,
|
||||
roundExponential,
|
||||
} from '../../helpers/utils/confirm-tx.util';
|
||||
import { getWeiHexFromDecimalValue } from '../../helpers/utils/conversions.util';
|
||||
import { ETH, PRIMARY } from '../../helpers/constants/common';
|
||||
import {
|
||||
getWeiHexFromDecimalValue,
|
||||
hexWEIToDecETH,
|
||||
} from '../../helpers/utils/conversions.util';
|
||||
import {
|
||||
ERC1155,
|
||||
ERC20,
|
||||
ERC721,
|
||||
ETH,
|
||||
PRIMARY,
|
||||
} from '../../helpers/constants/common';
|
||||
import {
|
||||
contractExchangeRateSelector,
|
||||
getCurrentCurrency,
|
||||
} from '../../selectors';
|
||||
import {
|
||||
getConversionRate,
|
||||
getNativeCurrency,
|
||||
} from '../../ducks/metamask/metamask';
|
||||
|
||||
export default function ConfirmTokenTransactionBase({
|
||||
image,
|
||||
title,
|
||||
subtitle,
|
||||
image = '',
|
||||
assetName,
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
tokenAmount = '0',
|
||||
fiatTransactionTotal,
|
||||
ethTransactionTotal,
|
||||
ethTransactionTotalMaxAmount,
|
||||
contractExchangeRate,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
nativeCurrency,
|
||||
tokenSymbol,
|
||||
tokenId,
|
||||
assetStandard,
|
||||
onEdit,
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
hexMaximumTransactionFee,
|
||||
}) {
|
||||
const t = useContext(I18nContext);
|
||||
const contractExchangeRate = useSelector(contractExchangeRateSelector);
|
||||
const nativeCurrency = useSelector(getNativeCurrency);
|
||||
const currentCurrency = useSelector(getCurrentCurrency);
|
||||
const conversionRate = useSelector(getConversionRate);
|
||||
|
||||
const ethTransactionTotalMaxAmount = Number(
|
||||
hexWEIToDecETH(hexMaximumTransactionFee),
|
||||
);
|
||||
|
||||
let title, subtitle;
|
||||
if (assetStandard === ERC721 || assetStandard === ERC1155) {
|
||||
title = assetName;
|
||||
subtitle = `#${tokenId}`;
|
||||
} else if (assetStandard === ERC20) {
|
||||
title = `${tokenAmount} ${tokenSymbol}`;
|
||||
}
|
||||
|
||||
const hexWeiValue = useMemo(() => {
|
||||
if (tokenAmount === '0' || !contractExchangeRate) {
|
||||
@ -105,17 +137,15 @@ export default function ConfirmTokenTransactionBase({
|
||||
|
||||
ConfirmTokenTransactionBase.propTypes = {
|
||||
image: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
tokenAddress: PropTypes.string,
|
||||
assetName: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
tokenAddress: PropTypes.string,
|
||||
tokenAmount: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
contractExchangeRate: PropTypes.number,
|
||||
conversionRate: PropTypes.number,
|
||||
currentCurrency: PropTypes.string,
|
||||
tokenSymbol: PropTypes.string,
|
||||
tokenId: PropTypes.string,
|
||||
assetStandard: PropTypes.string,
|
||||
onEdit: PropTypes.func,
|
||||
nativeCurrency: PropTypes.string,
|
||||
ethTransactionTotalMaxAmount: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
hexMaximumTransactionFee: PropTypes.string,
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { store } from '../../../.storybook/preview';
|
||||
import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component';
|
||||
import ConfirmTokenTransactionBase from './confirm-token-transaction-base';
|
||||
|
||||
export default {
|
||||
title: 'Pages/ConfirmTokenTransactionBase',
|
||||
|
@ -1,2 +1 @@
|
||||
export { default } from './confirm-token-transaction-base.container';
|
||||
export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component';
|
||||
export { default } from './confirm-token-transaction-base';
|
||||
|
@ -174,7 +174,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
}
|
||||
|
||||
const isCollectibleTransfer = Boolean(
|
||||
allCollectibleContracts?.[selectedAddress]?.[chainId].find((contract) => {
|
||||
allCollectibleContracts?.[selectedAddress]?.[chainId]?.find((contract) => {
|
||||
return isEqualCaseInsensitive(contract.address, fullTxData.txParams.to);
|
||||
}),
|
||||
);
|
||||
|
125
ui/pages/confirm-transaction/confirm-token-transaction-switch.js
Normal file
125
ui/pages/confirm-transaction/confirm-token-transaction-switch.js
Normal file
@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import {
|
||||
CONFIRM_APPROVE_PATH,
|
||||
CONFIRM_SEND_TOKEN_PATH,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
CONFIRM_TRANSFER_FROM_PATH,
|
||||
} from '../../helpers/constants/routes';
|
||||
import { transactionFeeSelector } from '../../selectors';
|
||||
import ConfirmApprove from '../confirm-approve';
|
||||
import ConfirmSendToken from '../confirm-send-token';
|
||||
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base';
|
||||
import ConfirmTransactionSwitch from '../confirm-transaction-switch';
|
||||
|
||||
import { useAssetDetails } from '../../hooks/useAssetDetails';
|
||||
|
||||
export default function ConfirmTokenTransactionSwitch({ transaction }) {
|
||||
const {
|
||||
txParams: { data, to: tokenAddress, from: userAddress } = {},
|
||||
} = transaction;
|
||||
|
||||
const {
|
||||
assetStandard,
|
||||
assetName,
|
||||
userBalance,
|
||||
tokenSymbol,
|
||||
decimals,
|
||||
tokenImage,
|
||||
tokenAmount,
|
||||
tokenId,
|
||||
toAddress,
|
||||
} = useAssetDetails(tokenAddress, userAddress, data);
|
||||
|
||||
const {
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
hexTransactionTotal,
|
||||
hexMaximumTransactionFee,
|
||||
} = useSelector((state) => transactionFeeSelector(state, transaction));
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
|
||||
render={() => (
|
||||
<ConfirmApprove
|
||||
assetStandard={assetStandard}
|
||||
assetName={assetName}
|
||||
userBalance={userBalance}
|
||||
tokenSymbol={tokenSymbol}
|
||||
decimals={decimals}
|
||||
tokenImage={tokenImage}
|
||||
tokenAmount={tokenAmount}
|
||||
tokenId={tokenId}
|
||||
userAddress={userAddress}
|
||||
tokenAddress={tokenAddress}
|
||||
toAddress={toAddress}
|
||||
transaction={transaction}
|
||||
ethTransactionTotal={ethTransactionTotal}
|
||||
fiatTransactionTotal={fiatTransactionTotal}
|
||||
hexTransactionTotal={hexTransactionTotal}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
|
||||
render={() => (
|
||||
<ConfirmTokenTransactionBase
|
||||
assetStandard={assetStandard}
|
||||
assetName={assetName}
|
||||
userBalance={userBalance}
|
||||
tokenSymbol={tokenSymbol}
|
||||
decimals={decimals}
|
||||
image={tokenImage}
|
||||
tokenAddress={tokenAddress}
|
||||
toAddress={toAddress}
|
||||
tokenAmount={tokenAmount}
|
||||
tokenId={tokenId}
|
||||
userAddress={userAddress}
|
||||
transaction={transaction}
|
||||
ethTransactionTotal={ethTransactionTotal}
|
||||
fiatTransactionTotal={fiatTransactionTotal}
|
||||
hexTransactionTotal={hexTransactionTotal}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
|
||||
render={() => (
|
||||
<ConfirmSendToken
|
||||
assetStandard={assetStandard}
|
||||
assetName={assetName}
|
||||
tokenSymbol={tokenSymbol}
|
||||
image={tokenImage}
|
||||
tokenAddress={tokenAddress}
|
||||
toAddress={toAddress}
|
||||
tokenAmount={tokenAmount}
|
||||
tokenId={tokenId}
|
||||
transaction={transaction}
|
||||
ethTransactionTotal={ethTransactionTotal}
|
||||
fiatTransactionTotal={fiatTransactionTotal}
|
||||
hexMaximumTransactionFee={hexMaximumTransactionFee}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="*" component={ConfirmTransactionSwitch} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
ConfirmTokenTransactionSwitch.propTypes = {
|
||||
transaction: PropTypes.shape({
|
||||
origin: PropTypes.string,
|
||||
txParams: PropTypes.shape({
|
||||
data: PropTypes.string,
|
||||
to: PropTypes.string,
|
||||
from: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
};
|
@ -5,10 +5,7 @@ import Loading from '../../components/ui/loading-screen';
|
||||
import ConfirmTransactionSwitch from '../confirm-transaction-switch';
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base';
|
||||
import ConfirmSendEther from '../confirm-send-ether';
|
||||
import ConfirmSendToken from '../confirm-send-token';
|
||||
import ConfirmDeployContract from '../confirm-deploy-contract';
|
||||
import ConfirmApprove from '../confirm-approve';
|
||||
import ConfirmTokenTransactionBaseContainer from '../confirm-token-transaction-base';
|
||||
import ConfirmDecryptMessage from '../confirm-decrypt-message';
|
||||
import ConfirmEncryptionPublicKey from '../confirm-encryption-public-key';
|
||||
|
||||
@ -16,9 +13,6 @@ import {
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
CONFIRM_DEPLOY_CONTRACT_PATH,
|
||||
CONFIRM_SEND_ETHER_PATH,
|
||||
CONFIRM_SEND_TOKEN_PATH,
|
||||
CONFIRM_APPROVE_PATH,
|
||||
CONFIRM_TRANSFER_FROM_PATH,
|
||||
CONFIRM_TOKEN_METHOD_PATH,
|
||||
SIGNATURE_REQUEST_PATH,
|
||||
DECRYPT_MESSAGE_REQUEST_PATH,
|
||||
@ -31,6 +25,7 @@ import {
|
||||
addPollingTokenToAppState,
|
||||
removePollingTokenFromAppState,
|
||||
} from '../../store/actions';
|
||||
import ConfirmTokenTransactionSwitch from './confirm-token-transaction-switch';
|
||||
import ConfTx from './conf-tx';
|
||||
|
||||
export default class ConfirmTransaction extends Component {
|
||||
@ -49,7 +44,6 @@ export default class ConfirmTransaction extends Component {
|
||||
getContractMethodData: PropTypes.func,
|
||||
transactionId: PropTypes.string,
|
||||
paramsTransactionId: PropTypes.string,
|
||||
getTokenParams: PropTypes.func,
|
||||
isTokenMethodAction: PropTypes.bool,
|
||||
setDefaultHomeActiveTabName: PropTypes.func,
|
||||
};
|
||||
@ -74,12 +68,10 @@ export default class ConfirmTransaction extends Component {
|
||||
sendTo,
|
||||
history,
|
||||
mostRecentOverviewPage,
|
||||
transaction: { txParams: { data, to } = {} } = {},
|
||||
transaction: { txParams: { data } = {} } = {},
|
||||
getContractMethodData,
|
||||
transactionId,
|
||||
paramsTransactionId,
|
||||
getTokenParams,
|
||||
isTokenMethodAction,
|
||||
} = this.props;
|
||||
|
||||
getGasFeeEstimatesAndStartPolling().then((pollingToken) => {
|
||||
@ -100,9 +92,7 @@ export default class ConfirmTransaction extends Component {
|
||||
}
|
||||
|
||||
getContractMethodData(data);
|
||||
if (isTokenMethodAction) {
|
||||
getTokenParams(to);
|
||||
}
|
||||
|
||||
const txId = transactionId || paramsTransactionId;
|
||||
if (txId) {
|
||||
this.props.setTransactionToConfirm(txId);
|
||||
@ -154,23 +144,30 @@ export default class ConfirmTransaction extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { transactionId, paramsTransactionId } = this.props;
|
||||
const {
|
||||
transactionId,
|
||||
paramsTransactionId,
|
||||
isTokenMethodAction,
|
||||
transaction,
|
||||
} = this.props;
|
||||
|
||||
const validTransactionId =
|
||||
transactionId &&
|
||||
(!paramsTransactionId || paramsTransactionId === transactionId);
|
||||
|
||||
if (isTokenMethodAction && validTransactionId) {
|
||||
return <ConfirmTokenTransactionSwitch transaction={transaction} />;
|
||||
}
|
||||
// Show routes when state.confirmTransaction has been set and when either the ID in the params
|
||||
// isn't specified or is specified and matches the ID in state.confirmTransaction in order to
|
||||
// support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
|
||||
return transactionId &&
|
||||
(!paramsTransactionId || paramsTransactionId === transactionId) ? (
|
||||
return validTransactionId ? (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
|
||||
component={ConfirmDeployContract}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
|
||||
component={ConfirmTransactionBase}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
|
||||
@ -178,18 +175,8 @@ export default class ConfirmTransaction extends Component {
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
|
||||
component={ConfirmSendToken}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
|
||||
component={ConfirmApprove}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
|
||||
component={ConfirmTokenTransactionBaseContainer}
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
|
||||
component={ConfirmTransactionBase}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
|
@ -9,7 +9,6 @@ import { isTokenMethodAction } from '../../helpers/utils/transactions.util';
|
||||
|
||||
import {
|
||||
getContractMethodData,
|
||||
getTokenParams,
|
||||
setDefaultHomeActiveTabName,
|
||||
} from '../../store/actions';
|
||||
import { unconfirmedTransactionsListSelector } from '../../selectors';
|
||||
@ -54,7 +53,6 @@ const mapDispatchToProps = (dispatch) => {
|
||||
},
|
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
|
||||
getContractMethodData: (data) => dispatch(getContractMethodData(data)),
|
||||
getTokenParams: (tokenAddress) => dispatch(getTokenParams(tokenAddress)),
|
||||
setDefaultHomeActiveTabName: (tabName) =>
|
||||
dispatch(setDefaultHomeActiveTabName(tabName)),
|
||||
};
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
loadRelativeTimeFormatLocaleData,
|
||||
} from '../helpers/utils/i18n-helper';
|
||||
import { getMethodDataAsync } from '../helpers/utils/transactions.util';
|
||||
import { getSymbolAndDecimals } from '../helpers/utils/token-util';
|
||||
import switchDirection from '../helpers/utils/switch-direction';
|
||||
import {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
@ -22,7 +21,6 @@ import {
|
||||
getMetaMaskAccounts,
|
||||
getPermittedAccountsForCurrentTab,
|
||||
getSelectedAddress,
|
||||
getTokenList,
|
||||
} from '../selectors';
|
||||
import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
|
||||
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
|
||||
@ -2871,46 +2869,6 @@ export function loadingTokenParamsFinished() {
|
||||
};
|
||||
}
|
||||
|
||||
export function getTokenParams(address) {
|
||||
return (dispatch, getState) => {
|
||||
const tokenList = getTokenList(getState());
|
||||
const existingTokens = getState().metamask.tokens;
|
||||
const { selectedAddress } = getState().metamask;
|
||||
const { chainId } = getState().metamask.provider;
|
||||
const existingCollectibles = getState().metamask?.allCollectibles?.[
|
||||
selectedAddress
|
||||
]?.[chainId];
|
||||
const existingToken = existingTokens.find(({ address: tokenAddress }) =>
|
||||
isEqualCaseInsensitive(address, tokenAddress),
|
||||
);
|
||||
const existingCollectible = existingCollectibles?.find(
|
||||
({ address: collectibleAddress }) =>
|
||||
isEqualCaseInsensitive(address, collectibleAddress),
|
||||
);
|
||||
|
||||
if (existingCollectible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (existingToken) {
|
||||
return Promise.resolve({
|
||||
symbol: existingToken.symbol,
|
||||
decimals: existingToken.decimals,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(loadingTokenParamsStarted());
|
||||
log.debug(`loadingTokenParams`);
|
||||
|
||||
return getSymbolAndDecimals(address, tokenList).then(
|
||||
({ symbol, decimals }) => {
|
||||
dispatch(addToken(address, symbol, Number(decimals)));
|
||||
dispatch(loadingTokenParamsFinished());
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function setSeedPhraseBackedUp(seedPhraseBackupState) {
|
||||
return (dispatch) => {
|
||||
log.debug(`background.setSeedPhraseBackedUp`);
|
||||
|
Loading…
Reference in New Issue
Block a user