import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { InputAdornment, LinearProgress, ButtonGroup, Slider, Switch, Tooltip, Button, Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Box, useTheme, Collapse, IconButton, } from '@mui/material'; import { LimitList, Maker, Favorites, defaultMaker } from '../../models'; import { LocalizationProvider, TimePicker } from '@mui/x-date-pickers'; import DateFnsUtils from '@date-io/date-fns'; import { useHistory } from 'react-router-dom'; import { ConfirmationDialog } from '../Dialogs'; import { apiClient } from '../../services/api'; import { FlagWithProps } from '../Icons'; import AutocompletePayments from './AutocompletePayments'; import AmountRange from './AmountRange'; import currencyDict from '../../../static/assets/currencies.json'; import { pn } from '../../utils'; import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit } from '@mui/icons-material'; import { LoadingButton } from '@mui/lab'; import { Page } from '../../basic/NavBar'; interface MakerFormProps { limits: { list: LimitList; loading: boolean }; fetchLimits: () => void; pricingMethods?: boolean; maker: Maker; fav: Favorites; setFav: (state: Favorites) => void; setMaker: (state: Maker) => void; disableRequest?: boolean; collapseAll?: boolean; onSubmit?: () => void; onReset?: () => void; submitButtonLabel?: string; onOrderCreated?: (id: number) => void; hasRobot?: boolean; setPage?: (state: Page) => void; baseUrl: string; } const MakerForm = ({ limits, fetchLimits, pricingMethods = false, fav, setFav, maker, setMaker, disableRequest = false, collapseAll = false, onSubmit = () => {}, onReset = () => {}, submitButtonLabel = 'Create Order', onOrderCreated = () => null, hasRobot = true, setPage = () => null, baseUrl, }: MakerFormProps): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); const history = useHistory(); const [badRequest, setBadRequest] = useState(null); const [amountLimits, setAmountLimits] = useState([1, 1000]); const [satoshisLimits, setSatoshisLimits] = useState([20000, 4000000]); const [currentPrice, setCurrentPrice] = useState('...'); const [currencyCode, setCurrencyCode] = useState('USD'); const [openDialogs, setOpenDialogs] = useState(false); const [submittingRequest, setSubmittingRequest] = useState(false); const maxRangeAmountMultiple = 7.8; const minRangeAmountMultiple = 1.6; const amountSafeThresholds = [1.03, 0.98]; useEffect(() => { setCurrencyCode(currencyDict[fav.currency == 0 ? 1 : fav.currency]); if (Object.keys(limits.list).length === 0) { fetchLimits().then((data) => { updateAmountLimits(data, fav.currency, maker.premium); updateCurrentPrice(data, fav.currency, maker.premium); updateSatoshisLimits(data); }); } else { updateAmountLimits(limits.list, fav.currency, maker.premium); updateCurrentPrice(limits.list, fav.currency, maker.premium); updateSatoshisLimits(limits.list); fetchLimits(); } }, []); const updateAmountLimits = function (limitList: LimitList, currency: number, premium: number) { const index = currency == 0 ? 1 : currency; let minAmountLimit: number = limitList[index].min_amount * (1 + premium / 100); let maxAmountLimit: number = limitList[index].max_amount * (1 + premium / 100); // apply thresholds to ensure good request minAmountLimit = minAmountLimit * amountSafeThresholds[0]; maxAmountLimit = maxAmountLimit * amountSafeThresholds[1]; setAmountLimits([minAmountLimit, maxAmountLimit]); }; const updateSatoshisLimits = function (limitList: LimitList) { const minAmount: number = limitList[1000].min_amount * 100000000; const maxAmount: number = limitList[1000].max_amount * 100000000; setSatoshisLimits([minAmount, maxAmount]); }; const updateCurrentPrice = function (limitsList: LimitList, currency: number, premium: number) { const index = currency == 0 ? 1 : currency; let price = '...'; if (maker.isExplicit && maker.amount > 0 && maker.satoshis > 0) { price = maker.amount / (maker.satoshis / 100000000); } else if (!maker.is_explicit) { price = limitsList[index].price * (1 + premium / 100); } setCurrentPrice(parseFloat(Number(price).toPrecision(5))); }; const handleCurrencyChange = function (newCurrency: number) { const currencyCode: string = currencyDict[newCurrency]; setCurrencyCode(currencyCode); setFav({ ...fav, currency: newCurrency, }); updateAmountLimits(limits.list, newCurrency, maker.premium); updateCurrentPrice(limits.list, newCurrency, maker.premium); if (maker.advancedOptions) { setMaker({ ...maker, minAmount: parseFloat(Number(limits.list[newCurrency].max_amount * 0.25).toPrecision(2)), maxAmount: parseFloat(Number(limits.list[newCurrency].max_amount * 0.75).toPrecision(2)), }); } }; const handlePaymentMethodChange = function (paymentArray: string[]) { let str = ''; const arrayLength = paymentArray.length; for (let i = 0; i < arrayLength; i++) { str += paymentArray[i].name + ' '; } const paymentMethodText = str.slice(0, -1); setMaker({ ...maker, paymentMethods: paymentArray, paymentMethodsText: paymentMethodText, badPaymentMethod: paymentMethodText.length > 50, }); }; const handleMinAmountChange = function (e) { setMaker({ ...maker, minAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)), }); }; const handleMaxAmountChange = function (e) { setMaker({ ...maker, maxAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)), }); }; const handlePremiumChange = function (e: object) { const max = 999; const min = -100; const newPremium = Math.floor(e.target.value * Math.pow(10, 2)) / Math.pow(10, 2); let premium: number = newPremium; let badPremiumText: string = ''; if (newPremium > 999) { badPremiumText = t('Must be less than {{max}}%', { max }); premium = 999; } else if (newPremium <= -100) { badPremiumText = t('Must be more than {{min}}%', { min }); premium = -99.99; } updateCurrentPrice(limits.list, fav.currency, premium); updateAmountLimits(limits.list, fav.currency, premium); setMaker({ ...maker, premium, badPremiumText, }); }; const handleSatoshisChange = function (e: object) { const newSatoshis = e.target.value; let badSatoshisText: string = ''; let satoshis: string = newSatoshis; if (newSatoshis > satoshisLimits[1]) { badSatoshisText = t('Must be less than {{maxSats}', { maxSats: pn(satoshisLimits[1]) }); satoshis = satoshisLimits[1]; } if (newSatoshis < satoshisLimits[0]) { badSatoshisText = t('Must be more than {{minSats}}', { minSats: pn(satoshisLimits[0]) }); satoshis = satoshisLimits[0]; } setMaker({ ...maker, satoshis, badSatoshisText, }); }; const handleClickRelative = function () { setMaker({ ...maker, isExplicit: false, }); }; const handleClickExplicit = function () { if (!maker.advancedOptions) { setMaker({ ...maker, isExplicit: true, }); } }; const handleCreateOrder = function () { if (!disableRequest) { setSubmittingRequest(true); const body = { type: fav.type == 0 ? 1 : 0, currency: fav.currency == 0 ? 1 : fav.currency, amount: maker.advancedOptions ? null : maker.amount, has_range: maker.advancedOptions, min_amount: maker.advancedOptions ? maker.minAmount : null, max_amount: maker.advancedOptions ? maker.maxAmount : null, payment_method: maker.paymentMethodsText === '' ? 'not specified' : maker.paymentMethodsText, is_explicit: maker.isExplicit, premium: maker.isExplicit ? null : maker.premium == '' ? 0 : maker.premium, satoshis: maker.isExplicit ? maker.satoshis : null, public_duration: maker.publicDuration, escrow_duration: maker.escrowDuration, bond_size: maker.bondSize, }; apiClient.post(baseUrl, '/api/make/', body).then((data: object) => { setBadRequest(data.bad_request); if (data.id) { onOrderCreated(data.id); } setSubmittingRequest(false); }); } setOpenDialogs(false); }; const handleChangePublicDuration = function (date: Date) { const d = new Date(date); const hours: number = d.getHours(); const minutes: number = d.getMinutes(); const total_secs: number = hours * 60 * 60 + minutes * 60; setMaker({ ...maker, publicExpiryTime: date, publicDuration: total_secs, }); }; const handleChangeEscrowDuration = function (date: Date) { const d = new Date(date); const hours: number = d.getHours(); const minutes: number = d.getMinutes(); const total_secs: number = hours * 60 * 60 + minutes * 60; setMaker({ ...maker, escrowExpiryTime: date, escrowDuration: total_secs, }); }; const handleClickAdvanced = function () { if (maker.advancedOptions) { handleClickRelative(); setMaker({ ...maker, advancedOptions: false }); } else { resetRange(true); } }; const minAmountError = function () { return ( maker.minAmount < amountLimits[0] * 0.99 || maker.maxAmount < maker.minAmount || maker.minAmount < maker.maxAmount / (maxRangeAmountMultiple + 0.15) || maker.minAmount * (minRangeAmountMultiple - 0.1) > maker.maxAmount ); }; const maxAmountError = function () { return ( maker.maxAmount > amountLimits[1] * 1.01 || maker.maxAmount < maker.minAmount || maker.minAmount < maker.maxAmount / (maxRangeAmountMultiple + 0.15) || maker.minAmount * (minRangeAmountMultiple - 0.1) > maker.maxAmount ); }; const resetRange = function (advancedOptions: boolean) { const index = fav.currency === 0 ? 1 : fav.currency; const minAmount = maker.amount ? parseFloat((maker.amount / 2).toPrecision(2)) : parseFloat(Number(limits.list[index].max_amount * 0.25).toPrecision(2)); const maxAmount = maker.amount ? parseFloat(maker.amount) : parseFloat(Number(limits.list[index].max_amount * 0.75).toPrecision(2)); setMaker({ ...maker, advancedOptions, minAmount, maxAmount, }); }; const handleRangeAmountChange = function (e: any, newValue, activeThumb: number) { let minAmount = e.target.value[0]; let maxAmount = e.target.value[1]; minAmount = Math.min( (amountLimits[1] * amountSafeThresholds[1]) / minRangeAmountMultiple, minAmount, ); maxAmount = Math.max( minRangeAmountMultiple * amountLimits[0] * amountSafeThresholds[0], maxAmount, ); if (minAmount > maxAmount / minRangeAmountMultiple) { if (activeThumb === 0) { maxAmount = minRangeAmountMultiple * minAmount; } else { minAmount = maxAmount / minRangeAmountMultiple; } } else if (minAmount < maxAmount / maxRangeAmountMultiple) { if (activeThumb === 0) { maxAmount = maxRangeAmountMultiple * minAmount; } else { minAmount = maxAmount / maxRangeAmountMultiple; } } setMaker({ ...maker, minAmount: parseFloat(Number(minAmount).toPrecision(minAmount < 100 ? 2 : 3)), maxAmount: parseFloat(Number(maxAmount).toPrecision(maxAmount < 100 ? 2 : 3)), }); }; const disableSubmit = function () { return ( fav.type == null || (maker.amount != '' && !maker.advancedOptions && (maker.amount < amountLimits[0] || maker.amount > amountLimits[1])) || (maker.amount == null && (!maker.advancedOptions || limits.loading)) || (maker.advancedOptions && (minAmountError() || maxAmountError())) || (maker.amount <= 0 && !maker.advancedOptions) || (maker.isExplicit && (maker.badSatoshisText != '' || maker.satoshis == '')) || (!maker.isExplicit && maker.badPremiumText != '') ); }; const clearMaker = function () { setFav({ ...fav, type: null }); setMaker(defaultMaker); }; const SummaryText = function () { return ( {fav.type == null ? t('Order for ') : fav.type == 1 ? t('Buy order for ') : t('Sell order for ')} {maker.advancedOptions && maker.minAmount != '' ? pn(maker.minAmount) + '-' + pn(maker.maxAmount) : pn(maker.amount)} {' ' + currencyCode} {maker.isExplicit ? t(' of {{satoshis}} Satoshis', { satoshis: pn(maker.satoshis) }) : maker.premium == 0 ? t(' at market price') : maker.premium > 0 ? t(' at a {{premium}}% premium', { premium: maker.premium }) : t(' at a {{discount}}% discount', { discount: -maker.premium })} ); }; return ( setOpenDialogs(false)} setPage={setPage} onClickDone={handleCreateOrder} hasRobot={hasRobot} />
{t('Buy or Sell Bitcoin?')}
amountLimits[1]) } helperText={ maker.amount < amountLimits[0] && maker.amount != '' ? t('Must be more than {{minAmount}}', { minAmount: pn(parseFloat(amountLimits[0].toPrecision(2))), }) : maker.amount > amountLimits[1] && maker.amount != '' ? t('Must be less than {{maxAmount}}', { maxAmount: pn(parseFloat(amountLimits[1].toPrecision(2))), }) : null } label={t('Amount')} required={true} value={maker.amount} type='number' inputProps={{ min: 0, style: { textAlign: 'center', backgroundColor: theme.palette.background.paper, borderRadius: '4px', }, }} onChange={(e) => setMaker({ ...maker, amount: e.target.value })} /> {!maker.advancedOptions && pricingMethods ? ( {t('Choose a Pricing Method')} } label={t('Relative')} labelPlacement='end' onClick={handleClickRelative} /> } label={t('Exact')} labelPlacement='end' onClick={handleClickExplicit} /> ) : null}
), style: { backgroundColor: theme.palette.background.paper, borderRadius: '4px', }, }} renderInput={(props) => } label={t('Public Duration (HH:mm)')} value={maker.publicExpiryTime} onChange={handleChangePublicDuration} minTime={new Date(0, 0, 0, 0, 10)} maxTime={new Date(0, 0, 0, 23, 59)} /> ), style: { backgroundColor: theme.palette.background.paper, borderRadius: '4px', }, }} renderInput={(props) => } label={t('Escrow/Invoice Timer (HH:mm)')} value={maker.escrowExpiryTime} onChange={handleChangeEscrowDuration} minTime={new Date(0, 0, 0, 1, 0)} maxTime={new Date(0, 0, 0, 8, 0)} /> {t('Fidelity Bond Size')}{' '} x + '%'} step={0.25} marks={[ { value: 2, label: '2%' }, { value: 5, label: '5%' }, { value: 10, label: '10%' }, { value: 15, label: '15%' }, ]} min={2} max={15} onChange={(e) => setMaker({ ...maker, bondSize: e.target.value })} />
{/* conditions to disable the make button */} {disableSubmit() ? (
) : ( { disableRequest ? onSubmit() : setOpenDialogs(true); }} > {t(submitButtonLabel)} )}
{collapseAll ? ( ) : null}
{badRequest} {(maker.isExplicit ? t('Order rate:') : t('Order current rate:')) + ` ${pn(currentPrice)} ${currencyCode}/BTC`}
); }; export default MakerForm;