Changeset View
Changeset View
Standalone View
Standalone View
cashtab/src/components/Send/SendXec.tsx
| Show All 12 Lines | |||||
| import { Capacitor } from '@capacitor/core'; | import { Capacitor } from '@capacitor/core'; | ||||
| import { App as CapacitorApp } from '@capacitor/app'; | import { App as CapacitorApp } from '@capacitor/app'; | ||||
| import { WalletContext, isWalletContextLoaded } from 'wallet/context'; | import { WalletContext, isWalletContextLoaded } from 'wallet/context'; | ||||
| import Modal from 'components/Common/Modal'; | import Modal from 'components/Common/Modal'; | ||||
| import PrimaryButton, { | import PrimaryButton, { | ||||
| SecondaryButton, | SecondaryButton, | ||||
| CopyIconButton, | CopyIconButton, | ||||
| } from 'components/Common/Buttons'; | } from 'components/Common/Buttons'; | ||||
| import ActionButtonRow from 'components/Common/ActionButtonRow'; | |||||
| import { | import { | ||||
| toSatoshis, | toSatoshis, | ||||
| toXec, | toXec, | ||||
| SlpDecimals, | SlpDecimals, | ||||
| undecimalizeTokenAmount, | undecimalizeTokenAmount, | ||||
| } from 'wallet'; | } from 'wallet'; | ||||
| import { sumOneToManyXec, confirmRawTx, getFirmaRedeemFee } from './helpers'; | import { sumOneToManyXec, confirmRawTx, getFirmaRedeemFee } from './helpers'; | ||||
| import { Event } from 'components/Common/GoogleAnalytics'; | import { Event } from 'components/Common/GoogleAnalytics'; | ||||
| import { | import { | ||||
| isValidMultiSendUserInput, | isValidMultiSendUserInput, | ||||
| shouldSendXecBeDisabled, | shouldSendXecBeDisabled, | ||||
| parseAddressInput, | parseAddressInput, | ||||
| isValidXecSendAmount, | isValidXecSendAmount, | ||||
| getOpReturnRawError, | getOpReturnRawError, | ||||
| CashtabParsedAddressInfo, | CashtabParsedAddressInfo, | ||||
| isValidTokenSendOrBurnAmount, | isValidTokenSendOrBurnAmount, | ||||
| } from 'validation'; | } from 'validation'; | ||||
| import { | import { Alert, AlertMsg, Info } from 'components/Common/Atoms'; | ||||
| ConvertAmount, | |||||
| Alert, | |||||
| AlertMsg, | |||||
| Info, | |||||
| TokenIdPreview, | |||||
| } from 'components/Common/Atoms'; | |||||
| import { getMultisendTargetOutputs } from 'helpers'; | import { getMultisendTargetOutputs } from 'helpers'; | ||||
| import { ChronikClient } from 'chronik-client'; | import { ChronikClient } from 'chronik-client'; | ||||
| import { | import { | ||||
| getCashtabMsgTargetOutput, | getCashtabMsgTargetOutput, | ||||
| getAirdropTargetOutput, | getAirdropTargetOutput, | ||||
| getCashtabMsgByteCount, | getCashtabMsgByteCount, | ||||
| getOpreturnParamTargetOutput, | getOpreturnParamTargetOutput, | ||||
| parseOpReturnRaw, | parseOpReturnRaw, | ||||
| parseFirma, | parseFirma, | ||||
| parseEmppRaw, | parseEmppRaw, | ||||
| parseInputDataRaw, | parseInputDataRaw, | ||||
| ParsedOpReturnRaw, | ParsedOpReturnRaw, | ||||
| } from 'opreturn'; | } from 'opreturn'; | ||||
| import ApiError from 'components/Common/ApiError'; | import ApiError from 'components/Common/ApiError'; | ||||
| import { formatFiatBalance, formatBalance } from 'formatting'; | import { formatFiatBalance, formatBalance } from 'formatting'; | ||||
| import styled from 'styled-components'; | import styled from 'styled-components'; | ||||
| import { theme } from 'assets/styles/theme'; | |||||
| import { opReturn as opreturnConfig } from 'config/opreturn'; | import { opReturn as opreturnConfig } from 'config/opreturn'; | ||||
| import { explorer } from 'config/explorer'; | import { explorer } from 'config/explorer'; | ||||
| import { supportedFiatCurrencies } from 'config/CashtabSettings'; | import { supportedFiatCurrencies } from 'config/CashtabSettings'; | ||||
| import appConfig from 'config/app'; | import appConfig from 'config/app'; | ||||
| import { getUserLocale } from 'helpers'; | import { getUserLocale } from 'helpers'; | ||||
| import { fiatToSatoshis } from 'wallet'; | import { fiatToSatoshis } from 'wallet'; | ||||
| import { toast } from 'react-toastify'; | import { toast } from 'react-toastify'; | ||||
| import { | import { | ||||
| InputWithScanner, | InputWithScanner, | ||||
| SendXecInput, | SendXecInput, | ||||
| SendTokenInput, | SendTokenInput, | ||||
| Input, | Input, | ||||
| TextArea, | TextArea, | ||||
| } from 'components/Common/Inputs'; | } from 'components/Common/Inputs'; | ||||
| import Switch from 'components/Common/Switch'; | |||||
| import { opReturn } from 'config/opreturn'; | import { opReturn } from 'config/opreturn'; | ||||
| import { Script, payment, fromHex, TokenType, strToBytes } from 'ecash-lib'; | import { Script, payment, fromHex, TokenType, strToBytes } from 'ecash-lib'; | ||||
| import { isValidCashAddress } from 'ecashaddrjs'; | import { isValidCashAddress } from 'ecashaddrjs'; | ||||
| import { CashtabCachedTokenInfo } from 'config/CashtabCache'; | import { CashtabCachedTokenInfo } from 'config/CashtabCache'; | ||||
| import TokenIcon from 'components/Etokens/TokenIcon'; | import TokenIcon from 'components/Etokens/TokenIcon'; | ||||
| import { ExtendedCashtabCachedTokenInfo } from 'components/Etokens/TokenListItem'; | import { ExtendedCashtabCachedTokenInfo } from 'components/Etokens/TokenListItem'; | ||||
| import { TokenInfoKv } from 'components/Etokens/TokenList'; | import { TokenInfoKv } from 'components/Etokens/TokenList'; | ||||
| import { getTokenGenesisInfo } from 'chronik'; | import { getTokenGenesisInfo } from 'chronik'; | ||||
| Show All 15 Lines | |||||
| } from './styled'; | } from './styled'; | ||||
| import { | import { | ||||
| FIRMA, | FIRMA, | ||||
| FIRMA_REDEEM_ADDRESS, | FIRMA_REDEEM_ADDRESS, | ||||
| FIRMA_REDEEM_EMPP_RAW_LENGTH, | FIRMA_REDEEM_EMPP_RAW_LENGTH, | ||||
| } from 'constants/tokens'; | } from 'constants/tokens'; | ||||
| import { FirmaIcon, TetherIcon } from 'components/Common/CustomIcons'; | import { FirmaIcon, TetherIcon } from 'components/Common/CustomIcons'; | ||||
| import Burst from 'assets/burst.png'; | import Burst from 'assets/burst.png'; | ||||
| import { ReactComponent as DropDownArrowIcon } from 'assets/drop-down-arrow.svg'; | |||||
| const OuterCtn = styled.div` | const OuterCtn = styled.div` | ||||
| background: ${props => props.theme.primaryBackground}; | |||||
| padding: 20px; | |||||
| border-radius: 10px; | |||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| min-height: calc(100vh - 250px); | min-height: calc(100vh - 250px); | ||||
| @media (max-width: 768px) { | @media (max-width: 768px) { | ||||
| min-height: calc(100vh - 300px); | min-height: calc(100vh - 300px); | ||||
| padding: 0 12px; | |||||
| } | |||||
| `; | |||||
| const SendContentWrapper = styled.div` | |||||
| width: 100%; | |||||
| padding: 0 16px; | |||||
| margin: 0 auto; | |||||
| margin-top: 40px; | |||||
| box-sizing: border-box; | |||||
| @media (max-width: 768px) { | |||||
| padding: 0 0; | |||||
| margin-top: 10px; | |||||
| } | } | ||||
| `; | `; | ||||
| const SendXecForm = styled.div` | const SendXecForm = styled.div` | ||||
| margin: 12px 0; | margin: 12px 0; | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| gap: 12px; | gap: 12px; | ||||
| flex-grow: 1; | flex-grow: 1; | ||||
| `; | `; | ||||
| const SendXecRow = styled.div``; | const SendXecRow = styled.div``; | ||||
| const SwitchAndLabel = styled.div` | |||||
| const AdvancedSection = styled.div` | |||||
| margin: 12px 0; | |||||
| width: 100%; | |||||
| `; | |||||
| const AdvancedHeader = styled.button` | |||||
| display: flex; | display: flex; | ||||
| flex-direction: row; | |||||
| align-items: center; | align-items: center; | ||||
| gap: 12px; | gap: 2px; | ||||
| `; | width: 100%; | ||||
| const SwitchLabel = styled.div` | background: none; | ||||
| padding: 12px 0; | |||||
| border: none; | |||||
| color: ${props => props.theme.primaryText}; | color: ${props => props.theme.primaryText}; | ||||
| font-size: var(--text-base); | |||||
| line-height: var(--text-base--line-height); | |||||
| cursor: pointer; | |||||
| font-weight: 700; | |||||
| text-align: left; | |||||
| transition: color 0.2s ease; | |||||
| :focus-visible { | |||||
| outline: none; | |||||
| } | |||||
| :hover { | |||||
| color: ${props => props.theme.accent}; | |||||
| } | |||||
| `; | `; | ||||
| const SwitchContainer = styled.div` | const AdvancedButtonsRow = styled.div` | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | gap: 8px; | ||||
| justify-content: flex-start; | margin-bottom: 12px; | ||||
| @media (max-width: 768px) { | |||||
| gap: 6px; | |||||
| } | |||||
| `; | |||||
| const AdvancedButton = styled.button<{ $active?: boolean }>` | |||||
| flex: 1; | |||||
| padding: 14px 4px; | |||||
| border-radius: 8px; | |||||
| border: none; | |||||
| background-color: ${props => | |||||
| props.$active ? props.theme.accent : props.theme.inputBackground}; | |||||
| color: ${props => props.theme.primaryText}; | color: ${props => props.theme.primaryText}; | ||||
| white-space: nowrap; | font-size: var(--text-sm); | ||||
| margin: 12px 0; | line-height: 1em; | ||||
| cursor: pointer; | |||||
| transition: | |||||
| background-color 0.2s ease, | |||||
| border-color 0.2s ease; | |||||
| :hover:not(:disabled) { | |||||
| background-color: ${props => | |||||
| props.$active ? props.theme.accent : 'rgba(255, 255, 255, 0.2)'}; | |||||
| } | |||||
| :disabled { | |||||
| cursor: not-allowed; | |||||
| opacity: 0.6; | |||||
| } | |||||
| `; | |||||
| const AdvancedContent = styled.div` | |||||
| margin-top: 0; | |||||
| `; | |||||
| const AdvancedChevron = styled(DropDownArrowIcon)<{ $open?: boolean }>` | |||||
| width: 18px; | |||||
| height: 18px; | |||||
| flex-shrink: 0; | |||||
| fill: currentColor; | |||||
| margin-left: 4px; | |||||
| transform: ${props => (props.$open ? 'rotate(180deg)' : 'none')}; | |||||
| transition: fill 0.2s ease; | |||||
| path { | |||||
| fill: currentColor; | |||||
| } | |||||
| `; | `; | ||||
| const AmountPreviewCtn = styled.div` | const AmountPreviewCtn = styled.div` | ||||
| margin: 12px; | margin: 12px 0; | ||||
| display: flex; | |||||
| flex-direction: row; | |||||
| align-items: flex-start; | |||||
| justify-content: space-between; | |||||
| gap: 12px; | |||||
| border-top: 1px solid ${props => props.theme.border}; | |||||
| padding-top: 20px; | |||||
| `; | |||||
| const AmountPreviewLabel = styled.div` | |||||
| color: ${props => props.theme.primaryText}; | |||||
| font-size: var(--text-lg); | |||||
| font-weight: 700; | |||||
| flex-shrink: 0; | |||||
| `; | |||||
| const AmountPreviewValues = styled.div` | |||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| justify-content: center; | align-items: flex-end; | ||||
| text-align: right; | |||||
| min-width: 0; | |||||
| `; | |||||
| const AmountPreviewFiat = styled.div` | |||||
| color: ${props => props.theme.secondaryText}; | |||||
| font-size: var(--text-sm); | |||||
| line-height: var(--text-sm--line-height); | |||||
| `; | `; | ||||
| const ParsedBip21InfoRow = styled.div` | const ParsedBip21InfoRow = styled.div` | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| word-break: break-word; | word-break: break-word; | ||||
| margin-bottom: 12px; | |||||
| `; | `; | ||||
| const ParsedBip21InfoLabel = styled.div` | const ParsedBip21InfoLabel = styled.div` | ||||
| color: ${props => props.theme.primaryText}; | color: ${props => props.theme.primaryText}; | ||||
| text-align: left; | text-align: left; | ||||
| width: 100%; | width: 100%; | ||||
| `; | `; | ||||
| const ParsedBip21Info = styled.div` | const ParsedBip21Info = styled.div` | ||||
| background-color: #fff2f0; | background-color: #fff2f0; | ||||
| border-radius: 12px; | border-radius: 12px; | ||||
| color: ${props => props.theme.accent}; | color: ${props => props.theme.accent}; | ||||
| padding: 12px; | padding: 12px; | ||||
| text-align: left; | text-align: left; | ||||
| `; | `; | ||||
| const LocaleFormattedValue = styled.div` | const LocaleFormattedValue = styled.div` | ||||
| color: ${props => props.theme.primaryText}; | color: ${props => props.theme.primaryText}; | ||||
| font-weight: bold; | font-weight: bold; | ||||
| font-size: var(--text-lg); | font-size: var(--text-lg); | ||||
| line-height: var(--text-lg--line-height); | line-height: var(--text-lg--line-height); | ||||
| margin-bottom: 0; | margin-bottom: 2px; | ||||
| `; | `; | ||||
| const SendToOneHolder = styled.div``; | const SendToOneHolder = styled.div``; | ||||
| const SendToManyHolder = styled.div``; | const SendToManyHolder = styled.div``; | ||||
| const SendToOneInputForm = styled.div` | const SendToOneInputForm = styled.div` | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| gap: 12px; | gap: 12px; | ||||
| `; | `; | ||||
| const InputModesHolder = styled.div<{ open: boolean }>` | const InputModesHolder = styled.div<{ open: boolean }>` | ||||
| min-height: 9rem; | min-height: 9rem; | ||||
| ${SendToOneHolder} { | ${SendToOneHolder} { | ||||
| overflow: hidden; | overflow: hidden; | ||||
| transition: ${props => | transition: ${props => | ||||
| props.open | props.open | ||||
| ? 'max-height 200ms ease-in, opacity 200ms ease-out' | ? 'max-height 200ms ease-in, opacity 200ms ease-out' | ||||
| : 'max-height 200ms cubic-bezier(0, 1, 0, 1), opacity 200ms ease-in'}; | : 'max-height 200ms cubic-bezier(0, 1, 0, 1), opacity 200ms ease-in'}; | ||||
| max-height: ${props => (props.open ? '0rem' : '12rem')}; | max-height: ${props => (props.open ? '0rem' : '20rem')}; | ||||
| opacity: ${props => (props.open ? 0 : 1)}; | opacity: ${props => (props.open ? 0 : 1)}; | ||||
| } | } | ||||
| ${SendToManyHolder} { | ${SendToManyHolder} { | ||||
| overflow: hidden; | overflow: hidden; | ||||
| transition: ${props => | transition: ${props => | ||||
| props.open | props.open | ||||
| ? 'max-height 200ms ease-in, transform 200ms ease-out, opacity 200ms ease-in' | ? 'max-height 200ms ease-in, transform 200ms ease-out, opacity 200ms ease-in' | ||||
| : 'max-height 200ms cubic-bezier(0, 1, 0, 1), transform 200ms ease-out'}; | : 'max-height 200ms cubic-bezier(0, 1, 0, 1), transform 200ms ease-out'}; | ||||
| max-height: ${props => (props.open ? '12rem' : '0rem')}; | max-height: ${props => (props.open ? '20rem' : '0rem')}; | ||||
| transform: ${props => | transform: ${props => | ||||
| props.open ? 'translateY(0%)' : 'translateY(100%)'}; | props.open ? 'translateY(0%)' : 'translateY(100%)'}; | ||||
| opacity: ${props => (props.open ? 1 : 0)}; | opacity: ${props => (props.open ? 1 : 0)}; | ||||
| } | } | ||||
| `; | `; | ||||
| const ParsedTokenSend = styled.div` | const ParsedTokenSend = styled.div` | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| Show All 27 Lines | const TokenSelectDropdown = styled.div` | ||||
| position: relative; | position: relative; | ||||
| width: 100%; | width: 100%; | ||||
| `; | `; | ||||
| const TokenSelectInputWrapper = styled.div` | const TokenSelectInputWrapper = styled.div` | ||||
| position: relative; | position: relative; | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| flex-direction: column; | |||||
| width: 100%; | width: 100%; | ||||
| `; | `; | ||||
| const TokenSelectClearButton = styled.button` | const TokenSelectClearButton = styled.button` | ||||
| position: absolute; | |||||
| right: 12px; | |||||
| background: transparent; | background: transparent; | ||||
| border: none; | border: none; | ||||
| color: ${props => props.theme.secondaryText}; | color: ${props => props.theme.secondaryText}; | ||||
| cursor: pointer; | cursor: pointer; | ||||
| font-size: 20px; | font-size: 20px; | ||||
| padding: 4px 8px; | padding: 0 0 0 18px; | ||||
| height: 100%; | |||||
| border-left: 1px solid ${props => props.theme.primaryBackground}; | |||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| justify-content: center; | justify-content: center; | ||||
| z-index: 10; | z-index: 10; | ||||
| &:hover { | &:hover { | ||||
| color: ${props => props.theme.primaryText}; | color: ${props => props.theme.primaryText}; | ||||
| } | } | ||||
| @media (max-width: 768px) { | |||||
| font-size: 16px; | |||||
| padding: 0 0 0 12px; | |||||
| } | |||||
| `; | `; | ||||
| const SelectedTokenDisplay = styled.div` | const SelectedTokenDisplay = styled.div` | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| gap: 12px; | gap: 12px; | ||||
| padding: 16px 12px; | padding: 0px 18px 0 12px; | ||||
| background-color: ${props => props.theme.secondaryBackground}; | background-color: ${props => props.theme.inputBackground}; | ||||
| border: 1px solid ${props => props.theme.border}; | border-radius: 10px; | ||||
| border-radius: 9px; | |||||
| width: 100%; | width: 100%; | ||||
| position: relative; | position: relative; | ||||
| height: 80px; | |||||
| margin-bottom: 18px; | |||||
| @media (max-width: 768px) { | |||||
| height: 70px; | |||||
| margin-bottom: 14px; | |||||
| padding: 0 12px 0 6px; | |||||
| } | |||||
| `; | `; | ||||
| const SelectedTokenInfo = styled.div` | const SelectedTokenInfo = styled.div` | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| gap: 12px; | gap: 12px; | ||||
| flex: 1; | flex: 1; | ||||
| `; | `; | ||||
| const SelectedTokenText = styled.div` | const SelectedTokenText = styled.div` | ||||
| color: ${props => props.theme.primaryText}; | color: ${props => props.theme.primaryText}; | ||||
| font-size: var(--text-lg); | font-size: var(--text-base); | ||||
| line-height: var(--text-lg--line-height); | font-weight: 700; | ||||
| `; | `; | ||||
| const SelectedTokenDetails = styled.div` | const SelectedTokenDetails = styled.div` | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | align-items: center; | ||||
| justify-content: space-between; | |||||
| gap: 4px; | gap: 4px; | ||||
| flex: 1; | flex: 1; | ||||
| text-align: left; | |||||
| `; | `; | ||||
| const SelectedTokenBalance = styled.div` | const SelectedTokenBalance = styled.div` | ||||
| color: ${props => props.theme.secondaryText}; | color: ${props => props.theme.primaryText}; | ||||
| font-size: var(--text-base); | |||||
| font-weight: 700; | |||||
| @media (max-width: 768px) { | |||||
| font-size: var(--text-sm); | font-size: var(--text-sm); | ||||
| line-height: var(--text-sm--line-height); | } | ||||
| `; | |||||
| const SendingTokenLabel = styled.div` | |||||
| color: ${props => props.theme.primaryText}; | |||||
| font-size: var(--text-lg); | |||||
| font-weight: 700; | |||||
| margin-bottom: 8px; | |||||
| text-align: left; | |||||
| `; | `; | ||||
| const SelectedTokenIdWrapper = styled.div` | const SelectedTokenIdWrapper = styled.div` | ||||
| font-size: var(--text-xs); | font-size: 12px; | ||||
| line-height: var(--text-xs--line-height); | color: ${props => props.theme.secondaryText}; | ||||
| `; | `; | ||||
| const TokenDropdownList = styled.div` | const TokenDropdownList = styled.div` | ||||
| position: absolute; | width: 100%; | ||||
| top: 100%; | background: ${props => props.theme.inputBackground}; | ||||
| left: 0; | border-radius: 10px; | ||||
| right: 0; | |||||
| background: ${props => props.theme.secondaryBackground}; | |||||
| border: 1px solid ${props => props.theme.border}; | |||||
| border-radius: 8px; | |||||
| max-height: 300px; | max-height: 300px; | ||||
| overflow-y: auto; | overflow-y: auto; | ||||
| z-index: 1000; | z-index: 1000; | ||||
| margin-top: 4px; | margin-top: 0px; | ||||
| `; | `; | ||||
| const TokenDropdownItem = styled.div` | const TokenDropdownItem = styled.div` | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: flex-start; | ||||
| padding: 12px; | padding: 12px; | ||||
| cursor: pointer; | cursor: pointer; | ||||
| border-bottom: 1px solid ${props => props.theme.border}; | border-bottom: 1px solid ${props => props.theme.primaryBackground}; | ||||
| &:hover { | &:hover { | ||||
| background: ${props => props.theme.primaryBackground}; | background: ${props => props.theme.accent}; | ||||
| } | } | ||||
| &:last-child { | &:last-child { | ||||
| border-bottom: none; | border-bottom: none; | ||||
| } | } | ||||
| `; | `; | ||||
| const TokenDropdownItemContent = styled.div` | const TokenDropdownItemContent = styled.div` | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| gap: 12px; | gap: 16px; | ||||
| flex: 1; | flex: 1; | ||||
| `; | `; | ||||
| const TokenDropdownItemInfo = styled.div` | const TokenDropdownItemInfo = styled.div` | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | |||||
| flex: 1; | flex: 1; | ||||
| align-items: center; | |||||
| justify-content: space-between; | |||||
| text-align: left; | |||||
| `; | `; | ||||
| const TokenDropdownItemTicker = styled.div` | const TokenDropdownItemTicker = styled.div` | ||||
| font-weight: bold; | font-weight: bold; | ||||
| color: ${props => props.theme.primaryText}; | color: ${props => props.theme.primaryText}; | ||||
| font-size: var(--text-base); | font-size: var(--text-base); | ||||
| `; | `; | ||||
| const TokenDropdownItemBalance = styled.div` | const TokenDropdownItemBalance = styled.div` | ||||
| color: ${props => props.theme.secondaryText}; | color: ${props => props.theme.primaryText}; | ||||
| font-size: var(--text-sm); | font-size: var(--text-base); | ||||
| font-weight: 700; | |||||
| `; | `; | ||||
| const TokenDropdownItemTokenId = styled.div` | const TokenDropdownItemTokenId = styled.div` | ||||
| color: ${props => props.theme.secondaryText}; | color: ${props => props.theme.secondaryText}; | ||||
| font-size: var(--text-sm); | font-size: 12px; | ||||
| margin-top: 2px; | |||||
| `; | `; | ||||
| const TokenFormContainer = styled.div` | const TokenFormContainer = styled.div` | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| gap: 12px; | |||||
| margin: 12px 0; | margin: 12px 0; | ||||
| `; | `; | ||||
| interface CashtabTxInfo { | interface CashtabTxInfo { | ||||
| address?: string; | address?: string; | ||||
| bip21?: string; | bip21?: string; | ||||
| value?: string; | value?: string; | ||||
| parseAllAsBip21?: boolean; | parseAllAsBip21?: boolean; | ||||
| returnToBrowser?: boolean; | returnToBrowser?: boolean; | ||||
| Show All 16 Lines | const SendXec: React.FC = () => { | ||||
| } = ContextValue; | } = ContextValue; | ||||
| const { settings, cashtabCache, tokens } = cashtabState; | const { settings, cashtabCache, tokens } = cashtabState; | ||||
| if (!ecashWallet || !tokens) { | if (!ecashWallet || !tokens) { | ||||
| return null; | return null; | ||||
| } | } | ||||
| const balanceSats = Number(ecashWallet.balanceSats); | const balanceSats = Number(ecashWallet.balanceSats); | ||||
| const [isTokenMode, setIsTokenMode] = useState<boolean>(false); | const [isTokenMode, setIsTokenMode] = useState<boolean>( | ||||
| () => new URLSearchParams(location.search).get('mode') === 'token', | |||||
| ); | |||||
| const [selectedTokenId, setSelectedTokenId] = useState<string | null>(null); | const [selectedTokenId, setSelectedTokenId] = useState<string | null>(null); | ||||
| const [tokenSearch, setTokenSearch] = useState<string>(''); | const [tokenSearch, setTokenSearch] = useState<string>(''); | ||||
| const [tokensInWallet, setTokensInWallet] = useState<TokenInfoKv[]>([]); | const [tokensInWallet, setTokensInWallet] = useState<TokenInfoKv[]>([]); | ||||
| const [filteredTokens, setFilteredTokens] = useState<TokenInfoKv[]>([]); | const [filteredTokens, setFilteredTokens] = useState<TokenInfoKv[]>([]); | ||||
| const [isOneToManyXECSend, setIsOneToManyXECSend] = | const [isOneToManyXECSend, setIsOneToManyXECSend] = | ||||
| useState<boolean>(false); | useState<boolean>(false); | ||||
| const [advancedOpen, setAdvancedOpen] = useState<boolean>(false); | |||||
| const [sendWithCashtabMsg, setSendWithCashtabMsg] = | const [sendWithCashtabMsg, setSendWithCashtabMsg] = | ||||
| useState<boolean>(false); | useState<boolean>(false); | ||||
| const [sendWithOpReturnRaw, setSendWithOpReturnRaw] = | const [sendWithOpReturnRaw, setSendWithOpReturnRaw] = | ||||
| useState<boolean>(false); | useState<boolean>(false); | ||||
| const [opReturnRawError, setOpReturnRawError] = useState<false | string>( | const [opReturnRawError, setOpReturnRawError] = useState<false | string>( | ||||
| false, | false, | ||||
| ); | ); | ||||
| const [parsedOpReturnRaw, setParsedOpReturnRaw] = | const [parsedOpReturnRaw, setParsedOpReturnRaw] = | ||||
| ▲ Show 20 Lines • Show All 324 Lines • ▼ Show 20 Lines | useEffect(() => { | ||||
| return a[1].genesisInfo.tokenTicker.localeCompare( | return a[1].genesisInfo.tokenTicker.localeCompare( | ||||
| b[1].genesisInfo.tokenTicker, | b[1].genesisInfo.tokenTicker, | ||||
| ); | ); | ||||
| }); | }); | ||||
| setTokensInWallet(walletTokensKeyValueArray); | setTokensInWallet(walletTokensKeyValueArray); | ||||
| }, [tokens, isTokenMode, cashtabCache]); | }, [tokens, isTokenMode, cashtabCache]); | ||||
| // Sync Send vs Send Token view from URL (so /send and /send?mode=token work) | |||||
| useEffect(() => { | |||||
| const params = new URLSearchParams(location.search); | |||||
| setIsTokenMode(params.get('mode') === 'token'); | |||||
| }, [location.search]); | |||||
| // Filter tokens based on search | // Filter tokens based on search | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!isTokenMode || tokensInWallet.length === 0) { | if (!isTokenMode || tokensInWallet.length === 0) { | ||||
| setFilteredTokens([]); | setFilteredTokens([]); | ||||
| return; | return; | ||||
| } | } | ||||
| if (tokenSearch === '') { | if (tokenSearch === '') { | ||||
| setFilteredTokens(tokensInWallet); | setFilteredTokens(tokensInWallet); | ||||
| ▲ Show 20 Lines • Show All 586 Lines • ▼ Show 20 Lines | useEffect(() => { | ||||
| // Get everything after the first ? mark | // Get everything after the first ? mark | ||||
| const hashRoute = window.location.hash; | const hashRoute = window.location.hash; | ||||
| // The "+1" is because we want to also omit the first question mark | // The "+1" is because we want to also omit the first question mark | ||||
| // So we need to slice at 1 character past it | // So we need to slice at 1 character past it | ||||
| const txInfoStr = hashRoute.slice(hashRoute.indexOf('?') + 1); | const txInfoStr = hashRoute.slice(hashRoute.indexOf('?') + 1); | ||||
| const txInfo: CashtabTxInfo = {}; | const txInfo: CashtabTxInfo = {}; | ||||
| // Set URL-based transaction flag as soon as we detect URL parameters | // Set URL-based transaction flag as soon as we detect URL parameters | ||||
| // (but not when the only param is mode=token, e.g. navigating to Send Token view) | |||||
| const urlParamsForFlag = new URLSearchParams(txInfoStr); | |||||
| const isSendTokenViewOnly = | |||||
| urlParamsForFlag.get('mode') === 'token' && | |||||
| !urlParamsForFlag.has('address') && | |||||
| !txInfoStr.startsWith('bip21'); | |||||
| if (!isSendTokenViewOnly) { | |||||
| setIsUrlBasedTransaction(true); | setIsUrlBasedTransaction(true); | ||||
| } | |||||
| // If bip21 is the first param, parse the whole string as a bip21 param string | // If bip21 is the first param, parse the whole string as a bip21 param string | ||||
| const parseAllAsBip21 = txInfoStr.startsWith('bip21'); | const parseAllAsBip21 = txInfoStr.startsWith('bip21'); | ||||
| if (parseAllAsBip21) { | if (parseAllAsBip21) { | ||||
| // Cashtab requires param string to start with bip21 if this is requesting bip21 validation | // Cashtab requires param string to start with bip21 if this is requesting bip21 validation | ||||
| txInfo.bip21 = txInfoStr.slice('bip21='.length); | txInfo.bip21 = txInfoStr.slice('bip21='.length); | ||||
| // If bip21 param string contains &returnToBrowser=1, remove it and set returnToBrowser flag | // If bip21 param string contains &returnToBrowser=1, remove it and set returnToBrowser flag | ||||
| ▲ Show 20 Lines • Show All 114 Lines • ▼ Show 20 Lines | useEffect(() => { | ||||
| } else { | } else { | ||||
| // Regular BIP21 XEC send | // Regular BIP21 XEC send | ||||
| handleAddressChange({ | handleAddressChange({ | ||||
| target: { | target: { | ||||
| name: 'address', | name: 'address', | ||||
| value: bip21Uri, | value: bip21Uri, | ||||
| }, | }, | ||||
| } as React.ChangeEvent<HTMLInputElement>); | } as React.ChangeEvent<HTMLInputElement>); | ||||
| const xecBip21Parsed = parseAddressInput( | |||||
| bip21Uri, | |||||
| balanceSats, | |||||
| userLocale, | |||||
| ); | |||||
| if ( | |||||
| typeof xecBip21Parsed.op_return_raw !== 'undefined' && | |||||
| typeof xecBip21Parsed.op_return_raw.value === 'string' | |||||
| ) { | |||||
| setAdvancedOpen(true); | |||||
| } | |||||
| } | } | ||||
| } else { | } else { | ||||
| // Enter address into input field and trigger handleAddressChange for validation | // Enter address into input field and trigger handleAddressChange for validation | ||||
| handleAddressChange({ | handleAddressChange({ | ||||
| target: { | target: { | ||||
| name: 'address', | name: 'address', | ||||
| value: txInfoFromUrl.address, | value: txInfoFromUrl.address, | ||||
| }, | }, | ||||
| ▲ Show 20 Lines • Show All 679 Lines • ▼ Show 20 Lines | ) => { | ||||
| } | } | ||||
| setCashtabMsgError(cashtabMsgError); | setCashtabMsgError(cashtabMsgError); | ||||
| setFormData(p => ({ | setFormData(p => ({ | ||||
| ...p, | ...p, | ||||
| [name]: value, | [name]: value, | ||||
| })); | })); | ||||
| }; | }; | ||||
| const handleAdvancedHeaderClick = () => { | |||||
| if (advancedOpen && txInfoFromUrl === false) { | |||||
| const rehydrateOpReturnFromBip21 = | |||||
| !isTokenMode && | |||||
| typeof parsedAddressInput.op_return_raw !== 'undefined' && | |||||
| typeof parsedAddressInput.op_return_raw.value === 'string' | |||||
| ? parsedAddressInput.op_return_raw.value | |||||
| : ''; | |||||
| setIsOneToManyXECSend(false); | |||||
| setSendWithCashtabMsg(false); | |||||
| setSendWithOpReturnRaw(rehydrateOpReturnFromBip21 !== ''); | |||||
| setAirdropFlag(false); | |||||
| setFormData(p => ({ | |||||
| ...p, | |||||
| multiAddressInput: '', | |||||
| cashtabMsg: '', | |||||
| opReturnRaw: rehydrateOpReturnFromBip21, | |||||
| })); | |||||
| setMultiSendAddressError(false); | |||||
| setCashtabMsgError(false); | |||||
| if (rehydrateOpReturnFromBip21 !== '') { | |||||
| const error = getOpReturnRawError(rehydrateOpReturnFromBip21); | |||||
| setOpReturnRawError(error); | |||||
| if (error === false) { | |||||
| setParsedOpReturnRaw( | |||||
| parseOpReturnRaw(rehydrateOpReturnFromBip21), | |||||
| ); | |||||
| } else { | |||||
| setParsedOpReturnRaw({ protocol: '', data: '' }); | |||||
| } | |||||
| } else { | |||||
| setOpReturnRawError(false); | |||||
| setParsedOpReturnRaw({ protocol: '', data: '' }); | |||||
| } | |||||
| } | |||||
| setAdvancedOpen(prev => !prev); | |||||
| }; | |||||
| const handleTokenCashtabMsgChange = ( | const handleTokenCashtabMsgChange = ( | ||||
| e: React.ChangeEvent<HTMLTextAreaElement>, | e: React.ChangeEvent<HTMLTextAreaElement>, | ||||
| ) => { | ) => { | ||||
| const { name, value } = e.target; | const { name, value } = e.target; | ||||
| let tokenCashtabMsgError: false | string = false; | let tokenCashtabMsgError: false | string = false; | ||||
| // For EMPP cashtab msg: user can input up to 100 bytes | // For EMPP cashtab msg: user can input up to 100 bytes | ||||
| // Total will be 104 bytes (100 user input + 4 byte lokad) | // Total will be 104 bytes (100 user input + 4 byte lokad) | ||||
| ▲ Show 20 Lines • Show All 139 Lines • ▼ Show 20 Lines | if (fiatPrice !== null && !isNaN(parseFloat(formData.amount))) { | ||||
| formData.amount !== '0' | formData.amount !== '0' | ||||
| ? formatFiatBalance( | ? formatFiatBalance( | ||||
| toXec(fiatToSatoshis(formData.amount, fiatPrice)), | toXec(fiatToSatoshis(formData.amount, fiatPrice)), | ||||
| userLocale, | userLocale, | ||||
| ) | ) | ||||
| : formatFiatBalance(0, userLocale) | : formatFiatBalance(0, userLocale) | ||||
| } ${appConfig.ticker}`; | } ${appConfig.ticker}`; | ||||
| } | } | ||||
| } else if ( | |||||
| fiatPrice !== null && | |||||
| selectedCurrency === appConfig.ticker && | |||||
| !isOneToManyXECSend && | |||||
| !isBip21MultipleOutputsSafe(parsedAddressInput) | |||||
| ) { | |||||
| // Show 0 fiat when amount is empty so amount preview always shows something | |||||
| fiatPriceString = `${ | |||||
| settings | |||||
| ? `${supportedFiatCurrencies[settings.fiatCurrency].symbol} ` | |||||
| : '$ ' | |||||
| }${(0).toLocaleString(userLocale, { | |||||
| minimumFractionDigits: appConfig.cashDecimals, | |||||
| maximumFractionDigits: appConfig.cashDecimals, | |||||
| })} ${ | |||||
| settings && settings.fiatCurrency | |||||
| ? settings.fiatCurrency.toUpperCase() | |||||
| : 'USD' | |||||
| }`; | |||||
| } | } | ||||
| const priceApiError = | const priceApiError = | ||||
| fiatPrice === null && selectedCurrency !== appConfig.ticker; | fiatPrice === null && selectedCurrency !== appConfig.ticker; | ||||
| const disableSendButton = shouldSendXecBeDisabled( | const disableSendButton = shouldSendXecBeDisabled( | ||||
| formData, | formData, | ||||
| balanceSats, | balanceSats, | ||||
| ▲ Show 20 Lines • Show All 93 Lines • ▼ Show 20 Lines | if (cachedInfoLoaded && isBip21TokenSend(parsedAddressInput)) { | ||||
| ? false | ? false | ||||
| : isValidAmountOrErrorMsg; | : isValidAmountOrErrorMsg; | ||||
| } | } | ||||
| } | } | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <OuterCtn> | <OuterCtn> | ||||
| <ActionButtonRow | |||||
| variant="sendReceive" | |||||
| activeIndex={isTokenMode ? 2 : 1} | |||||
| /> | |||||
| <SendContentWrapper> | |||||
| {showConfirmSendModal && ( | {showConfirmSendModal && ( | ||||
| <Modal | <Modal | ||||
| title={`Send ${decimalizedTokenQty} ${nameAndTicker} to ${addressPreview}?`} | title={`Send ${decimalizedTokenQty} ${nameAndTicker} to ${addressPreview}?`} | ||||
| handleOk={sendToken} | handleOk={sendToken} | ||||
| handleCancel={() => setShowConfirmSendModal(false)} | handleCancel={() => setShowConfirmSendModal(false)} | ||||
| showCancelButton | showCancelButton | ||||
| /> | /> | ||||
| )} | )} | ||||
| {isModalVisible && ( | {isModalVisible && ( | ||||
| <Modal | <Modal | ||||
| title="Confirm Send" | title="Confirm Send" | ||||
| description={ | description={ | ||||
| isTokenMode && selectedTokenId | isTokenMode && selectedTokenId | ||||
| ? `Send ${tokenFormData.amount} ${ | ? `Send ${tokenFormData.amount} ${ | ||||
| cashtabCache.tokens.get(selectedTokenId) | cashtabCache.tokens.get( | ||||
| ?.genesisInfo.tokenTicker || 'token' | selectedTokenId, | ||||
| )?.genesisInfo.tokenTicker || 'token' | |||||
| } to ${tokenFormData.address}` | } to ${tokenFormData.address}` | ||||
| : isOneToManyXECSend | : isOneToManyXECSend | ||||
| ? `Send | ? `Send | ||||
| ${multiSendTotal.toLocaleString(userLocale, { | ${multiSendTotal.toLocaleString(userLocale, { | ||||
| maximumFractionDigits: 2, | maximumFractionDigits: 2, | ||||
| })} | })} | ||||
| XEC to multiple recipients?` | XEC to multiple recipients?` | ||||
| : `Send ${formData.amount}${' '} | : `Send ${formData.amount}${' '} | ||||
| ${selectedCurrency} to ${parsedAddressInput.address.value}` | ${selectedCurrency} to ${parsedAddressInput.address.value}` | ||||
| } | } | ||||
| handleOk={ | handleOk={ | ||||
| isTokenMode && selectedTokenId | isTokenMode && selectedTokenId | ||||
| ? handleTokenSendOk | ? handleTokenSendOk | ||||
| : handleOk | : handleOk | ||||
| } | } | ||||
| handleCancel={handleCancel} | handleCancel={handleCancel} | ||||
| showCancelButton | showCancelButton | ||||
| /> | /> | ||||
| )} | )} | ||||
| {showSuccessModal && ( | {showSuccessModal && ( | ||||
| <SuccessModalOverlay | <SuccessModalOverlay | ||||
| onClick={() => { | onClick={() => { | ||||
| // Close the window or navigate home on any click off of the modal | // Close the window or navigate home on any click off of the modal | ||||
| closeOrNavigateFromUrlBasedTransaction(); | closeOrNavigateFromUrlBasedTransaction(); | ||||
| }} | }} | ||||
| > | > | ||||
| <SuccessModalContent | <SuccessModalContent | ||||
| onClick={e => { | onClick={e => { | ||||
| // Close when clicking on the modal content | // Close when clicking on the modal content | ||||
| // (but not on interactive elements like copy button or link) | // (but not on interactive elements like copy button or link) | ||||
| if (e.target === e.currentTarget) { | if (e.target === e.currentTarget) { | ||||
| closeOrNavigateFromUrlBasedTransaction(); | closeOrNavigateFromUrlBasedTransaction(); | ||||
| } | } | ||||
| }} | }} | ||||
| > | > | ||||
| <SuccessIcon> | <SuccessIcon> | ||||
| <div /> | <div /> | ||||
| <img src={Burst} alt="Success" /> | <img src={Burst} alt="Success" /> | ||||
| </SuccessIcon> | </SuccessIcon> | ||||
| <SuccessTitle>Sent!</SuccessTitle> | <SuccessTitle>Sent!</SuccessTitle> | ||||
| <TransactionIdLink | <TransactionIdLink | ||||
| href={`${explorer.blockExplorerUrl}/tx/${successTxid}`} | href={`${explorer.blockExplorerUrl}/tx/${successTxid}`} | ||||
| target="_blank" | target="_blank" | ||||
| rel="noopener noreferrer" | rel="noopener noreferrer" | ||||
| // Prevent closing the window when clicking on the link | // Prevent closing the window when clicking on the link | ||||
| onClick={e => e.stopPropagation()} | onClick={e => e.stopPropagation()} | ||||
| > | > | ||||
| View Transaction | View Transaction | ||||
| </TransactionIdLink> | </TransactionIdLink> | ||||
| <SuccessButton | <SuccessButton | ||||
| onClick={() => { | onClick={() => { | ||||
| closeOrNavigateFromUrlBasedTransaction(); | closeOrNavigateFromUrlBasedTransaction(); | ||||
| }} | }} | ||||
| > | > | ||||
| Close | Close | ||||
| </SuccessButton> | </SuccessButton> | ||||
| </SuccessModalContent> | </SuccessModalContent> | ||||
| </SuccessModalOverlay> | </SuccessModalOverlay> | ||||
| )} | )} | ||||
| <SwitchContainer> | |||||
| <Switch | |||||
| name="Toggle XEC/Token Mode" | |||||
| on="Tokens" | |||||
| off="XEC" | |||||
| width={150} | |||||
| right={115} | |||||
| checked={isTokenMode} | |||||
| bgColorOn={theme.secondaryAccent} | |||||
| bgColorOff={theme.accent} | |||||
| handleToggle={() => setIsTokenMode(!isTokenMode)} | |||||
| /> | |||||
| </SwitchContainer> | |||||
| {!isTokenMode && ( | |||||
| <SwitchContainer> | |||||
| <Switch | |||||
| name="Toggle Multisend" | |||||
| on="Send to many" | |||||
| off="Send to one" | |||||
| width={150} | |||||
| right={115} | |||||
| checked={isOneToManyXECSend} | |||||
| disabled={ | |||||
| txInfoFromUrl !== false || | |||||
| 'queryString' in parsedAddressInput | |||||
| } | |||||
| handleToggle={() => | |||||
| setIsOneToManyXECSend(!isOneToManyXECSend) | |||||
| } | |||||
| /> | |||||
| </SwitchContainer> | |||||
| )} | |||||
| {isTokenMode ? ( | {isTokenMode ? ( | ||||
| <TokenFormContainer> | <TokenFormContainer> | ||||
| <TokenSelectDropdown> | <TokenSelectDropdown> | ||||
| {selectedTokenId ? ( | {selectedTokenId ? ( | ||||
| <> | |||||
| <SendingTokenLabel> | |||||
| Sending | |||||
| </SendingTokenLabel> | |||||
| <SelectedTokenDisplay> | <SelectedTokenDisplay> | ||||
| <SelectedTokenInfo> | <SelectedTokenInfo> | ||||
| <TokenIcon | <TokenIcon | ||||
| size={32} | size={32} | ||||
| tokenId={selectedTokenId} | tokenId={selectedTokenId} | ||||
| /> | /> | ||||
| <SelectedTokenDetails> | <SelectedTokenDetails> | ||||
| <div> | |||||
| <SelectedTokenText> | <SelectedTokenText> | ||||
| {filteredTokens.find( | {filteredTokens.find( | ||||
| kv => | kv => | ||||
| kv[0] === | kv[0] === | ||||
| selectedTokenId, | selectedTokenId, | ||||
| )?.[1].genesisInfo | )?.[1].genesisInfo | ||||
| .tokenTicker || | .tokenTicker || | ||||
| tokensInWallet.find( | tokensInWallet.find( | ||||
| kv => | kv => | ||||
| kv[0] === | kv[0] === | ||||
| selectedTokenId, | selectedTokenId, | ||||
| )?.[1].genesisInfo | )?.[1] | ||||
| .genesisInfo | |||||
| .tokenTicker || | .tokenTicker || | ||||
| ''} | ''} | ||||
| </SelectedTokenText> | </SelectedTokenText> | ||||
| <SelectedTokenIdWrapper> | |||||
| {selectedTokenId.slice( | |||||
| 0, | |||||
| 3, | |||||
| )} | |||||
| ... | |||||
| {selectedTokenId.slice( | |||||
| -3, | |||||
| )} | |||||
| </SelectedTokenIdWrapper> | |||||
| </div> | |||||
| <SelectedTokenBalance> | <SelectedTokenBalance> | ||||
| {formatBalance( | {formatBalance( | ||||
| tokens.get( | tokens.get( | ||||
| selectedTokenId, | selectedTokenId, | ||||
| ) || '0', | ) || '0', | ||||
| userLocale, | userLocale, | ||||
| )} | )} | ||||
| </SelectedTokenBalance> | </SelectedTokenBalance> | ||||
| <SelectedTokenIdWrapper> | |||||
| <TokenIdPreview | |||||
| tokenId={selectedTokenId} | |||||
| /> | |||||
| </SelectedTokenIdWrapper> | |||||
| </SelectedTokenDetails> | </SelectedTokenDetails> | ||||
| </SelectedTokenInfo> | </SelectedTokenInfo> | ||||
| <TokenSelectClearButton | <TokenSelectClearButton | ||||
| onClick={() => { | onClick={() => { | ||||
| _clearTokenInputForms(); | _clearTokenInputForms(); | ||||
| }} | }} | ||||
| title="Clear token selection" | title="Clear token selection" | ||||
| > | > | ||||
| × | × | ||||
| </TokenSelectClearButton> | </TokenSelectClearButton> | ||||
| </SelectedTokenDisplay> | </SelectedTokenDisplay> | ||||
| </> | |||||
| ) : ( | ) : ( | ||||
| <TokenSelectInputWrapper> | <TokenSelectInputWrapper> | ||||
| <Input | <Input | ||||
| placeholder="Start typing a token ticker or name" | placeholder="Search by token ticker or name" | ||||
| name="tokenSearch" | name="tokenSearch" | ||||
| value={tokenSearch} | value={tokenSearch} | ||||
| handleInput={handleTokenSearchInput} | handleInput={handleTokenSearchInput} | ||||
| /> | /> | ||||
| {filteredTokens.length > 0 && ( | {filteredTokens.length > 0 && ( | ||||
| <TokenDropdownList> | <TokenDropdownList> | ||||
| {filteredTokens.map(kv => ( | {filteredTokens.map(kv => ( | ||||
| <TokenDropdownItem | <TokenDropdownItem | ||||
| key={kv[0]} | key={kv[0]} | ||||
| onClick={() => | onClick={() => | ||||
| handleTokenSelect(kv[0]) | handleTokenSelect( | ||||
| kv[0], | |||||
| ) | |||||
| } | } | ||||
| > | > | ||||
| <TokenDropdownItemContent> | <TokenDropdownItemContent> | ||||
| <TokenIcon | <TokenIcon | ||||
| size={32} | size={32} | ||||
| tokenId={kv[0]} | tokenId={kv[0]} | ||||
| /> | /> | ||||
| <TokenDropdownItemInfo> | <TokenDropdownItemInfo> | ||||
| <div> | |||||
| <TokenDropdownItemTicker> | <TokenDropdownItemTicker> | ||||
| { | { | ||||
| kv[1] | kv[1] | ||||
| .genesisInfo | .genesisInfo | ||||
| .tokenTicker | .tokenTicker | ||||
| } | } | ||||
| </TokenDropdownItemTicker> | </TokenDropdownItemTicker> | ||||
| <TokenDropdownItemBalance> | |||||
| {formatBalance( | |||||
| kv[1] | |||||
| .balance, | |||||
| userLocale, | |||||
| )} | |||||
| </TokenDropdownItemBalance> | |||||
| <TokenDropdownItemTokenId> | <TokenDropdownItemTokenId> | ||||
| {kv[0].slice( | {kv[0].slice( | ||||
| 0, | 0, | ||||
| 3, | 3, | ||||
| )} | )} | ||||
| ... | ... | ||||
| {kv[0].slice( | {kv[0].slice( | ||||
| -3, | -3, | ||||
| )} | )} | ||||
| </TokenDropdownItemTokenId> | </TokenDropdownItemTokenId> | ||||
| </div> | |||||
| <TokenDropdownItemBalance> | |||||
| {formatBalance( | |||||
| kv[1] | |||||
| .balance, | |||||
| userLocale, | |||||
| )} | |||||
| </TokenDropdownItemBalance> | |||||
| </TokenDropdownItemInfo> | </TokenDropdownItemInfo> | ||||
| </TokenDropdownItemContent> | </TokenDropdownItemContent> | ||||
| </TokenDropdownItem> | </TokenDropdownItem> | ||||
| ))} | ))} | ||||
| </TokenDropdownList> | </TokenDropdownList> | ||||
| )} | )} | ||||
| </TokenSelectInputWrapper> | </TokenSelectInputWrapper> | ||||
| )} | )} | ||||
| </TokenSelectDropdown> | </TokenSelectDropdown> | ||||
| {selectedTokenId && ( | {selectedTokenId && ( | ||||
| <InputWithScanner | <InputWithScanner | ||||
| label="Address" | |||||
| placeholder="Address" | placeholder="Address" | ||||
| name="address" | name="address" | ||||
| value={tokenFormData.address} | value={tokenFormData.address} | ||||
| disabled={ | disabled={ | ||||
| txInfoFromUrl !== false && | txInfoFromUrl !== false && | ||||
| isBip21TokenSendWithTokenId( | isBip21TokenSendWithTokenId( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) && | ) && | ||||
| selectedTokenId !== null && | selectedTokenId !== null && | ||||
| parsedAddressInput.token_id?.value === | parsedAddressInput.token_id?.value === | ||||
| selectedTokenId | selectedTokenId | ||||
| } | } | ||||
| handleInput={e => { | handleInput={e => { | ||||
| const parsed = parseAddressInput( | const parsed = parseAddressInput( | ||||
| e.target.value, | e.target.value, | ||||
| balanceSats, | balanceSats, | ||||
| userLocale, | userLocale, | ||||
| ); | ); | ||||
| handleTokenModeAddressChange(e, parsed); | handleTokenModeAddressChange(e, parsed); | ||||
| }} | }} | ||||
| error={sendAddressError} | error={sendAddressError} | ||||
| /> | /> | ||||
| )} | )} | ||||
| {/* Show token amount input */} | {/* Show token amount input */} | ||||
| {selectedTokenId && ( | {selectedTokenId && ( | ||||
| <SendTokenInput | <SendTokenInput | ||||
| label="Amount" | |||||
| name="amount" | name="amount" | ||||
| placeholder="Amount" | placeholder="Amount" | ||||
| value={tokenFormData.amount} | value={tokenFormData.amount} | ||||
| inputDisabled={ | inputDisabled={ | ||||
| isBip21TokenSendWithTokenId( | isBip21TokenSendWithTokenId( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) && | ) && | ||||
| selectedTokenId !== null && | selectedTokenId !== null && | ||||
| parsedAddressInput.token_id?.value === | parsedAddressInput.token_id?.value === | ||||
| selectedTokenId && | selectedTokenId && | ||||
| typeof parsedAddressInput.token_decimalized_qty !== | typeof parsedAddressInput.token_decimalized_qty !== | ||||
| 'undefined' && | 'undefined' && | ||||
| typeof parsedAddressInput | typeof parsedAddressInput | ||||
| .token_decimalized_qty.value === | .token_decimalized_qty.value === | ||||
| 'string' && | 'string' && | ||||
| parsedAddressInput.token_decimalized_qty | parsedAddressInput.token_decimalized_qty | ||||
| .value !== null && | .value !== null && | ||||
| parsedAddressInput.token_decimalized_qty | parsedAddressInput.token_decimalized_qty | ||||
| .error === false | .error === false | ||||
| } | } | ||||
| error={sendTokenAmountError} | error={sendTokenAmountError} | ||||
| handleInput={handleTokenAmountChange} | handleInput={handleTokenAmountChange} | ||||
| handleOnMax={onTokenMax} | handleOnMax={onTokenMax} | ||||
| /> | /> | ||||
| )} | )} | ||||
| {/* Show error if empp_raw is present but token is not ALP */} | {/* Show error if empp_raw is present but token is not ALP */} | ||||
| {shouldShowSlpErrorForEmppRaw && ( | {shouldShowSlpErrorForEmppRaw && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <Alert> | <Alert> | ||||
| <AlertMsg> | <AlertMsg> | ||||
| empp_raw is only supported for ALP token | empp_raw is only supported for ALP | ||||
| txs | token txs | ||||
| </AlertMsg> | </AlertMsg> | ||||
| </Alert> | </Alert> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | )} | ||||
| {/* Show EMPP options for ALP tokens */} | {/* Show EMPP options for ALP tokens */} | ||||
| {selectedTokenId && | {selectedTokenId && | ||||
| typeof cashtabCache.tokens.get(selectedTokenId) !== | typeof cashtabCache.tokens.get( | ||||
| 'undefined' && | selectedTokenId, | ||||
| cashtabCache.tokens.get(selectedTokenId)?.tokenType | ) !== 'undefined' && | ||||
| .type === 'ALP_TOKEN_TYPE_STANDARD' && ( | cashtabCache.tokens.get(selectedTokenId) | ||||
| ?.tokenType.type === | |||||
| 'ALP_TOKEN_TYPE_STANDARD' && ( | |||||
| <> | <> | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <SwitchAndLabel> | <AdvancedButtonsRow> | ||||
| <Switch | <AdvancedButton | ||||
| name="Toggle Cashtab Msg Token" | type="button" | ||||
| on="✉️" | $active={ | ||||
| off="✉️" | |||||
| checked={ | |||||
| sendWithCashtabMsgToken | sendWithCashtabMsgToken | ||||
| } | } | ||||
| disabled={ | onClick={() => { | ||||
| txInfoFromUrl !== false || | |||||
| 'queryString' in | |||||
| parsedAddressInput | |||||
| } | |||||
| handleToggle={() => { | |||||
| // If we are sending a Cashtab msg, toggle off empp_raw | |||||
| if ( | if ( | ||||
| !sendWithCashtabMsgToken && | !sendWithCashtabMsgToken && | ||||
| sendWithEmppRaw | sendWithEmppRaw | ||||
| ) { | ) { | ||||
| setSendWithEmppRaw( | setSendWithEmppRaw( | ||||
| false, | false, | ||||
| ); | ); | ||||
| } | } | ||||
| setSendWithCashtabMsgToken( | setSendWithCashtabMsgToken( | ||||
| !sendWithCashtabMsgToken, | !sendWithCashtabMsgToken, | ||||
| ); | ); | ||||
| }} | }} | ||||
| /> | disabled={ | ||||
| <SwitchLabel> | txInfoFromUrl !== | ||||
| false || | |||||
| 'queryString' in | |||||
| parsedAddressInput | |||||
| } | |||||
| > | |||||
| Cashtab Msg | Cashtab Msg | ||||
| </SwitchLabel> | </AdvancedButton> | ||||
| </SwitchAndLabel> | <AdvancedButton | ||||
| type="button" | |||||
| $active={sendWithEmppRaw} | |||||
| onClick={() => { | |||||
| if ( | |||||
| !sendWithEmppRaw && | |||||
| sendWithCashtabMsgToken | |||||
| ) { | |||||
| setSendWithCashtabMsgToken( | |||||
| false, | |||||
| ); | |||||
| } | |||||
| setSendWithEmppRaw( | |||||
| !sendWithEmppRaw, | |||||
| ); | |||||
| }} | |||||
| disabled={ | |||||
| txInfoFromUrl !== | |||||
| false || | |||||
| 'queryString' in | |||||
| parsedAddressInput | |||||
| } | |||||
| > | |||||
| empp_raw | |||||
| </AdvancedButton> | |||||
| </AdvancedButtonsRow> | |||||
| </SendXecRow> | </SendXecRow> | ||||
| {sendWithCashtabMsgToken && ( | {sendWithCashtabMsgToken && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <TextArea | <TextArea | ||||
| name="tokenCashtabMsg" | name="tokenCashtabMsg" | ||||
| height={62} | height={62} | ||||
| placeholder={`Include a Cashtab msg EMPP push with this token tx (max 100 bytes)`} | placeholder={`Include a Cashtab msg EMPP push with this token tx (max 100 bytes)`} | ||||
| value={ | value={ | ||||
| tokenFormData.tokenCashtabMsg | tokenFormData.tokenCashtabMsg | ||||
| } | } | ||||
| error={tokenCashtabMsgError} | error={tokenCashtabMsgError} | ||||
| showCount | showCount | ||||
| customCount={ | customCount={ | ||||
| strToBytes( | strToBytes( | ||||
| tokenFormData.tokenCashtabMsg, | tokenFormData.tokenCashtabMsg, | ||||
| ).length | ).length | ||||
| } | } | ||||
| max={100} | max={100} | ||||
| handleInput={ | handleInput={ | ||||
| handleTokenCashtabMsgChange | handleTokenCashtabMsgChange | ||||
| } | } | ||||
| /> | /> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | )} | ||||
| <SendXecRow> | |||||
| <SwitchAndLabel> | |||||
| <Switch | |||||
| name="Toggle empp_raw" | |||||
| checked={sendWithEmppRaw} | |||||
| disabled={ | |||||
| txInfoFromUrl !== false || | |||||
| 'queryString' in | |||||
| parsedAddressInput | |||||
| } | |||||
| handleToggle={() => { | |||||
| // If we are sending with empp_raw, toggle off CashtabMsg | |||||
| if ( | |||||
| !sendWithEmppRaw && | |||||
| sendWithCashtabMsgToken | |||||
| ) { | |||||
| setSendWithCashtabMsgToken( | |||||
| false, | |||||
| ); | |||||
| } | |||||
| setSendWithEmppRaw( | |||||
| !sendWithEmppRaw, | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| <SwitchLabel>empp_raw</SwitchLabel> | |||||
| </SwitchAndLabel> | |||||
| </SendXecRow> | |||||
| {(sendWithEmppRaw || | {(sendWithEmppRaw || | ||||
| (typeof parsedAddressInput.empp_raw !== | (typeof parsedAddressInput.empp_raw !== | ||||
| 'undefined' && | 'undefined' && | ||||
| parsedAddressInput.empp_raw | parsedAddressInput.empp_raw | ||||
| .error === false)) && ( | .error === false)) && ( | ||||
| <> | <> | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <TextArea | <TextArea | ||||
| name="emppRaw" | name="emppRaw" | ||||
| height={62} | height={62} | ||||
| placeholder={`(Advanced) Enter raw hex EMPP push (max 100 bytes)`} | placeholder={`(Advanced) Enter raw hex EMPP push (max 100 bytes)`} | ||||
| value={ | value={ | ||||
| tokenFormData.emppRaw | tokenFormData.emppRaw | ||||
| } | } | ||||
| error={emppRawError} | error={emppRawError} | ||||
| disabled={ | disabled={ | ||||
| txInfoFromUrl !== | txInfoFromUrl !== | ||||
| false || | false || | ||||
| 'queryString' in | 'queryString' in | ||||
| parsedAddressInput | parsedAddressInput | ||||
| } | } | ||||
| showCount | showCount | ||||
| max={200} | max={200} | ||||
| customCount={ | customCount={ | ||||
| tokenFormData.emppRaw | tokenFormData | ||||
| .emppRaw | |||||
| .length / 2 | .length / 2 | ||||
| } | } | ||||
| handleInput={ | handleInput={ | ||||
| handleEmppRawInput | handleEmppRawInput | ||||
| } | } | ||||
| /> | /> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| {emppRawError === false && | {emppRawError === false && | ||||
| tokenFormData.emppRaw !== | tokenFormData.emppRaw !== | ||||
| '' && ( | '' && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <ParsedBip21InfoRow> | <ParsedBip21InfoRow> | ||||
| <ParsedBip21InfoLabel> | <ParsedBip21InfoLabel> | ||||
| Parsed empp_raw | Parsed | ||||
| empp_raw | |||||
| </ParsedBip21InfoLabel> | </ParsedBip21InfoLabel> | ||||
| <ParsedBip21Info> | <ParsedBip21Info> | ||||
| <b> | <b> | ||||
| { | { | ||||
| parsedEmppRaw.protocol | parsedEmppRaw.protocol | ||||
| } | } | ||||
| </b> | </b> | ||||
| <br /> | <br /> | ||||
| { | { | ||||
| parsedEmppRaw.data | parsedEmppRaw.data | ||||
| } | } | ||||
| </ParsedBip21Info> | </ParsedBip21Info> | ||||
| </ParsedBip21InfoRow> | </ParsedBip21InfoRow> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} | ||||
| {/* Parsed input_data_raw when present in BIP21 (token mode) */} | {/* Parsed input_data_raw when present in BIP21 (token mode) */} | ||||
| {selectedTokenId && | {selectedTokenId && | ||||
| (parsedInputDataRaw.protocol !== '' || | (parsedInputDataRaw.protocol !== '' || | ||||
| parsedInputDataRaw.data !== '') && ( | parsedInputDataRaw.data !== '') && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <ParsedBip21InfoRow> | <ParsedBip21InfoRow> | ||||
| <ParsedBip21InfoLabel> | <ParsedBip21InfoLabel> | ||||
| Parsed input_data_raw | Parsed input_data_raw | ||||
| </ParsedBip21InfoLabel> | </ParsedBip21InfoLabel> | ||||
| <ParsedBip21Info> | <ParsedBip21Info> | ||||
| <b>{parsedInputDataRaw.protocol}</b> | <b> | ||||
| { | |||||
| parsedInputDataRaw.protocol | |||||
| } | |||||
| </b> | |||||
| <br /> | <br /> | ||||
| {parsedInputDataRaw.data} | {parsedInputDataRaw.data} | ||||
| </ParsedBip21Info> | </ParsedBip21Info> | ||||
| </ParsedBip21InfoRow> | </ParsedBip21InfoRow> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | )} | ||||
| {/* Show BIP21 token send info in token mode (only if token_decimalized_qty is present) */} | {/* Show BIP21 token send info in token mode (only if token_decimalized_qty is present) */} | ||||
| {isBip21TokenSend(parsedAddressInput) && | {isBip21TokenSend(parsedAddressInput) && | ||||
| selectedTokenId !== null && | selectedTokenId !== null && | ||||
| selectedTokenId === | selectedTokenId === | ||||
| parsedAddressInput.token_id.value && | parsedAddressInput.token_id.value && | ||||
| tokenIdQueryError === false && ( | tokenIdQueryError === false && ( | ||||
| <> | <> | ||||
| {typeof cashtabCache.tokens.get( | {typeof cashtabCache.tokens.get( | ||||
| parsedAddressInput.token_id.value, | parsedAddressInput.token_id.value, | ||||
| ) !== 'undefined' ? ( | ) !== 'undefined' ? ( | ||||
| <> | <> | ||||
| {/* Show parsed token send info */} | {/* Show parsed token send info */} | ||||
| {isValidFirmaRedeemTx( | {isValidFirmaRedeemTx( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) ? ( | ) ? ( | ||||
| <ParsedTokenSend | <ParsedTokenSend | ||||
| style={{ | style={{ | ||||
| marginBottom: '12px', | marginBottom: | ||||
| '12px', | |||||
| }} | }} | ||||
| > | > | ||||
| <FirmaRedeemLogoWrapper> | <FirmaRedeemLogoWrapper> | ||||
| <FirmaIcon /> | <FirmaIcon /> | ||||
| <TetherIcon /> | <TetherIcon /> | ||||
| </FirmaRedeemLogoWrapper> | </FirmaRedeemLogoWrapper> | ||||
| <FirmaRedeemTextAndCopy> | <FirmaRedeemTextAndCopy> | ||||
| On tx finalized,{' '} | On tx finalized,{' '} | ||||
| {( | {( | ||||
| Number( | Number( | ||||
| parsedAddressInput | parsedAddressInput | ||||
| .token_decimalized_qty | .token_decimalized_qty | ||||
| .value, | .value, | ||||
| ) - | ) - | ||||
| getFirmaRedeemFee( | getFirmaRedeemFee( | ||||
| Number( | Number( | ||||
| parsedAddressInput | parsedAddressInput | ||||
| .token_decimalized_qty | .token_decimalized_qty | ||||
| .value, | .value, | ||||
| ), | ), | ||||
| ) | ) | ||||
| ).toLocaleString( | ).toLocaleString( | ||||
| userLocale, | userLocale, | ||||
| { | { | ||||
| maximumFractionDigits: 4, | maximumFractionDigits: 4, | ||||
| minimumFractionDigits: 4, | minimumFractionDigits: 4, | ||||
| }, | }, | ||||
| )}{' '} | )}{' '} | ||||
| USDT will be sent to{' '} | USDT will be sent to{' '} | ||||
| {parsedFirma.data.slice( | {parsedFirma.data.slice( | ||||
| 0, | 0, | ||||
| 3, | 3, | ||||
| )} | )} | ||||
| ... | ... | ||||
| {parsedFirma.data.slice( | {parsedFirma.data.slice( | ||||
| -3, | -3, | ||||
| )}{' '} | )}{' '} | ||||
| <CopyIconButton | <CopyIconButton | ||||
| name="Copy SOL addr" | name="Copy SOL addr" | ||||
| data={ | data={ | ||||
| parsedFirma.data | parsedFirma.data | ||||
| } | } | ||||
| showToast | showToast | ||||
| /> | /> | ||||
| </FirmaRedeemTextAndCopy> | </FirmaRedeemTextAndCopy> | ||||
| </ParsedTokenSend> | </ParsedTokenSend> | ||||
| ) : ( | ) : ( | ||||
| <ParsedTokenSend | <ParsedTokenSend | ||||
| style={{ | style={{ | ||||
| marginBottom: '12px', | marginBottom: | ||||
| '12px', | |||||
| }} | }} | ||||
| > | > | ||||
| <TokenIcon | <TokenIcon | ||||
| size={64} | size={64} | ||||
| tokenId={ | tokenId={ | ||||
| parsedAddressInput | parsedAddressInput | ||||
| .token_id.value | .token_id | ||||
| .value | |||||
| } | } | ||||
| /> | /> | ||||
| Sending{' '} | Sending{' '} | ||||
| {decimalizedTokenQty}{' '} | {decimalizedTokenQty}{' '} | ||||
| {nameAndTicker} to{' '} | {nameAndTicker} to{' '} | ||||
| {addressPreview} | {addressPreview} | ||||
| </ParsedTokenSend> | </ParsedTokenSend> | ||||
| )} | )} | ||||
| </> | </> | ||||
| ) : ( | ) : ( | ||||
| <InlineLoader /> | <InlineLoader /> | ||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} | ||||
| {/* Show parsed firma info in token mode when firma is present but not a redeem tx */} | {/* Show parsed firma info in token mode when firma is present but not a redeem tx */} | ||||
| {isBip21TokenSend(parsedAddressInput) && | {isBip21TokenSend(parsedAddressInput) && | ||||
| !isValidFirmaRedeemTx(parsedAddressInput) && | !isValidFirmaRedeemTx(parsedAddressInput) && | ||||
| selectedTokenId !== null && | selectedTokenId !== null && | ||||
| selectedTokenId === | selectedTokenId === | ||||
| parsedAddressInput.token_id.value && | parsedAddressInput.token_id.value && | ||||
| typeof parsedAddressInput.firma?.value === | typeof parsedAddressInput.firma?.value === | ||||
| 'string' && | 'string' && | ||||
| parsedAddressInput.firma.error === false && | parsedAddressInput.firma.error === false && | ||||
| parsedFirma.protocol !== '' && | parsedFirma.protocol !== '' && | ||||
| parsedFirma.data !== '' && ( | parsedFirma.data !== '' && ( | ||||
| <div style={{ margin: '12px 0' }}> | <div style={{ margin: '12px 0' }}> | ||||
| <ParsedBip21InfoRow> | <ParsedBip21InfoRow> | ||||
| <ParsedBip21InfoLabel> | <ParsedBip21InfoLabel> | ||||
| Parsed firma | Parsed firma | ||||
| </ParsedBip21InfoLabel> | </ParsedBip21InfoLabel> | ||||
| <ParsedBip21Info> | <ParsedBip21Info> | ||||
| <b>{parsedFirma.protocol}</b> | <b>{parsedFirma.protocol}</b> | ||||
| <br /> | <br /> | ||||
| {parsedFirma.data} | {parsedFirma.data} | ||||
| </ParsedBip21Info> | </ParsedBip21Info> | ||||
| </ParsedBip21InfoRow> | </ParsedBip21InfoRow> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| {(isBip21TokenSend(parsedAddressInput) || | {(isBip21TokenSend(parsedAddressInput) || | ||||
| isBip21TokenSendWithTokenId(parsedAddressInput)) && | isBip21TokenSendWithTokenId( | ||||
| parsedAddressInput, | |||||
| )) && | |||||
| selectedTokenId !== null && | selectedTokenId !== null && | ||||
| parsedAddressInput.token_id?.value === | parsedAddressInput.token_id?.value === | ||||
| selectedTokenId && | selectedTokenId && | ||||
| tokenIdQueryError && ( | tokenIdQueryError && ( | ||||
| <Alert> | <Alert> | ||||
| Error querying token info for{' '} | Error querying token info for{' '} | ||||
| {parsedAddressInput.token_id?.value || | {parsedAddressInput.token_id?.value || | ||||
| 'unknown token'} | 'unknown token'} | ||||
| </Alert> | </Alert> | ||||
| )} | )} | ||||
| </TokenFormContainer> | </TokenFormContainer> | ||||
| ) : ( | ) : ( | ||||
| <InputModesHolder open={isOneToManyXECSend}> | <InputModesHolder open={isOneToManyXECSend}> | ||||
| <SendToOneHolder> | <SendToOneHolder> | ||||
| <SendToOneInputForm> | <SendToOneInputForm> | ||||
| <InputWithScanner | <InputWithScanner | ||||
| label="Send to" | |||||
| placeholder={'Address'} | placeholder={'Address'} | ||||
| name="address" | name="address" | ||||
| value={formData.address} | value={formData.address} | ||||
| disabled={txInfoFromUrl !== false} | disabled={txInfoFromUrl !== false} | ||||
| handleInput={handleAddressChange} | handleInput={handleAddressChange} | ||||
| error={sendAddressError} | error={sendAddressError} | ||||
| /> | /> | ||||
| {isBip21MultipleOutputsSafe( | {isBip21MultipleOutputsSafe( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) ? ( | ) ? ( | ||||
| <Info> | <Info> | ||||
| <b> | <b> | ||||
| BIP21: Sending{' '} | BIP21: Sending{' '} | ||||
| {bip21MultipleOutputsFormattedTotalSendXec.toLocaleString( | {bip21MultipleOutputsFormattedTotalSendXec.toLocaleString( | ||||
| userLocale, | userLocale, | ||||
| { | { | ||||
| maximumFractionDigits: 2, | maximumFractionDigits: 2, | ||||
| minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
| }, | }, | ||||
| )}{' '} | )}{' '} | ||||
| XEC to{' '} | XEC to{' '} | ||||
| {parsedAddressInput | {parsedAddressInput | ||||
| .parsedAdditionalXecOutputs | .parsedAdditionalXecOutputs | ||||
| .value.length + 1}{' '} | .value.length + 1}{' '} | ||||
| outputs | outputs | ||||
| </b> | </b> | ||||
| </Info> | </Info> | ||||
| ) : ( | ) : ( | ||||
| <SendXecInput | <SendXecInput | ||||
| label="Amount" | |||||
| name="amount" | name="amount" | ||||
| value={formData.amount} | value={formData.amount} | ||||
| selectValue={selectedCurrency} | selectValue={selectedCurrency} | ||||
| selectDisabled={ | selectDisabled={ | ||||
| 'amount' in parsedAddressInput || | 'amount' in | ||||
| parsedAddressInput || | |||||
| txInfoFromUrl !== false | txInfoFromUrl !== false | ||||
| } | } | ||||
| inputDisabled={ | inputDisabled={ | ||||
| priceApiError || | priceApiError || | ||||
| (txInfoFromUrl !== false && | (txInfoFromUrl !== false && | ||||
| 'value' in txInfoFromUrl && | 'value' in txInfoFromUrl && | ||||
| txInfoFromUrl.value !== | txInfoFromUrl.value !== | ||||
| 'null' && | 'null' && | ||||
| txInfoFromUrl.value !== | txInfoFromUrl.value !== | ||||
| 'undefined') || | 'undefined') || | ||||
| 'amount' in parsedAddressInput | 'amount' in parsedAddressInput | ||||
| } | } | ||||
| fiatCode={settings.fiatCurrency.toUpperCase()} | fiatCode={settings.fiatCurrency.toUpperCase()} | ||||
| error={sendAmountError} | error={sendAmountError} | ||||
| handleInput={handleAmountChange} | handleInput={handleAmountChange} | ||||
| handleSelect={ | handleSelect={ | ||||
| handleSelectedCurrencyChange | handleSelectedCurrencyChange | ||||
| } | } | ||||
| handleOnMax={onMax} | handleOnMax={onMax} | ||||
| /> | /> | ||||
| )} | )} | ||||
| </SendToOneInputForm> | </SendToOneInputForm> | ||||
| </SendToOneHolder> | </SendToOneHolder> | ||||
| {isBip21TokenSend(parsedAddressInput) && | {isBip21TokenSend(parsedAddressInput) && | ||||
| tokenIdQueryError && ( | tokenIdQueryError && ( | ||||
| <Alert> | <Alert> | ||||
| Error querying token info for{' '} | Error querying token info for{' '} | ||||
| {parsedAddressInput.token_id.value} | {parsedAddressInput.token_id.value} | ||||
| </Alert> | </Alert> | ||||
| )} | )} | ||||
| {priceApiError && ( | {priceApiError && ( | ||||
| <AlertMsg> | <AlertMsg> | ||||
| Error fetching fiat price. Setting send by{' '} | Error fetching fiat price. Setting send by{' '} | ||||
| {supportedFiatCurrencies[ | {supportedFiatCurrencies[ | ||||
| settings.fiatCurrency | settings.fiatCurrency | ||||
| ].slug.toUpperCase()}{' '} | ].slug.toUpperCase()}{' '} | ||||
| disabled | disabled | ||||
| </AlertMsg> | </AlertMsg> | ||||
| )} | )} | ||||
| <SendToManyHolder> | <SendToManyHolder> | ||||
| <TextArea | <TextArea | ||||
| label="Send to many" | |||||
| placeholder={`One address & amount per line, separated by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500 \necash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700`} | placeholder={`One address & amount per line, separated by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500 \necash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700`} | ||||
| name="multiAddressInput" | name="multiAddressInput" | ||||
| handleInput={e => handleMultiAddressChange(e)} | handleInput={e => | ||||
| handleMultiAddressChange(e) | |||||
| } | |||||
| value={formData.multiAddressInput} | value={formData.multiAddressInput} | ||||
| error={multiSendAddressError} | error={multiSendAddressError} | ||||
| /> | /> | ||||
| </SendToManyHolder> | </SendToManyHolder> | ||||
| </InputModesHolder> | </InputModesHolder> | ||||
| )} | )} | ||||
| {!isTokenMode && ( | {!isTokenMode && ( | ||||
| <SendXecForm> | <SendXecForm> | ||||
| <SendXecRow> | <AdvancedSection> | ||||
| <SwitchAndLabel> | <AdvancedHeader | ||||
| <Switch | type="button" | ||||
| name="Toggle Cashtab Msg" | onClick={handleAdvancedHeaderClick} | ||||
| on="✉️" | > | ||||
| off="✉️" | Advanced{' '} | ||||
| checked={sendWithCashtabMsg} | <AdvancedChevron $open={advancedOpen} /> | ||||
| </AdvancedHeader> | |||||
| {advancedOpen && ( | |||||
| <> | |||||
| <AdvancedButtonsRow> | |||||
| <AdvancedButton | |||||
| type="button" | |||||
| $active={isOneToManyXECSend} | |||||
| onClick={() => | |||||
| setIsOneToManyXECSend( | |||||
| !isOneToManyXECSend, | |||||
| ) | |||||
| } | |||||
| disabled={ | disabled={ | ||||
| txInfoFromUrl !== false || | txInfoFromUrl !== false || | ||||
| 'queryString' in parsedAddressInput | 'queryString' in | ||||
| parsedAddressInput | |||||
| } | } | ||||
| handleToggle={() => { | > | ||||
| // If we are sending a Cashtab msg, toggle off op_return_raw | Send to many | ||||
| </AdvancedButton> | |||||
| <AdvancedButton | |||||
| type="button" | |||||
| $active={sendWithCashtabMsg} | |||||
| onClick={() => { | |||||
| if ( | if ( | ||||
| !sendWithCashtabMsg && | !sendWithCashtabMsg && | ||||
| sendWithOpReturnRaw | sendWithOpReturnRaw | ||||
| ) { | ) { | ||||
| setSendWithOpReturnRaw(false); | setSendWithOpReturnRaw( | ||||
| false, | |||||
| ); | |||||
| } | } | ||||
| setSendWithCashtabMsg( | setSendWithCashtabMsg( | ||||
| !sendWithCashtabMsg, | !sendWithCashtabMsg, | ||||
| ); | ); | ||||
| }} | }} | ||||
| /> | disabled={ | ||||
| <SwitchLabel>Cashtab Msg </SwitchLabel> | txInfoFromUrl !== false || | ||||
| </SwitchAndLabel> | 'queryString' in | ||||
| </SendXecRow> | parsedAddressInput | ||||
| } | |||||
| > | |||||
| Message | |||||
| </AdvancedButton> | |||||
| <AdvancedButton | |||||
| type="button" | |||||
| $active={sendWithOpReturnRaw} | |||||
| onClick={() => { | |||||
| if ( | |||||
| !sendWithOpReturnRaw && | |||||
| sendWithCashtabMsg | |||||
| ) { | |||||
| setSendWithCashtabMsg( | |||||
| false, | |||||
| ); | |||||
| } | |||||
| setSendWithOpReturnRaw( | |||||
| !sendWithOpReturnRaw, | |||||
| ); | |||||
| }} | |||||
| disabled={ | |||||
| txInfoFromUrl !== false || | |||||
| 'queryString' in | |||||
| parsedAddressInput | |||||
| } | |||||
| > | |||||
| op_return_raw | |||||
| </AdvancedButton> | |||||
| </AdvancedButtonsRow> | |||||
| <AdvancedContent> | |||||
| {sendWithCashtabMsg && ( | {sendWithCashtabMsg && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <TextArea | <TextArea | ||||
| name="cashtabMsg" | name="cashtabMsg" | ||||
| height={62} | height={62} | ||||
| placeholder={`Include a public Cashtab msg with this tx ${ | placeholder={`Include a public Cashtab msg with this tx ${ | ||||
| location && | location && | ||||
| location.state && | location.state && | ||||
| location.state.airdropTokenId | location.state | ||||
| .airdropTokenId | |||||
| ? `(max ${ | ? `(max ${ | ||||
| opreturnConfig.cashtabMsgByteLimit - | opreturnConfig.cashtabMsgByteLimit - | ||||
| localAirdropTxAddedBytes | localAirdropTxAddedBytes | ||||
| } bytes)` | } bytes)` | ||||
| : `(max ${opreturnConfig.cashtabMsgByteLimit} bytes)` | : `(max ${opreturnConfig.cashtabMsgByteLimit} bytes)` | ||||
| }`} | }`} | ||||
| value={formData.cashtabMsg} | value={ | ||||
| formData.cashtabMsg | |||||
| } | |||||
| error={cashtabMsgError} | error={cashtabMsgError} | ||||
| showCount | showCount | ||||
| customCount={getCashtabMsgByteCount( | customCount={getCashtabMsgByteCount( | ||||
| formData.cashtabMsg, | formData.cashtabMsg, | ||||
| )} | )} | ||||
| max={ | max={ | ||||
| location && | location && | ||||
| location.state && | location.state && | ||||
| location.state.airdropTokenId | location.state | ||||
| .airdropTokenId | |||||
| ? opreturnConfig.cashtabMsgByteLimit - | ? opreturnConfig.cashtabMsgByteLimit - | ||||
| localAirdropTxAddedBytes | localAirdropTxAddedBytes | ||||
| : opreturnConfig.cashtabMsgByteLimit | : opreturnConfig.cashtabMsgByteLimit | ||||
| } | } | ||||
| handleInput={e => handleCashtabMsgChange(e)} | handleInput={e => | ||||
| /> | handleCashtabMsgChange( | ||||
| </SendXecRow> | e, | ||||
| )} | ) | ||||
| <SendXecRow> | |||||
| <SwitchAndLabel> | |||||
| <Switch | |||||
| name="Toggle op_return_raw" | |||||
| checked={sendWithOpReturnRaw} | |||||
| disabled={ | |||||
| txInfoFromUrl !== false || | |||||
| 'queryString' in parsedAddressInput | |||||
| } | |||||
| handleToggle={() => { | |||||
| // If we are sending with op_return_raw, toggle off CashtabMsg | |||||
| if ( | |||||
| !sendWithOpReturnRaw && | |||||
| sendWithCashtabMsg | |||||
| ) { | |||||
| setSendWithCashtabMsg(false); | |||||
| } | } | ||||
| setSendWithOpReturnRaw( | |||||
| !sendWithOpReturnRaw, | |||||
| ); | |||||
| }} | |||||
| /> | /> | ||||
| <SwitchLabel>op_return_raw</SwitchLabel> | |||||
| </SwitchAndLabel> | |||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | |||||
| </AdvancedContent> | |||||
| </> | |||||
| )} | |||||
| </AdvancedSection> | |||||
| {isBip21TokenSend(parsedAddressInput) && | {isBip21TokenSend(parsedAddressInput) && | ||||
| tokenIdQueryError === false && ( | tokenIdQueryError === false && ( | ||||
| <> | <> | ||||
| {isValidFirmaRedeemTx( | {isValidFirmaRedeemTx( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) ? ( | ) ? ( | ||||
| <ParsedTokenSend | <ParsedTokenSend | ||||
| /** make sure the bottom is not stuck behind SEND button */ | /** make sure the bottom is not stuck behind SEND button */ | ||||
| style={{ marginBottom: '48px' }} | style={{ marginBottom: '48px' }} | ||||
| > | > | ||||
| <FirmaRedeemLogoWrapper> | <FirmaRedeemLogoWrapper> | ||||
| <FirmaIcon /> | <FirmaIcon /> | ||||
| <TetherIcon /> | <TetherIcon /> | ||||
| </FirmaRedeemLogoWrapper> | </FirmaRedeemLogoWrapper> | ||||
| <FirmaRedeemTextAndCopy> | <FirmaRedeemTextAndCopy> | ||||
| On tx finalized,{' '} | On tx finalized,{' '} | ||||
| {( | {( | ||||
| Number( | Number( | ||||
| parsedAddressInput | parsedAddressInput | ||||
| .token_decimalized_qty | .token_decimalized_qty | ||||
| .value, | .value, | ||||
| ) - | ) - | ||||
| getFirmaRedeemFee( | getFirmaRedeemFee( | ||||
| Number( | Number( | ||||
| parsedAddressInput | parsedAddressInput | ||||
| .token_decimalized_qty | .token_decimalized_qty | ||||
| .value, | .value, | ||||
| ), | ), | ||||
| ) | ) | ||||
| ).toLocaleString(userLocale, { | ).toLocaleString( | ||||
| userLocale, | |||||
| { | |||||
| maximumFractionDigits: 4, | maximumFractionDigits: 4, | ||||
| minimumFractionDigits: 4, | minimumFractionDigits: 4, | ||||
| })}{' '} | }, | ||||
| )}{' '} | |||||
| USDT will be sent to{' '} | USDT will be sent to{' '} | ||||
| {parsedFirma.data.slice(0, 3)} | {parsedFirma.data.slice( | ||||
| 0, | |||||
| 3, | |||||
| )} | |||||
| ... | ... | ||||
| {parsedFirma.data.slice( | {parsedFirma.data.slice( | ||||
| -3, | -3, | ||||
| )}{' '} | )}{' '} | ||||
| <CopyIconButton | <CopyIconButton | ||||
| name="Copy SOL addr" | name="Copy SOL addr" | ||||
| data={parsedFirma.data} | data={parsedFirma.data} | ||||
| showToast | showToast | ||||
| /> | /> | ||||
| </FirmaRedeemTextAndCopy> | </FirmaRedeemTextAndCopy> | ||||
| </ParsedTokenSend> | </ParsedTokenSend> | ||||
| ) : ( | ) : ( | ||||
| <ParsedTokenSend> | <ParsedTokenSend> | ||||
| <TokenIcon | <TokenIcon | ||||
| size={64} | size={64} | ||||
| tokenId={ | tokenId={ | ||||
| parsedAddressInput.token_id | parsedAddressInput | ||||
| .value | .token_id.value | ||||
| } | } | ||||
| /> | /> | ||||
| Sending {decimalizedTokenQty}{' '} | Sending {decimalizedTokenQty}{' '} | ||||
| {nameAndTicker} to {addressPreview} | {nameAndTicker} to{' '} | ||||
| {addressPreview} | |||||
| </ParsedTokenSend> | </ParsedTokenSend> | ||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} | ||||
| {isBip21TokenSend(parsedAddressInput) && | {isBip21TokenSend(parsedAddressInput) && | ||||
| !isValidFirmaRedeemTx(parsedAddressInput) && | !isValidFirmaRedeemTx(parsedAddressInput) && | ||||
| typeof parsedAddressInput.firma?.value === | typeof parsedAddressInput.firma?.value === | ||||
| 'string' && | 'string' && | ||||
| parsedAddressInput.firma.error === false && ( | parsedAddressInput.firma.error === false && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <ParsedBip21InfoRow> | <ParsedBip21InfoRow> | ||||
| <ParsedBip21InfoLabel> | <ParsedBip21InfoLabel> | ||||
| Parsed firma | Parsed firma | ||||
| </ParsedBip21InfoLabel> | </ParsedBip21InfoLabel> | ||||
| <ParsedBip21Info> | <ParsedBip21Info> | ||||
| <b>{parsedFirma.protocol}</b> | <b>{parsedFirma.protocol}</b> | ||||
| <br /> | <br /> | ||||
| {parsedFirma.data} | {parsedFirma.data} | ||||
| </ParsedBip21Info> | </ParsedBip21Info> | ||||
| </ParsedBip21InfoRow> | </ParsedBip21InfoRow> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | )} | ||||
| {sendWithOpReturnRaw && ( | {isBip21MultipleOutputsSafe(parsedAddressInput) && ( | ||||
| <SendXecRow> | |||||
| <ParsedBip21InfoRow> | |||||
| <ParsedBip21InfoLabel> | |||||
| Parsed BIP21 outputs | |||||
| </ParsedBip21InfoLabel> | |||||
| <ParsedBip21Info> | |||||
| <ol> | |||||
| <li | |||||
| title={ | |||||
| parsedAddressInput | |||||
| .address | |||||
| .value as string | |||||
| } | |||||
| >{`${( | |||||
| parsedAddressInput.address | |||||
| .value as string | |||||
| ).slice(6, 12)}...${( | |||||
| parsedAddressInput.address | |||||
| .value as string | |||||
| ).slice(-6)}, ${parseFloat( | |||||
| parsedAddressInput.amount | |||||
| .value, | |||||
| ).toLocaleString(userLocale, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} XEC`}</li> | |||||
| {Array.from( | |||||
| parsedAddressInput | |||||
| .parsedAdditionalXecOutputs | |||||
| .value, | |||||
| ).map( | |||||
| ([addr, amount], index) => { | |||||
| return ( | |||||
| <li | |||||
| key={index} | |||||
| title={addr} | |||||
| >{`${addr.slice( | |||||
| 6, | |||||
| 12, | |||||
| )}...${addr.slice( | |||||
| -6, | |||||
| )}, ${parseFloat( | |||||
| amount, | |||||
| ).toLocaleString( | |||||
| userLocale, | |||||
| { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| }, | |||||
| )} XEC`}</li> | |||||
| ); | |||||
| }, | |||||
| )} | |||||
| </ol> | |||||
| </ParsedBip21Info> | |||||
| </ParsedBip21InfoRow> | |||||
| </SendXecRow> | |||||
| )} | |||||
| {advancedOpen && sendWithOpReturnRaw && ( | |||||
| <> | <> | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <TextArea | <TextArea | ||||
| name="opReturnRaw" | name="opReturnRaw" | ||||
| height={62} | height={62} | ||||
| placeholder={`(Advanced) Enter raw hex to be included with this transaction's OP_RETURN`} | placeholder={`(Advanced) Enter raw hex to be included with this transaction's OP_RETURN`} | ||||
| value={formData.opReturnRaw} | value={formData.opReturnRaw} | ||||
| error={opReturnRawError} | error={opReturnRawError} | ||||
| disabled={ | disabled={ | ||||
| txInfoFromUrl !== false || | txInfoFromUrl !== false || | ||||
| 'queryString' in parsedAddressInput | 'queryString' in | ||||
| parsedAddressInput | |||||
| } | } | ||||
| showCount | showCount | ||||
| max={ | max={ | ||||
| 2 * opReturn.opreturnParamByteLimit | 2 * | ||||
| opReturn.opreturnParamByteLimit | |||||
| } | } | ||||
| handleInput={handleOpReturnRawInput} | handleInput={handleOpReturnRawInput} | ||||
| /> | /> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| {opReturnRawError === false && | {opReturnRawError === false && | ||||
| formData.opReturnRaw !== '' && ( | formData.opReturnRaw !== '' && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <ParsedBip21InfoRow> | <ParsedBip21InfoRow> | ||||
| <ParsedBip21InfoLabel> | <ParsedBip21InfoLabel> | ||||
| Parsed op_return_raw | Parsed op_return_raw | ||||
| </ParsedBip21InfoLabel> | </ParsedBip21InfoLabel> | ||||
| <ParsedBip21Info> | <ParsedBip21Info> | ||||
| <b> | <b> | ||||
| { | { | ||||
| parsedOpReturnRaw.protocol | parsedOpReturnRaw.protocol | ||||
| } | } | ||||
| </b> | </b> | ||||
| <br /> | <br /> | ||||
| {parsedOpReturnRaw.data} | {parsedOpReturnRaw.data} | ||||
| </ParsedBip21Info> | </ParsedBip21Info> | ||||
| </ParsedBip21InfoRow> | </ParsedBip21InfoRow> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} | ||||
| {/* Parsed input_data_raw when present in BIP21 (XEC mode) */} | {/* Parsed input_data_raw when present in BIP21 (XEC mode) */} | ||||
| {(parsedInputDataRaw.protocol !== '' || | {(parsedInputDataRaw.protocol !== '' || | ||||
| parsedInputDataRaw.data !== '') && ( | parsedInputDataRaw.data !== '') && ( | ||||
| <SendXecRow> | <SendXecRow> | ||||
| <ParsedBip21InfoRow> | <ParsedBip21InfoRow> | ||||
| <ParsedBip21InfoLabel> | <ParsedBip21InfoLabel> | ||||
| Parsed input_data_raw | Parsed input_data_raw | ||||
| </ParsedBip21InfoLabel> | </ParsedBip21InfoLabel> | ||||
| <ParsedBip21Info> | <ParsedBip21Info> | ||||
| <b>{parsedInputDataRaw.protocol}</b> | <b>{parsedInputDataRaw.protocol}</b> | ||||
| <br /> | <br /> | ||||
| {parsedInputDataRaw.data} | {parsedInputDataRaw.data} | ||||
| </ParsedBip21Info> | </ParsedBip21Info> | ||||
| </ParsedBip21InfoRow> | </ParsedBip21InfoRow> | ||||
| </SendXecRow> | </SendXecRow> | ||||
| )} | )} | ||||
| {isBip21MultipleOutputsSafe(parsedAddressInput) && ( | |||||
| <SendXecRow> | |||||
| <ParsedBip21InfoRow> | |||||
| <ParsedBip21InfoLabel> | |||||
| Parsed BIP21 outputs | |||||
| </ParsedBip21InfoLabel> | |||||
| <ParsedBip21Info> | |||||
| <ol> | |||||
| <li | |||||
| title={ | |||||
| parsedAddressInput.address | |||||
| .value as string | |||||
| } | |||||
| >{`${( | |||||
| parsedAddressInput.address | |||||
| .value as string | |||||
| ).slice(6, 12)}...${( | |||||
| parsedAddressInput.address | |||||
| .value as string | |||||
| ).slice(-6)}, ${parseFloat( | |||||
| parsedAddressInput.amount.value, | |||||
| ).toLocaleString(userLocale, { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| })} XEC`}</li> | |||||
| {Array.from( | |||||
| parsedAddressInput | |||||
| .parsedAdditionalXecOutputs | |||||
| .value, | |||||
| ).map(([addr, amount], index) => { | |||||
| return ( | |||||
| <li | |||||
| key={index} | |||||
| title={addr} | |||||
| >{`${addr.slice( | |||||
| 6, | |||||
| 12, | |||||
| )}...${addr.slice( | |||||
| -6, | |||||
| )}, ${parseFloat( | |||||
| amount, | |||||
| ).toLocaleString( | |||||
| userLocale, | |||||
| { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| }, | |||||
| )} XEC`}</li> | |||||
| ); | |||||
| })} | |||||
| </ol> | |||||
| </ParsedBip21Info> | |||||
| </ParsedBip21InfoRow> | |||||
| </SendXecRow> | |||||
| )} | |||||
| </SendXecForm> | </SendXecForm> | ||||
| )} | )} | ||||
| {!priceApiError && !isTokenMode && ( | |||||
| <AmountPreviewCtn> | <AmountPreviewCtn> | ||||
| {!priceApiError && ( | <AmountPreviewLabel>Sending</AmountPreviewLabel> | ||||
| <> | <AmountPreviewValues> | ||||
| {isOneToManyXECSend ? ( | {isOneToManyXECSend ? ( | ||||
| <LocaleFormattedValue> | <LocaleFormattedValue> | ||||
| {formatBalance( | {formatBalance( | ||||
| multiSendTotal.toString(), | multiSendTotal.toString(), | ||||
| userLocale, | userLocale, | ||||
| ) + | ) + | ||||
| ' ' + | ' ' + | ||||
| selectedCurrency} | selectedCurrency} | ||||
| </LocaleFormattedValue> | </LocaleFormattedValue> | ||||
| ) : isBip21MultipleOutputsSafe( | ) : isBip21MultipleOutputsSafe( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) ? ( | ) ? ( | ||||
| <LocaleFormattedValue> | <LocaleFormattedValue> | ||||
| {bip21MultipleOutputsFormattedTotalSendXec.toLocaleString( | {bip21MultipleOutputsFormattedTotalSendXec.toLocaleString( | ||||
| userLocale, | userLocale, | ||||
| { | { | ||||
| maximumFractionDigits: 2, | maximumFractionDigits: 2, | ||||
| minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
| }, | }, | ||||
| )}{' '} | )}{' '} | ||||
| XEC | XEC | ||||
| </LocaleFormattedValue> | </LocaleFormattedValue> | ||||
| ) : ( | ) : ( | ||||
| <LocaleFormattedValue> | <LocaleFormattedValue> | ||||
| {!isNaN(parseFloat(formData.amount)) | {!isNaN(parseFloat(formData.amount)) | ||||
| ? formatBalance( | ? formatBalance( | ||||
| formData.amount, | formData.amount, | ||||
| userLocale, | userLocale, | ||||
| ) + | ) + | ||||
| ' ' + | ' ' + | ||||
| selectedCurrency | selectedCurrency | ||||
| : ''} | : `0 ${selectedCurrency}`} | ||||
| </LocaleFormattedValue> | </LocaleFormattedValue> | ||||
| )} | )} | ||||
| <ConvertAmount> | <AmountPreviewFiat> | ||||
| {fiatPriceString !== '' && '='}{' '} | {fiatPriceString !== '' && '= '} | ||||
| {fiatPriceString} | {fiatPriceString} | ||||
| </ConvertAmount> | </AmountPreviewFiat> | ||||
| </> | </AmountPreviewValues> | ||||
| )} | |||||
| </AmountPreviewCtn> | </AmountPreviewCtn> | ||||
| )} | |||||
| <SendButtonContainer> | <SendButtonContainer> | ||||
| {isUrlBasedTransaction ? ( | {isUrlBasedTransaction ? ( | ||||
| <> | <> | ||||
| <PrimaryButton | <PrimaryButton | ||||
| disabled={ | disabled={ | ||||
| isTokenMode | isTokenMode | ||||
| ? isBip21TokenSend( | ? isBip21TokenSend( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) && | ) && | ||||
| selectedTokenId !== null && | selectedTokenId !== null && | ||||
| selectedTokenId === | selectedTokenId === | ||||
| parsedAddressInput.token_id.value | parsedAddressInput.token_id | ||||
| .value | |||||
| ? tokenError !== false || | ? tokenError !== false || | ||||
| tokenIdQueryError || | tokenIdQueryError || | ||||
| shouldShowSlpErrorForEmppRaw || | shouldShowSlpErrorForEmppRaw || | ||||
| isSending | isSending | ||||
| : disableTokenSendButton | : disableTokenSendButton | ||||
| : (!isBip21TokenSend( | : (!isBip21TokenSend( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) && | ) && | ||||
| disableSendButton) || | disableSendButton) || | ||||
| (isBip21TokenSend( | (isBip21TokenSend( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) && | ) && | ||||
| tokenError !== false) || | tokenError !== false) || | ||||
| tokenIdQueryError || | tokenIdQueryError || | ||||
| isSending | isSending | ||||
| } | } | ||||
| onClick={ | onClick={ | ||||
| isTokenMode | isTokenMode | ||||
| ? isBip21TokenSend( | ? isBip21TokenSend( | ||||
| parsedAddressInput, | parsedAddressInput, | ||||
| ) && | ) && | ||||
| selectedTokenId !== null && | selectedTokenId !== null && | ||||
| selectedTokenId === | selectedTokenId === | ||||
| parsedAddressInput.token_id.value | parsedAddressInput.token_id | ||||
| .value | |||||
| ? checkForConfirmationBeforeBip21TokenSend | ? checkForConfirmationBeforeBip21TokenSend | ||||
| : checkForConfirmationBeforeSendToken | : checkForConfirmationBeforeSendToken | ||||
| : isBip21TokenSend(parsedAddressInput) | : isBip21TokenSend( | ||||
| parsedAddressInput, | |||||
| ) | |||||
| ? checkForConfirmationBeforeBip21TokenSend | ? checkForConfirmationBeforeBip21TokenSend | ||||
| : checkForConfirmationBeforeSendXec | : checkForConfirmationBeforeSendXec | ||||
| } | } | ||||
| > | > | ||||
| {isSending ? <InlineLoader /> : 'Accept'} | {isSending ? <InlineLoader /> : 'Accept'} | ||||
| </PrimaryButton> | </PrimaryButton> | ||||
| <SecondaryButton | <SecondaryButton | ||||
| disabled={isSending} | disabled={isSending} | ||||
| onClick={() => handleTransactionRejection()} | onClick={() => handleTransactionRejection()} | ||||
| style={{ | style={{ | ||||
| marginLeft: '10px', | marginLeft: '10px', | ||||
| }} | }} | ||||
| > | > | ||||
| Reject | Reject | ||||
| </SecondaryButton> | </SecondaryButton> | ||||
| </> | </> | ||||
| ) : ( | ) : ( | ||||
| <PrimaryButton | <PrimaryButton | ||||
| disabled={ | disabled={ | ||||
| isTokenMode | isTokenMode | ||||
| ? disableTokenSendButton | ? disableTokenSendButton | ||||
| : (!isBip21TokenSend(parsedAddressInput) && | : (!isBip21TokenSend( | ||||
| parsedAddressInput, | |||||
| ) && | |||||
| disableSendButton) || | disableSendButton) || | ||||
| (isBip21TokenSend(parsedAddressInput) && | (isBip21TokenSend( | ||||
| parsedAddressInput, | |||||
| ) && | |||||
| tokenError !== false) || | tokenError !== false) || | ||||
| tokenIdQueryError | tokenIdQueryError | ||||
| } | } | ||||
| onClick={ | onClick={ | ||||
| isTokenMode | isTokenMode | ||||
| ? checkForConfirmationBeforeSendToken | ? checkForConfirmationBeforeSendToken | ||||
| : isBip21TokenSend(parsedAddressInput) | : isBip21TokenSend(parsedAddressInput) | ||||
| ? checkForConfirmationBeforeBip21TokenSend | ? checkForConfirmationBeforeBip21TokenSend | ||||
| : checkForConfirmationBeforeSendXec | : checkForConfirmationBeforeSendXec | ||||
| } | } | ||||
| > | > | ||||
| {isSending ? <InlineLoader /> : 'Send'} | {isSending ? <InlineLoader /> : 'Send'} | ||||
| </PrimaryButton> | </PrimaryButton> | ||||
| )} | )} | ||||
| </SendButtonContainer> | </SendButtonContainer> | ||||
| {apiError && <ApiError />} | {apiError && <ApiError />} | ||||
| </SendContentWrapper> | |||||
| </OuterCtn> | </OuterCtn> | ||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default SendXec; | export default SendXec; | ||||