Page MenuHomePhabricator

No OneTemporary

diff --git a/web/cashtab/src/components/Home/TokenListItem.js b/web/cashtab/src/components/Home/TokenListItem.js
index d33322b17..32d1f778e 100644
--- a/web/cashtab/src/components/Home/TokenListItem.js
+++ b/web/cashtab/src/components/Home/TokenListItem.js
@@ -1,89 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
-import makeBlockie from 'ethereum-blockies-base64';
-import { Img } from 'react-image';
-import { currency } from '@components/Common/Ticker';
+import TokenIcon from '@components/Tokens/TokenIcon';
-const TokenIcon = styled.div`
- height: 32px;
- width: 32px;
+const TokenIconWrapper = styled.div`
margin-right: 10px;
`;
const TokenNameCtn = styled.div`
display: flex;
align-items: center;
`;
const Wrapper = styled.div`
display: flex;
align-items: center;
border-top: 1px solid rgba(255, 255, 255, 0.12);
color: ${props => props.theme.contrast};
padding: 10px 0;
justify-content: space-between;
h4 {
font-size: 16px;
color: ${props => props.theme.contrast};
margin: 0;
font-weight: bold;
}
:hover {
h4 {
color: ${props => props.theme.eCashPurple};
}
}
`;
const TokenListItem = ({ ticker, balance, tokenId }) => {
return (
<Wrapper>
<TokenNameCtn>
- <TokenIcon>
- {currency.tokenIconsUrl !== '' ? (
- <Img
- src={`${currency.tokenIconsUrl}/32/${tokenId}.png`}
- width={32}
- height={32}
- unloader={
- <img
- alt={`identicon of tokenId ${tokenId} `}
- height="32"
- width="32"
- style={{
- borderRadius: '50%',
- }}
- key={`identicon-${tokenId}`}
- src={makeBlockie(tokenId)}
- />
- }
- />
- ) : (
- <img
- alt={`identicon of tokenId ${tokenId} `}
- height="32"
- width="32"
- style={{
- borderRadius: '50%',
- }}
- key={`identicon-${tokenId}`}
- src={makeBlockie(tokenId)}
- />
- )}
- </TokenIcon>
+ <TokenIconWrapper>
+ <TokenIcon size={32} tokenId={tokenId} />
+ </TokenIconWrapper>
<h4>{ticker}</h4>
</TokenNameCtn>
<h4>{balance}</h4>
</Wrapper>
);
};
TokenListItem.propTypes = {
ticker: PropTypes.string,
balance: PropTypes.string,
tokenId: PropTypes.string,
};
export default TokenListItem;
diff --git a/web/cashtab/src/components/Home/Tx.js b/web/cashtab/src/components/Home/Tx.js
index f43b15f33..94f8de7d9 100644
--- a/web/cashtab/src/components/Home/Tx.js
+++ b/web/cashtab/src/components/Home/Tx.js
@@ -1,570 +1,544 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import {
SendIcon,
ReceiveIcon,
GenesisIcon,
UnparsedIcon,
} from '@components/Common/CustomIcons';
import { currency } from '@components/Common/Ticker';
-import makeBlockie from 'ethereum-blockies-base64';
-import { Img } from 'react-image';
import { fromLegacyDecimals } from '@utils/cashMethods';
import { formatBalance, formatDate } from '@utils/formatting';
+import TokenIcon from '@components/Tokens/TokenIcon';
const TxIcon = styled.div`
svg {
width: 20px;
height: 20px;
}
height: 40px;
width: 40px;
border: 1px solid #fff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100px;
`;
const SentTx = styled(TxIcon)`
svg {
margin-right: -3px;
}
fill: ${props => props.theme.contrast};
`;
const ReceivedTx = styled(TxIcon)`
svg {
fill: ${props => props.theme.eCashBlue};
}
border-color: ${props => props.theme.eCashBlue};
`;
const GenesisTx = styled(TxIcon)`
border-color: ${props => props.theme.genesisGreen};
svg {
fill: ${props => props.theme.genesisGreen};
}
`;
const UnparsedTx = styled(TxIcon)`
color: ${props => props.theme.eCashBlue} !important;
`;
const DateType = styled.div`
text-align: left;
padding: 12px;
@media screen and (max-width: 500px) {
font-size: 0.8rem;
}
`;
const LeftTextCtn = styled.div`
text-align: left;
display: flex;
align-items: left;
flex-direction: column;
margin-left: 10px;
h3 {
color: ${props => props.theme.contrast};
font-size: 14px;
font-weight: 700;
margin: 0;
}
.genesis {
color: ${props => props.theme.genesisGreen};
}
.received {
color: ${props => props.theme.eCashBlue};
}
h4 {
font-size: 12px;
color: ${props => props.theme.lightWhite};
margin: 0;
}
`;
const RightTextCtn = styled.div`
text-align: right;
display: flex;
align-items: left;
flex-direction: column;
margin-left: 10px;
h3 {
color: ${props => props.theme.contrast};
font-size: 14px;
font-weight: 700;
margin: 0;
}
.genesis {
color: ${props => props.theme.genesisGreen};
}
.received {
color: ${props => props.theme.eCashBlue};
}
h4 {
font-size: 12px;
color: ${props => props.theme.lightWhite};
margin: 0;
}
`;
const OpReturnType = styled.div`
text-align: right;
width: 100%;
padding: 10px;
border-radius: 5px;
background: ${props => props.theme.sentMessage};
margin-top: 15px;
h4 {
color: ${props => props.theme.lightWhite};
margin: 0;
font-size: 12px;
display: inline-block;
}
p {
color: ${props => props.theme.contrast};
margin: 0;
font-size: 14px;
margin-bottom: 10px;
overflow-wrap: break-word;
}
a {
color: ${props => props.theme.contrast};
margin: 0;
font-size: 10px;
border: 1px solid ${props => props.theme.contrast};
border-radius: 5px;
padding: 2px 10px;
opacity: 0.6;
}
a:hover {
opacity: 1;
border-color: ${props => props.theme.eCashBlue};
color: ${props => props.theme.contrast};
background: ${props => props.theme.eCashBlue};
}
${({ received, ...props }) =>
received &&
`
text-align: left;
background: ${props.theme.receivedMessage};
`}
`;
const SentLabel = styled.span`
font-weight: bold;
color: ${props => props.theme.secondary} !important;
`;
const ReceivedLabel = styled.span`
font-weight: bold;
color: ${props => props.theme.eCashBlue} !important;
`;
const GenesisLabel = styled.span`
font-weight: bold;
color: ${props => props.theme.genesisGreen} !important;
`;
const CashtabMessageLabel = styled.span`
text-align: left;
font-weight: bold;
color: ${props => props.theme.eCashBlue} !important;
white-space: nowrap;
`;
const EncryptionMessageLabel = styled.span`
font-weight: bold;
font-size: 12px;
color: ${props => props.theme.encryptionRed};
white-space: nowrap;
`;
const UnauthorizedDecryptionMessage = styled.span`
text-align: left;
color: ${props => props.theme.encryptionRed};
white-space: nowrap;
font-style: italic;
`;
const MessageLabel = styled.span`
text-align: left;
font-weight: bold;
color: ${props => props.theme.secondary} !important;
white-space: nowrap;
`;
const ReplyMessageLabel = styled.span`
color: ${props => props.theme.eCashBlue} !important;
`;
const TxInfo = styled.div`
text-align: right;
display: flex;
align-items: left;
flex-direction: column;
margin-left: 10px;
flex-grow: 2;
h3 {
color: ${props => props.theme.contrast};
font-size: 14px;
font-weight: 700;
margin: 0;
}
.genesis {
color: ${props => props.theme.genesisGreen};
}
.received {
color: ${props => props.theme.eCashBlue};
}
h4 {
font-size: 12px;
color: ${props => props.theme.lightWhite};
margin: 0;
}
@media screen and (max-width: 500px) {
font-size: 0.8rem;
}
`;
const TokenInfo = styled.div`
display: flex;
flex-grow: 1;
justify-content: flex-end;
color: ${props =>
props.outgoing ? props.theme.secondary : props.theme.eCashBlue};
@media screen and (max-width: 500px) {
font-size: 0.8rem;
grid-template-columns: 16px auto;
}
`;
const TxTokenIcon = styled.div`
img {
height: 24px;
width: 24px;
}
@media screen and (max-width: 500px) {
img {
height: 16px;
width: 16px;
}
}
grid-column-start: 1;
grid-column-end: span 1;
grid-row-start: 1;
grid-row-end: span 2;
align-self: center;
`;
const TokenTxAmt = styled.h3`
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const TokenName = styled.h4`
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const TxWrapper = styled.div`
display: flex;
align-items: center;
border-top: 1px solid rgba(255, 255, 255, 0.12);
color: ${props => props.theme.contrast};
padding: 10px 0;
flex-wrap: wrap;
`;
const Tx = ({ data, fiatPrice, fiatCurrency }) => {
const txDate =
typeof data.blocktime === 'undefined'
? formatDate()
: formatDate(data.blocktime, navigator.language);
// if data only includes height and txid, then the tx could not be parsed by cashtab
// render as such but keep link to block explorer
let unparsedTx = false;
if (!Object.keys(data).includes('outgoingTx')) {
unparsedTx = true;
}
return (
<>
{unparsedTx ? (
<TxWrapper>
<UnparsedTx>
<UnparsedIcon />
</UnparsedTx>
<DateType>
<ReceivedLabel>Unparsed</ReceivedLabel>
<br />
{txDate}
</DateType>
<TxInfo>Open in Explorer</TxInfo>
</TxWrapper>
) : (
<TxWrapper>
{data.outgoingTx ? (
<>
{data.tokenTx &&
data.tokenInfo.transactionType === 'GENESIS' ? (
<GenesisTx>
<GenesisIcon />
</GenesisTx>
) : (
<SentTx>
<SendIcon />
</SentTx>
)}
</>
) : (
<ReceivedTx>
<ReceiveIcon />
</ReceivedTx>
)}
<LeftTextCtn>
{data.outgoingTx ? (
<>
{data.tokenTx &&
data.tokenInfo.transactionType === 'GENESIS' ? (
<h3 className="genesis">Genesis</h3>
) : (
<h3 className="sent">Sent</h3>
)}
</>
) : (
<h3 className="received">Received</h3>
)}
<h4>{txDate}</h4>
</LeftTextCtn>
{data.tokenTx ? (
<TokenInfo outgoing={data.outgoingTx}>
{data.tokenTx && data.tokenInfo ? (
<>
<TxTokenIcon>
- {currency.tokenIconsUrl !== '' ? (
- <Img
- src={`${currency.tokenIconsUrl}/32/${data.tokenInfo.tokenId}.png`}
- unloader={
- <img
- alt={`identicon of tokenId ${data.tokenInfo.tokenId} `}
- style={{
- borderRadius: '50%',
- }}
- key={`identicon-${data.tokenInfo.tokenId}`}
- src={makeBlockie(
- data.tokenInfo
- .tokenId,
- )}
- />
- }
- />
- ) : (
- <img
- alt={`identicon of tokenId ${data.tokenInfo.tokenId} `}
- style={{
- borderRadius: '50%',
- }}
- key={`identicon-${data.tokenInfo.tokenId}`}
- src={makeBlockie(
- data.tokenInfo.tokenId,
- )}
- />
- )}
+ <TokenIcon
+ size={32}
+ tokenId={data.tokenInfo.tokenId}
+ />
</TxTokenIcon>
{data.outgoingTx ? (
<RightTextCtn>
{data.tokenInfo.transactionType ===
'GENESIS' ? (
<>
<TokenTxAmt className="genesis">
+{' '}
{data.tokenInfo.qtyReceived.toString()}
&nbsp;
{
data.tokenInfo
.tokenTicker
}
</TokenTxAmt>
<TokenName>
{
data.tokenInfo
.tokenName
}
</TokenName>
</>
) : (
<>
<TokenTxAmt>
-{' '}
{data.tokenInfo.qtySent.toString()}
&nbsp;
{
data.tokenInfo
.tokenTicker
}
</TokenTxAmt>
<TokenName>
{
data.tokenInfo
.tokenName
}
</TokenName>
</>
)}
</RightTextCtn>
) : (
<RightTextCtn>
<TokenTxAmt className="received">
+{' '}
{data.tokenInfo.qtyReceived.toString()}
&nbsp;
{data.tokenInfo.tokenTicker}
</TokenTxAmt>
<TokenName>
{data.tokenInfo.tokenName}
</TokenName>
</RightTextCtn>
)}
</>
) : (
<span>Token Tx</span>
)}
</TokenInfo>
) : (
<>
<TxInfo outgoing={data.outgoingTx}>
{data.outgoingTx ? (
<>
<h3>
-
{formatBalance(
fromLegacyDecimals(
data.amountSent,
),
)}{' '}
{currency.ticker}
</h3>
{fiatPrice !== null &&
!isNaN(data.amountSent) && (
<h4>
-
{
currency.fiatCurrencies[
fiatCurrency
].symbol
}
{(
fromLegacyDecimals(
data.amountSent,
) * fiatPrice
).toFixed(2)}{' '}
{
currency.fiatCurrencies
.fiatCurrency
}
</h4>
)}
</>
) : (
<>
<h3 className="received">
+
{formatBalance(
fromLegacyDecimals(
data.amountReceived,
),
)}{' '}
{currency.ticker}
</h3>
{fiatPrice !== null &&
!isNaN(data.amountReceived) && (
<h4>
+
{
currency.fiatCurrencies[
fiatCurrency
].symbol
}
{(
fromLegacyDecimals(
data.amountReceived,
) * fiatPrice
).toFixed(2)}{' '}
{
currency.fiatCurrencies
.fiatCurrency
}
</h4>
)}
</>
)}
</TxInfo>
</>
)}
{data.opReturnMessage && (
<>
<OpReturnType received={!data.outgoingTx}>
{data.isCashtabMessage ? (
<h4>Cashtab Message</h4>
) : (
<h4>External Message</h4>
)}
{data.isEncryptedMessage ? (
<EncryptionMessageLabel>
&nbsp;-&nbsp;Encrypted
</EncryptionMessageLabel>
) : (
''
)}
<br />
{/*unencrypted OP_RETURN Message*/}
{data.opReturnMessage &&
!data.isEncryptedMessage ? (
<p>{data.opReturnMessage}</p>
) : (
''
)}
{/*encrypted and wallet is authorized to view OP_RETURN Message*/}
{data.opReturnMessage &&
data.isEncryptedMessage &&
data.decryptionSuccess ? (
<p>{data.opReturnMessage}</p>
) : (
''
)}
{/*encrypted but wallet is not authorized to view OP_RETURN Message*/}
{data.opReturnMessage &&
data.isEncryptedMessage &&
!data.decryptionSuccess ? (
<UnauthorizedDecryptionMessage>
{data.opReturnMessage}
</UnauthorizedDecryptionMessage>
) : (
''
)}
{!data.outgoingTx && data.replyAddress ? (
<Link
to={{
pathname: `/send`,
state: {
replyAddress: data.replyAddress,
},
}}
>
Reply To Message
</Link>
) : (
''
)}
</OpReturnType>
</>
)}
</TxWrapper>
)}
</>
);
};
Tx.propTypes = {
data: PropTypes.object,
fiatPrice: PropTypes.number,
fiatCurrency: PropTypes.string,
};
export default Tx;
diff --git a/web/cashtab/src/components/Send/SendToken.js b/web/cashtab/src/components/Send/SendToken.js
index b23fd47ed..8e9da51dd 100644
--- a/web/cashtab/src/components/Send/SendToken.js
+++ b/web/cashtab/src/components/Send/SendToken.js
@@ -1,493 +1,453 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { WalletContext } from '@utils/context';
-import { Form, message, Row, Col, Alert, Descriptions, Popover } from 'antd';
+import { Form, message, Row, Col, Alert, Descriptions } from 'antd';
import PrimaryButton, {
SecondaryButton,
} from '@components/Common/PrimaryButton';
import {
DestinationAmount,
DestinationAddressSingle,
} from '@components/Common/EnhancedInputs';
import useBCH from '@hooks/useBCH';
import { SidePaddingCtn } from '@components/Common/Atoms';
import BalanceHeader from '@components/Common/BalanceHeader';
import { Redirect } from 'react-router-dom';
import useWindowDimensions from '@hooks/useWindowDimensions';
import { isMobile, isIOS, isSafari } from 'react-device-detect';
import { Img } from 'react-image';
import makeBlockie from 'ethereum-blockies-base64';
import BigNumber from 'bignumber.js';
import { currency, parseAddressForParams } from '@components/Common/Ticker.js';
import { Event } from '@utils/GoogleAnalytics';
import { getWalletState, toLegacyToken } from '@utils/cashMethods';
import ApiError from '@components/Common/ApiError';
import {
sendTokenNotification,
errorNotification,
} from '@components/Common/Notifications';
import { isValidXecAddress, isValidEtokenAddress } from '@utils/validation';
import { formatDate } from '@utils/formatting';
import styled, { css } from 'styled-components';
-
+import TokenIcon from '@components/Tokens/TokenIcon';
const AntdDescriptionsCss = css`
.ant-descriptions-item-label,
.ant-descriptions-item-content {
background-color: ${props => props.theme.contrast} !important;
color: ${props => props.theme.dropdownText};
}
`;
const AntdDescriptionsWrapper = styled.div`
${AntdDescriptionsCss}
`;
const SendToken = ({ tokenId, jestBCH, passLoadingStatus }) => {
const { wallet, apiError } = React.useContext(WalletContext);
const walletState = getWalletState(wallet);
const { tokens, slpBalancesAndUtxos } = walletState;
const token = tokens.find(token => token.tokenId === tokenId);
const [tokenStats, setTokenStats] = useState(null);
const [queryStringText, setQueryStringText] = useState(null);
const [sendTokenAddressError, setSendTokenAddressError] = useState(false);
const [sendTokenAmountError, setSendTokenAmountError] = useState(false);
// Get device window width
// If this is less than 769, the page will open with QR scanner open
const { width } = useWindowDimensions();
// Load with QR code open if device is mobile and NOT iOS + anything but safari
const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari);
const [formData, setFormData] = useState({
value: '',
address: '',
});
const { getBCH, getRestUrl, sendToken, getTokenStats } = useBCH();
// jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
const BCH = jestBCH ? jestBCH : getBCH();
// Fetch token stats if you do not have them and API did not return an error
if (tokenStats === null) {
getTokenStats(BCH, tokenId).then(
result => {
setTokenStats(result);
},
err => {
console.log(`Error getting token stats: ${err}`);
},
);
}
// Clears address and amount fields following sendTokenNotification
const clearInputForms = () => {
setFormData({
value: '',
address: '',
});
};
async function submit() {
setFormData({
...formData,
});
if (
!formData.address ||
!formData.value ||
Number(formData.value <= 0) ||
sendTokenAmountError
) {
return;
}
// Event("Category", "Action", "Label")
// Track number of SLPA send transactions and
// SLPA token IDs
Event('SendToken.js', 'Send', tokenId);
passLoadingStatus(true);
const { address, value } = formData;
// Clear params from address
let cleanAddress = address.split('?')[0];
// Convert to simpleledger prefix if etoken
cleanAddress = toLegacyToken(cleanAddress);
try {
const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, {
tokenId: tokenId,
tokenReceiverAddress: cleanAddress,
amount: value,
});
sendTokenNotification(link);
clearInputForms();
} catch (e) {
passLoadingStatus(false);
let message;
if (!e.error && !e.message) {
message = `Transaction failed: no response from ${getRestUrl()}.`;
} else if (
/Could not communicate with full node or other external service/.test(
e.error,
)
) {
message = 'Could not communicate with API. Please try again.';
} else {
message = e.message || e.error || JSON.stringify(e);
}
errorNotification(e, message, 'Sending eToken');
}
}
const handleSlpAmountChange = e => {
let error = false;
const { value, name } = e.target;
// test if exceeds balance using BigNumber
let isGreaterThanBalance = false;
if (!isNaN(value)) {
const bigValue = new BigNumber(value);
// Returns 1 if greater, -1 if less, 0 if the same, null if n/a
isGreaterThanBalance = bigValue.comparedTo(token.balance);
}
// Validate value for > 0
if (isNaN(value)) {
error = 'Amount must be a number';
} else if (value <= 0) {
error = 'Amount must be greater than 0';
} else if (token && token.balance && isGreaterThanBalance === 1) {
error = `Amount cannot exceed your ${token.info.tokenTicker} balance of ${token.balance}`;
} else if (!isNaN(value) && value.toString().includes('.')) {
if (value.toString().split('.')[1].length > token.info.decimals) {
error = `This token only supports ${token.info.decimals} decimal places`;
}
}
setSendTokenAmountError(error);
setFormData(p => ({
...p,
[name]: value,
}));
};
const handleTokenAddressChange = e => {
const { value, name } = e.target;
// validate for token address
// validate for parameters
// show warning that query strings are not supported
let error = false;
let addressString = value;
const isValid = isValidEtokenAddress(addressString);
const addressInfo = parseAddressForParams(addressString);
/*
Model
addressInfo =
{
address: '',
queryString: '',
amount: null,
};
*/
const { address, queryString } = addressInfo;
// If query string,
// Show an alert that only amount and currency.ticker are supported
setQueryStringText(queryString);
// Is this valid address?
if (!isValid) {
error = 'Address is not a valid etoken: address';
// If valid address but xec format
if (isValidXecAddress(address)) {
error = `Cashtab does not support sending eTokens to XEC addresses. Please convert to an eToken address.`;
}
}
setSendTokenAddressError(error);
setFormData(p => ({
...p,
[name]: value,
}));
};
const onMax = async () => {
// Clear this error before updating field
setSendTokenAmountError(false);
try {
let value = token.balance;
setFormData({
...formData,
value,
});
} catch (err) {
console.log(`Error in onMax:`);
console.log(err);
message.error(
'Unable to calculate the max value due to network errors',
);
}
};
useEffect(() => {
// If the balance has changed, unlock the UI
// This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked
passLoadingStatus(false);
}, [token]);
return (
<>
{!token && <Redirect to="/" />}
{token && (
<SidePaddingCtn>
<BalanceHeader
balance={token.balance}
ticker={token.info.tokenTicker}
/>
<Row type="flex">
<Col span={24}>
<Form
style={{
width: 'auto',
}}
>
<DestinationAddressSingle
loadWithCameraOpen={scannerSupported}
validateStatus={
sendTokenAddressError ? 'error' : ''
}
help={
sendTokenAddressError
? sendTokenAddressError
: ''
}
onScan={result =>
handleTokenAddressChange({
target: {
name: 'address',
value: result,
},
})
}
inputProps={{
placeholder: `${currency.tokenTicker} Address`,
name: 'address',
onChange: e =>
handleTokenAddressChange(e),
required: true,
value: formData.address,
}}
/>
<DestinationAmount
validateStatus={
sendTokenAmountError ? 'error' : ''
}
help={
sendTokenAmountError
? sendTokenAmountError
: ''
}
onMax={onMax}
inputProps={{
name: 'value',
step: 1 / 10 ** token.info.decimals,
placeholder: 'Amount',
prefix:
currency.tokenIconsUrl !== '' ? (
<Img
src={`${currency.tokenIconsUrl}/32/${tokenId}.png`}
width={16}
height={16}
unloader={
<img
alt={`identicon of tokenId ${tokenId} `}
heigh="16"
width="16"
style={{
borderRadius:
'50%',
}}
key={`identicon-${tokenId}`}
src={makeBlockie(
tokenId,
)}
/>
}
/>
) : (
<img
alt={`identicon of tokenId ${tokenId} `}
heigh="16"
width="16"
style={{
borderRadius: '50%',
}}
key={`identicon-${tokenId}`}
src={makeBlockie(tokenId)}
/>
),
suffix: token.info.tokenTicker,
onChange: e => handleSlpAmountChange(e),
required: true,
value: formData.value,
}}
/>
<div
style={{
paddingTop: '12px',
}}
>
{apiError ||
sendTokenAmountError ||
sendTokenAddressError ? (
<>
<SecondaryButton>
Send {token.info.tokenName}
</SecondaryButton>
</>
) : (
<PrimaryButton onClick={() => submit()}>
Send {token.info.tokenName}
</PrimaryButton>
)}
</div>
{queryStringText && (
<Alert
message={`You are sending a transaction to an address including query parameters "${queryStringText}." Token transactions do not support query parameters and they will be ignored.`}
type="warning"
/>
)}
{apiError && <ApiError />}
</Form>
{tokenStats !== null && (
<AntdDescriptionsWrapper>
<Descriptions
column={1}
bordered
title={`Token info for "${token.info.tokenName}"`}
>
<Descriptions.Item label="Icon">
- {currency.tokenIconsUrl !== '' ? (
- <Popover
- style={{ width: 256 }}
- content={
- <Img
- src={`${currency.tokenIconsUrl}/256/${tokenId}.png`}
- />
- }
- trigger="click"
- color="transparent"
- >
- <Img
- src={`${currency.tokenIconsUrl}/32/${tokenId}.png`}
- width={32}
- height={32}
- unloader={
- <img
- alt={`identicon of tokenId ${tokenId} `}
- height="32"
- width="32"
- style={{
- borderRadius:
- '50%',
- }}
- key={`identicon-${tokenId}`}
- src={makeBlockie(
- tokenId,
- )}
- />
- }
- />
- </Popover>
- ) : (
- <img
- alt={`identicon of tokenId ${tokenId} `}
- height="32"
- width="32"
- style={{
- borderRadius: '50%',
- }}
- key={`identicon-${tokenId}`}
- src={makeBlockie(tokenId)}
- />
- )}
+ <TokenIcon
+ size={64}
+ tokenId={tokenId}
+ />
</Descriptions.Item>
<Descriptions.Item label="Decimals">
{token.info.decimals}
</Descriptions.Item>
<Descriptions.Item label="Token ID">
{token.tokenId}
</Descriptions.Item>
{tokenStats && (
<>
<Descriptions.Item label="Document URI">
{tokenStats.documentUri}
</Descriptions.Item>
<Descriptions.Item label="Genesis Date">
{tokenStats.timestampUnix !==
null
? formatDate(
tokenStats.timestampUnix,
navigator.language,
)
: 'Just now (Genesis tx confirming)'}
</Descriptions.Item>
<Descriptions.Item label="Fixed Supply?">
{tokenStats.containsBaton
? 'No'
: 'Yes'}
</Descriptions.Item>
<Descriptions.Item label="Initial Quantity">
{tokenStats.initialTokenQty.toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="Total Burned">
{tokenStats.totalBurned.toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="Total Minted">
{tokenStats.totalMinted.toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="Circulating Supply">
{tokenStats.circulatingSupply.toLocaleString()}
</Descriptions.Item>
</>
)}
</Descriptions>
</AntdDescriptionsWrapper>
)}
</Col>
</Row>
</SidePaddingCtn>
)}
</>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in SendToken.test.js
status => {console.log(status)} is an arbitrary stub function
*/
SendToken.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
SendToken.propTypes = {
tokenId: PropTypes.string,
jestBCH: PropTypes.object,
passLoadingStatus: PropTypes.func,
};
export default SendToken;
diff --git a/web/cashtab/src/components/Tokens/TokenIcon.js b/web/cashtab/src/components/Tokens/TokenIcon.js
new file mode 100644
index 000000000..84515a42b
--- /dev/null
+++ b/web/cashtab/src/components/Tokens/TokenIcon.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import makeBlockie from 'ethereum-blockies-base64';
+import { Img } from 'react-image';
+import { currency } from '@components/Common/Ticker';
+
+const TokenIcon = ({ size, tokenId }) => {
+ return (
+ <>
+ <Img
+ src={`${currency.tokenIconsUrl}/${size}/${tokenId}.png`}
+ width={size}
+ height={size}
+ unloader={
+ <img
+ alt={`identicon of tokenId ${tokenId} `}
+ height="32"
+ width="32"
+ style={{
+ borderRadius: '50%',
+ }}
+ key={`identicon-${tokenId}`}
+ src={makeBlockie(tokenId)}
+ />
+ }
+ />
+ </>
+ );
+};
+TokenIcon.propTypes = {
+ size: PropTypes.oneOf([32, 64, 128, 256, 512]),
+ tokenId: PropTypes.string,
+};
+
+export default TokenIcon;

File Metadata

Mime Type
text/x-diff
Expires
Sun, Apr 27, 10:38 (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573312
Default Alt Text
(50 KB)

Event Timeline