// @flow import React from 'react'; import Button from 'component/button'; import { FormField, Form } from 'component/common/form'; import { Lbryio } from 'lbryinc'; import Card from 'component/common/card'; import LbcSymbol from 'component/common/lbc-symbol'; import Spinner from 'component/spinner'; import Nag from 'component/common/nag'; import CopyableText from 'component/copyableText'; import QRCode from 'component/common/qr-code'; import usePersistedState from 'effects/use-persisted-state'; import * as ICONS from 'constants/icons'; import * as MODALS from 'constants/modal_types'; import { clipboard } from 'electron'; import I18nMessage from 'component/i18nMessage'; const BTC_SATOSHIS = 100000000; const BTC_MAX = 21000000; const BTC_MIN = 1 / BTC_SATOSHIS; const STATUS_FETCH_INTERVAL_MS = 60000; const IS_DEV = process.env.NODE_ENV !== 'production'; const DEBOUNCE_BTC_CHANGE_MS = 400; const INTERNAL_APIS_DOWN = 'internal_apis_down'; const BTC_API_STATUS_PENDING = 'Pending'; const BTC_API_STATUS_PROCESSING = 'Processing'; const BTC_API_STATUS_SUCCESS = 'Success'; const BTC_API_STATUS_ERROR = 'Error'; const ACTION_MAIN = 'action_main'; const ACTION_STATUS_PENDING = 'action_pending'; const ACTION_STATUS_PROCESSING = 'action_processing'; const ACTION_STATUS_SUCCESS = 'action_success'; const ACTION_PAST_SWAPS = 'action_past_swaps'; const NAG_API_STATUS_PENDING = 'Waiting to receive your bitcoin.'; const NAG_API_STATUS_PROCESSING = 'Bitcoin received. Sending your LBC.'; const NAG_API_STATUS_SUCCESS = 'LBC sent. You should see it in your wallet.'; const NAG_API_STATUS_ERROR = 'An error occurred on the previous swap.'; const NAG_SWAP_CALL_FAILED = 'Failed to initiate swap.'; // const NAG_STATUS_CALL_FAILED = 'Failed to query swap status.'; const NAG_SERVER_DOWN = 'The system is currently down. Come back later.'; const NAG_RATE_CALL_FAILED = 'Unable to obtain exchange rate. Try again later.'; type Props = { receiveAddress: string, coinSwaps: Array, doToast: ({ message: string }) => void, addCoinSwap: (CoinSwapInfo) => void, getNewAddress: () => void, checkAddressIsMine: (string) => void, openModal: (string, {}) => void, }; function WalletSwap(props: Props) { const { receiveAddress, doToast, coinSwaps, addCoinSwap, getNewAddress, checkAddressIsMine, openModal } = props; const [btc, setBtc] = usePersistedState('swap-btc-amount', 0.001); const [btcError, setBtcError] = React.useState(); const [btcAddress, setBtcAddress] = React.useState(); const [lbc, setLbc] = React.useState(0); const [action, setAction] = React.useState(ACTION_MAIN); const [nag, setNag] = React.useState(null); const [showQr, setShowQr] = React.useState(false); const [isFetchingRate, setIsFetchingRate] = React.useState(false); const [isSwapping, setIsSwapping] = React.useState(false); const [statusMap, setStatusMap] = React.useState({}); const [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false); const status = btcAddress ? statusMap[btcAddress] : null; const btcTxId = status && status.receipt_txid ? status.receipt_txid : null; const lbcTxId = status && status.lbc_txid ? status.lbc_txid : null; function formatLbcString(lbc) { return lbc === 0 ? '---' : lbc.toLocaleString(undefined, { minimumFractionDigits: 8 }); } function returnToMainAction() { setIsSwapping(false); setAction(ACTION_MAIN); setBtcAddress(null); } function removeCoinSwap(sendAddress) { openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, { sendAddress: sendAddress, }); } // Ensure 'receiveAddress' is populated React.useEffect(() => { if (!receiveAddress) { getNewAddress(); } else { checkAddressIsMine(receiveAddress); } }, [receiveAddress, getNewAddress, checkAddressIsMine]); // Get 'btc::rate' React.useEffect(() => { if (isNaN(btc) || btc === 0) { setLbc(0); return; } setIsFetchingRate(true); const timer = setTimeout(() => { Lbryio.call('btc', 'rate', { satoshi: BTC_SATOSHIS }) .then((result) => { setIsFetchingRate(false); setLbc(btc / result); }) .catch((e) => { setIsFetchingRate(false); setLbc(0); setNag({ msg: NAG_RATE_CALL_FAILED, type: 'error' }); }); }, DEBOUNCE_BTC_CHANGE_MS); return () => clearTimeout(timer); }, [btc]); function queryStatus(btcAddress, successCb, failureCb) { Lbryio.call('btc', 'status', { pay_to_address: btcAddress }) .then((result) => { setStatusMap((statusMap) => { const tmpMap = { ...statusMap }; if (btcAddress) { tmpMap[btcAddress] = result; } return tmpMap; }); if (successCb) successCb(result); }) .catch((err) => { if (failureCb) failureCb(err); }); } // Poll 'btc::status' React.useEffect(() => { function fetchBtcStatus() { queryStatus( btcAddress, (result) => { switch (result.Status) { case BTC_API_STATUS_PENDING: setAction(ACTION_STATUS_PENDING); setNag({ msg: NAG_API_STATUS_PENDING, type: 'helpful' }); break; case BTC_API_STATUS_PROCESSING: setAction(ACTION_STATUS_PROCESSING); setNag({ msg: NAG_API_STATUS_PROCESSING, type: 'helpful' }); break; case BTC_API_STATUS_SUCCESS: setAction(ACTION_STATUS_SUCCESS); setNag({ msg: NAG_API_STATUS_SUCCESS, type: 'helpful' }); setIsSwapping(false); break; case BTC_API_STATUS_ERROR: setNag({ msg: NAG_API_STATUS_ERROR, type: 'error' }); returnToMainAction(); break; default: if (IS_DEV) throw new Error('Unhandled status: "' + result.Status + '"'); break; } }, (err) => { returnToMainAction(); setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SERVER_DOWN : err.message /* NAG_STATUS_CALL_FAILED */, type: 'error', }); } ); } let fetchInterval; if (btcAddress && isSwapping) { fetchBtcStatus(); fetchInterval = setInterval(fetchBtcStatus, STATUS_FETCH_INTERVAL_MS); } return () => { if (fetchInterval) { clearInterval(fetchInterval); } }; }, [btcAddress, isSwapping]); // Validate entered BTC React.useEffect(() => { let msg; if (btc < BTC_MIN) { msg = __('The BTC amount needs to be higher'); } else if (btc > BTC_MAX) { msg = __('The BTC amount is too high'); } setBtcError(msg); }, [btc]); // 'Refresh' button feedback React.useEffect(() => { let timer; if (isRefreshingStatus) { timer = setTimeout(() => { setIsRefreshingStatus(false); }, 1000); } return () => clearTimeout(timer); }, [isRefreshingStatus]); function handleStartSwap() { setIsSwapping(true); setBtcAddress(null); setNag(null); Lbryio.call('btc', 'swap', { lbc_satoshi_requested: parseInt(lbc * BTC_SATOSHIS), btc_satoshi_provided: parseInt(btc * BTC_SATOSHIS), pay_to_wallet_address: receiveAddress, }) .then((result) => { setBtcAddress(result); addCoinSwap({ coin: 'btc', sendAddress: result, sendAmount: btc, lbcAmount: lbc, }); }) .catch((err) => { setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : err.message, type: 'error' }); returnToMainAction(); }); } function handleCancelPending() { returnToMainAction(); setNag(null); } function handleBtcChange(event: SyntheticInputEvent<*>) { const btc = parseFloat(event.target.value); setBtc(btc); } function handleViewPastSwaps() { setAction(ACTION_PAST_SWAPS); setNag(null); setIsRefreshingStatus(true); coinSwaps.forEach((x) => { queryStatus(x.sendAddress, null, null); }); } function getStatusStr(btcAddress) { const status = statusMap[btcAddress]; if (!status) { return '---'; } let msg; switch (status.Status) { case BTC_API_STATUS_PENDING: msg = __('Waiting for BTC'); break; case BTC_API_STATUS_PROCESSING: msg = __('Sending LBC'); break; case BTC_API_STATUS_SUCCESS: msg = __('Completed'); break; case BTC_API_STATUS_ERROR: msg = __('Failed'); break; default: msg = '?'; // if (IS_DEV) throw new Error('Unhandled "status": ' + status.Status); break; } return msg; } function getViewTransactionElement(isSend) { if (isSend) { return btcTxId ? (