From 161bc2bda975d724766ca43393714c01f9f983ec Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Mon, 28 Mar 2022 21:28:33 +0000 Subject: [PATCH 1/9] Fix ElasticSearch reliability --- app/redux/FetchDataSaga.js | 38 +++++++++++++++++++++++--------------- app/utils/SearchClient.js | 25 +++++++++++++++++++++---- server/api/uia_address.js | 12 ++---------- shared/fetchWithTimeout.js | 10 ++++++++++ 4 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 shared/fetchWithTimeout.js diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index d0aa0f0da..89fa8aab3 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -612,23 +612,31 @@ export function* fetchData(action) { } else if (args[0].select_tags) { req = req.byOneOfTags(args[0].select_tags) } - let { results, total }= yield searchData(req) - let sepAdded = !!from - results.forEach(post => { - post.from_search = true - if (!post.net_rshares && !post.net_votes && !post.children) { - post.force_hide = true - } - if (!sepAdded) { - post.total_search = total - sepAdded = true + const firstPage = !author && !permlink + let searchRes = null + try { + searchRes = yield searchData(req, firstPage ? 0 : 3) + } catch (searchErr) { + } + if (searchRes) { + let { results, total } = searchRes + let sepAdded = !!from + results.forEach(post => { + post.from_search = true + if (!post.net_rshares && !post.net_votes && !post.children) { + post.force_hide = true + } + if (!sepAdded) { + post.total_search = total + sepAdded = true + } + data.push(post) + }) + if (firstPage) { + has_from_search = true } - data.push(post) - }) - if (!author && !permlink) { - has_from_search = true + next_from = (from || 0) + results.length } - next_from = (from || 0) + results.length } data.forEach(post => { diff --git a/app/utils/SearchClient.js b/app/utils/SearchClient.js index f1ccedba1..736c632eb 100644 --- a/app/utils/SearchClient.js +++ b/app/utils/SearchClient.js @@ -1,5 +1,7 @@ +import { Headers } from 'cross-fetch' import { detransliterate } from 'app/utils/ParsersAndFormatters' +import fetchWithTimeout from 'shared/fetchWithTimeout' const makeTag = (text) => { return /^[а-яё]/.test(text) @@ -149,11 +151,11 @@ export class SearchRequest { } } -export async function sendSearchRequest(sr) { +export async function sendSearchRequest(sr, timeoutMsec = 10000) { let body = sr.build() let url = new URL($STM_Config.elastic_search.url); url += 'blog/post/_search?pretty' - const response = await fetch(url, { + const response = await fetchWithTimeout(url, timeoutMsec, { method: 'post', headers: new Headers({ 'Authorization': 'Basic ' + btoa($STM_Config.elastic_search.login + ':' + $STM_Config.elastic_search.password), @@ -174,8 +176,23 @@ const copyField = (obj, hit, fieldName, fallbackValue) => { obj[fieldName] = val !== undefined ? val : fallbackValue } -export async function searchData(sr) { - let preResults = await sendSearchRequest(sr) +export async function searchData(sr, retries = 3, retryIntervalSec = 2, timeoutMsec = 10000) { + const retryMsec = retryIntervalSec * 1000 + let preResults = null + for (let i = 0; i < (retries + 1); ++i) { + try { + preResults = await sendSearchRequest(sr, timeoutMsec) + break + } catch (err) { + if (i + 1 < retries + 1) { + console.error('ElasticSearch failure, retrying after', retryIntervalSec, 'sec...', err) + await new Promise(resolve => setTimeout(resolve, retryMsec)) + } else { + console.error('ElasticSearch failure', err) + throw err + } + } + } let results = preResults.hits.hits.map((hit) => { let obj = {} diff --git a/server/api/uia_address.js b/server/api/uia_address.js index d1c583452..e455f3101 100644 --- a/server/api/uia_address.js +++ b/server/api/uia_address.js @@ -1,15 +1,7 @@ -import fetch from 'cross-fetch' import { api } from 'golos-lib-js' -import { rateLimitReq } from 'server/utils/misc' -async function fetchWithTimeout(url, timeoutMsec, opts) { - const controller = new AbortController() - setTimeout(() => controller.abort(), timeoutMsec) - return await fetch(url, { - signal: controller.signal, - ...opts - }) -} +import { rateLimitReq } from 'server/utils/misc' +import fetchWithTimeout from 'shared/fetchWithTimeout' export default function useGetAddressHandler(app) { app.get('/uia_address/:symbol/:account', function *() { diff --git a/shared/fetchWithTimeout.js b/shared/fetchWithTimeout.js new file mode 100644 index 000000000..7369f1554 --- /dev/null +++ b/shared/fetchWithTimeout.js @@ -0,0 +1,10 @@ +import fetch from 'cross-fetch' + +export default async function fetchWithTimeout(url, timeoutMsec, opts) { + const controller = new AbortController() + setTimeout(() => controller.abort(), timeoutMsec) + return await fetch(url, { + signal: controller.signal, + ...opts + }) +} From 06c5c1c5b0fc04b286c57dead6b1231f1d35d64c Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Mon, 28 Mar 2022 22:12:19 +0000 Subject: [PATCH 2/9] Desktop - fix goto external URLs --- app/components/pages/app/AppGotoURL.jsx | 2 +- electron/menu.js | 8 ++++++-- electron/settings_preload.js | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/components/pages/app/AppGotoURL.jsx b/app/components/pages/app/AppGotoURL.jsx index e08ff88a5..7b3687601 100644 --- a/app/components/pages/app/AppGotoURL.jsx +++ b/app/components/pages/app/AppGotoURL.jsx @@ -40,7 +40,7 @@ class AppGotoURL extends React.Component { console.error(err) } if (confirm(tt('app_goto_url.wrong_domain_DOMAINS', { DOMAINS: $STM_Config.url_domains }))) { - window.appNavigation.loadURL(url.toString()) + window.appNavigation.loadURL(url.toString(), true) this.close() } } diff --git a/electron/menu.js b/electron/menu.js index 10e40ea3f..149860cdb 100644 --- a/electron/menu.js +++ b/electron/menu.js @@ -66,8 +66,12 @@ function initMenu(appUrl, httpsUrl, appSet, full = true) { } }) ipcMain.removeAllListeners('load-url') - ipcMain.once('load-url', async (e, url) => { - win.webContents.send('router-push', url) + ipcMain.once('load-url', async (e, url, isExternal) => { + if (isExternal) { + shell.openExternal(url) + } else { + win.webContents.send('router-push', url) + } }) createContextMenu(gotoURL) gotoURL.loadURL(appUrl + '/__app_goto_url?' + url) diff --git a/electron/settings_preload.js b/electron/settings_preload.js index d7fb68e70..8f4f782d7 100644 --- a/electron/settings_preload.js +++ b/electron/settings_preload.js @@ -16,8 +16,8 @@ contextBridge.exposeInMainWorld('appSplash', { }) contextBridge.exposeInMainWorld('appNavigation', { - loadURL: (url) => { - ipcRenderer.send('load-url', url) + loadURL: (url, isExternal = false) => { + ipcRenderer.send('load-url', url, isExternal) }, onRouter: (cb) => { ipcRenderer.removeAllListeners('router-push') From ab05f767b37c86e124af73acaf28ed8aff739344 Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Tue, 29 Mar 2022 03:56:50 +0000 Subject: [PATCH 3/9] Quick convert modal, refactor market --- app/ResolveRoute.js | 3 + app/RootRoute.js | 2 + app/assets/images/gold-golos.png | Bin 6145 -> 7955 bytes app/assets/images/golos.png | Bin 7616 -> 10067 bytes app/components/all.scss | 5 +- app/components/elements/AssetBalance.jsx | 3 +- app/components/elements/ConvertToSteem.jsx | 2 +- app/components/elements/DropdownMenu.scss | 5 + app/components/elements/PagedDropdownMenu.jsx | 64 ++- .../elements/PagedDropdownMenu.scss | 4 + app/components/elements/market/CMCValue.jsx | 70 +++ .../elements/market/ConvertAssetsBtn.jsx | 57 ++ .../elements/market/ConvertAssetsBtn.scss | 19 + .../elements/market/FinishedOrder.jsx | 61 ++ app/components/elements/market/MarketPair.jsx | 153 +++++ .../elements/market/MarketPair.scss | 24 + .../{Orderbook.jsx => market/OrderBook.jsx} | 3 +- .../OrderBookRow.jsx} | 3 +- .../elements/{ => market}/OrderHistory.jsx | 6 +- .../OrderHistoryRow.jsx} | 5 +- .../elements/{ => market}/PriceChart.jsx | 0 .../elements/{ => market}/TickerPriceStat.jsx | 0 .../{ => market}/TickerPriceStat.scss | 0 app/components/modules/ConvertAssets.jsx | 525 ++++++++++++++++++ app/components/modules/ConvertAssets.scss | 14 + app/components/modules/Header.jsx | 2 + app/components/modules/Modals.jsx | 13 + app/components/modules/OpenOrders.jsx | 2 +- app/components/modules/Transfer.jsx | 2 +- app/components/modules/UserWallet.jsx | 16 +- .../modules/uia/AssetEditDeposit.jsx | 2 +- .../modules/uia/AssetEditWithdrawal.jsx | 2 +- app/components/modules/uia/AssetRules.jsx | 2 +- app/components/modules/uia/Assets.jsx | 25 +- app/components/pages/ConvertAssetsLoader.jsx | 35 ++ app/components/pages/Market.jsx | 397 ++++--------- app/components/pages/Market.scss | 10 - app/components/pages/Witnesses.jsx | 3 +- app/locales/en.json | 38 +- app/locales/ru-RU.json | 38 +- app/redux/User.js | 4 + app/utils/ApidexApiClient.js | 62 +++ app/utils/MarketUtils.js | 24 - .../{MarketClasses.js => market/Order.js} | 70 +-- app/utils/market/TradeHistory.js | 65 +++ app/utils/market/utils.js | 124 +++++ config/default.json | 7 + package.json | 2 +- server/index.js | 2 + yarn.lock | 58 +- 50 files changed, 1598 insertions(+), 435 deletions(-) create mode 100644 app/components/elements/market/CMCValue.jsx create mode 100644 app/components/elements/market/ConvertAssetsBtn.jsx create mode 100644 app/components/elements/market/ConvertAssetsBtn.scss create mode 100644 app/components/elements/market/FinishedOrder.jsx create mode 100644 app/components/elements/market/MarketPair.jsx create mode 100644 app/components/elements/market/MarketPair.scss rename app/components/elements/{Orderbook.jsx => market/OrderBook.jsx} (98%) rename app/components/elements/{OrderbookRow.jsx => market/OrderBookRow.jsx} (98%) rename app/components/elements/{ => market}/OrderHistory.jsx (95%) rename app/components/elements/{OrderhistoryRow.jsx => market/OrderHistoryRow.jsx} (95%) rename app/components/elements/{ => market}/PriceChart.jsx (100%) rename app/components/elements/{ => market}/TickerPriceStat.jsx (100%) rename app/components/elements/{ => market}/TickerPriceStat.scss (100%) create mode 100644 app/components/modules/ConvertAssets.jsx create mode 100644 app/components/modules/ConvertAssets.scss create mode 100644 app/components/pages/ConvertAssetsLoader.jsx create mode 100644 app/utils/ApidexApiClient.js delete mode 100644 app/utils/MarketUtils.js rename app/utils/{MarketClasses.js => market/Order.js} (59%) create mode 100644 app/utils/market/TradeHistory.js create mode 100644 app/utils/market/utils.js diff --git a/app/ResolveRoute.js b/app/ResolveRoute.js index a1e2e9808..94a8d39cd 100644 --- a/app/ResolveRoute.js +++ b/app/ResolveRoute.js @@ -114,6 +114,9 @@ export default function resolveRoute(path) if (match) { return {page: 'UserProfile', params: match.slice(1)}; } + if (path === '/convert') { + return {page: 'ConvertAssetsLoader', params: []} + } match = path.match(routeRegex.PostNoCategory); if (match) { return {page: 'PostNoCategory', params: match.slice(1)}; diff --git a/app/RootRoute.js b/app/RootRoute.js index fd8264c0d..24dbcf85a 100644 --- a/app/RootRoute.js +++ b/app/RootRoute.js @@ -67,6 +67,8 @@ export default { else cb(null, [require('@pages/SubmitPostServerRender')]); } else if (route.page === 'UserProfile') { cb(null, [require('@pages/UserProfile')]); + } else if (route.page === 'ConvertAssetsLoader') { + cb(null, [require('@pages/ConvertAssetsLoader')]); } else if (route.page === 'Market') { cb(null, [require('@pages/MarketLoader')]); } else if (route.page === 'Post') { diff --git a/app/assets/images/gold-golos.png b/app/assets/images/gold-golos.png index 3baf659043e8ab4f32c518ae3be36a31927f7889..24096397c81e05ee1a2b7d9cae728d87dbc1bbaf 100644 GIT binary patch literal 7955 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 literal 7616 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 diff --git a/app/components/all.scss b/app/components/all.scss index 68ec0391b..23d521b64 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -31,7 +31,6 @@ @import "./elements/ScrollButton"; @import "./elements/SignupProgressBar"; @import "./elements/TagList"; -@import "./elements/TickerPriceStat"; @import "./elements/UserNames"; @import "./elements/Userpic"; @import "./elements/VerticalMenu"; @@ -46,6 +45,9 @@ @import "./elements/common/HintIcon/HintIcon"; @import "./elements/common/DialogManager/index"; @import "./elements/common/TooltipManager/index"; +@import "./elements/market/ConvertAssetsBtn"; +@import "./elements/market/MarketPair"; +@import "./elements/market/TickerPriceStat"; @import "./elements/postEditor/MarkdownEditor/MarkdownEditor"; @import "./elements/postEditor/MarkdownEditorToolbar/index"; @import "./elements/postEditor/EditorSwitcher/EditorSwitcher"; @@ -63,6 +65,7 @@ // modules @import "./modules/uia/AssetEditWithdrawal"; @import "./modules/BottomPanel"; +@import "./modules/ConvertAssets"; @import "./modules/Footer"; @import "./modules/Header"; @import "./modules/LoginForm"; diff --git a/app/components/elements/AssetBalance.jsx b/app/components/elements/AssetBalance.jsx index cb61f6b0e..8f27eb8dc 100644 --- a/app/components/elements/AssetBalance.jsx +++ b/app/components/elements/AssetBalance.jsx @@ -1,4 +1,5 @@ -import tt from 'counterpart'; +import React from 'react' +import tt from 'counterpart' const AssetBalance = ({onClick, balanceValue, title}) => { let balance = (title || tt('transfer_jsx.balance')) + ': ' + balanceValue; diff --git a/app/components/elements/ConvertToSteem.jsx b/app/components/elements/ConvertToSteem.jsx index 465d40428..f00f04050 100644 --- a/app/components/elements/ConvertToSteem.jsx +++ b/app/components/elements/ConvertToSteem.jsx @@ -210,7 +210,7 @@ export default connect( const balance = account.get('balance'); const sbd_balance = account.get('sbd_balance'); const cprops = state.global.get('cprops'); - const max = Asset(from === DEBT_TICKER ? sbd_balance : balance).amountFloat; + const max = parseFloat(Asset(from === DEBT_TICKER ? sbd_balance : balance).amountFloat); return { ...ownProps, owner: username, diff --git a/app/components/elements/DropdownMenu.scss b/app/components/elements/DropdownMenu.scss index 844abe280..869dc4379 100644 --- a/app/components/elements/DropdownMenu.scss +++ b/app/components/elements/DropdownMenu.scss @@ -47,4 +47,9 @@ bottom: 100%; top: auto; } + + &.top-most > .VerticalMenu { + position: fixed; + top: auto; + } } diff --git a/app/components/elements/PagedDropdownMenu.jsx b/app/components/elements/PagedDropdownMenu.jsx index 647b53c85..abf195a4a 100644 --- a/app/components/elements/PagedDropdownMenu.jsx +++ b/app/components/elements/PagedDropdownMenu.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import cloneDeep from 'lodash/cloneDeep'; +import isEqual from 'lodash/isEqual' import tt from 'counterpart'; import DropdownMenu from 'app/components/elements/DropdownMenu'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; @@ -18,28 +19,51 @@ export default class PagedDropdownMenu extends React.Component { el: PropTypes.string.isRequired, noArrow: PropTypes.bool, + page: PropTypes.number, perPage: PropTypes.number.isRequired, - onLoadMore: PropTypes.func.isRequired, + renderItem: PropTypes.func.isRequired, + onLoadMore: PropTypes.func, }; + static defaultProps = { + page: 1 + } + constructor(props) { super(props); this.state = { items: [], - page: 0, + page: props.page, loading: false, }; } componentDidMount() { - this.initItems(this.props.items); + const { items, page, } = this.props + this.initItems(this.sliceItems(items, page)) + this.setState({ page }) } componentDidUpdate(prevProps) { - const { items, } = this.props; - if (items && (!prevProps.items || items.length !== prevProps.items.length)) { - this.initItems(items); + const { items, page, } = this.props + if (items && (!prevProps.items || !isEqual(items, prevProps.items))) { + const sliced = this.sliceItems(items, 1) + this.initItems(sliced) + this.setState({ page: 1 }) + } else if (page && prevProps.page !== page) { + this.setState({ page }) + } + } + + sliceItems = (items, page) => { + const { onLoadMore, perPage } = this.props + if (onLoadMore) { + return items } + const startIdx = perPage * (page - 1) + const endIdx = startIdx + perPage + 1 + const sliced = items.slice(startIdx, endIdx) + return sliced } initItems = (items) => { @@ -53,6 +77,16 @@ export default class PagedDropdownMenu extends React.Component { loadMore = async (newPage) => { const { items, page, } = this.state; const { onLoadMore, } = this.props; + if (!onLoadMore) { + setTimeout(async () => { + this.setState({ + page: newPage + }, () => { + this.initItems(this.sliceItems(this.props.items, newPage)) + }) + }, 10); + return + } setTimeout(async () => { this.setState({ page: newPage, @@ -74,7 +108,7 @@ export default class PagedDropdownMenu extends React.Component { }; prevPage = () => { - if (this.state.page === 0) return; + if (this.state.page === 1) return; const { page, } = this.state; this.loadMore(page - 1); }; @@ -83,26 +117,30 @@ export default class PagedDropdownMenu extends React.Component { const { perPage, } = this.props; const { items, page, } = this.state; const hasMore = items.length > perPage; - if (page === 0 && !hasMore) { + if (page === 1 && !hasMore) { return null; } + const hasPrev = page > 1 return { value: - - {page > 0 ? '< ' + tt('g.back') : ''} - + + {hasPrev ? '< ' + tt('g.back') : ''} + {hasMore ? tt('g.more_list') + ' >' : ''} , }; }; render() { - const { el, selected, children, className, title, href, noArrow, perPage, } = this.props; + const { el, selected, children, className, title, href, noArrow, perPage, renderItem, } = this.props const { items, loading, } = this.state; let itemsWithPaginator = []; if (!loading) { - itemsWithPaginator = [...items]; + for (let i = 0; i < items.length; ++i) { + const rendered = renderItem(items[i]) + itemsWithPaginator.push(rendered) + } if (items.length > perPage && hideLastItem) { itemsWithPaginator.pop(); } diff --git a/app/components/elements/PagedDropdownMenu.scss b/app/components/elements/PagedDropdownMenu.scss index 01a6b6640..a6c88ee8d 100644 --- a/app/components/elements/PagedDropdownMenu.scss +++ b/app/components/elements/PagedDropdownMenu.scss @@ -9,4 +9,8 @@ padding-top: 0.5rem; padding-bottom: 0.5rem; width: 50%; + + &.disabled { + cursor: auto; + } } diff --git a/app/components/elements/market/CMCValue.jsx b/app/components/elements/market/CMCValue.jsx new file mode 100644 index 000000000..6063af14c --- /dev/null +++ b/app/components/elements/market/CMCValue.jsx @@ -0,0 +1,70 @@ +import React from 'react' +import tt from 'counterpart' + +import { apidexGetPrices } from 'app/utils/ApidexApiClient' + +class CMCValue extends React.Component { + state = {} + + componentDidMount() { + this.updateCMCPrice(this.props.buyAmount) + } + + componentDidUpdate(prevProps) { + const { buyAmount } = this.props + if (buyAmount && (!prevProps.buyAmount || + buyAmount.ne(prevProps.buyAmount) || + buyAmount.symbol !== prevProps.buyAmount.symbol)) { + this.updateCMCPrice(buyAmount) + } + } + + updateCMCPrice = async (buyAmount) => { + if (!buyAmount) { + return + } + const { price_usd, price_rub, page_url } = await apidexGetPrices(buyAmount.symbol) + const calc = (price) => { + if (price === null || price === undefined) return null + return parseFloat(buyAmount.amountFloat) * price + } + const cmcPrice = { + price_usd: calc(price_usd), + price_rub: calc(price_rub), + page_url + } + this.setState({ + cmcPrice + }) + } + + render() { + const { renderer } = this.props + let cmc = null + const { cmcPrice } = this.state + if (cmcPrice) { + const formatVal = (val, fmt) => { + return (val && val.toFixed) ? fmt(val.toFixed(2)) : null + } + let mainVal, altVal + const price_usd = formatVal(cmcPrice.price_usd, v => `$${v}`) + const price_rub = formatVal(cmcPrice.price_rub, v => `${v} RUB`) + if (tt.getLocale() === 'ru') { + mainVal = price_rub || price_usd + altVal = price_usd || price_rub + } else { + mainVal = price_usd || price_rub + altVal = price_rub || price_usd + } + if (mainVal) { + cmc = {'~' + mainVal} + if (cmcPrice.page_url) { + cmc = {cmc} + } + } + } + return (cmc && renderer) ? renderer(cmc) : cmc + } +} + +export default CMCValue diff --git a/app/components/elements/market/ConvertAssetsBtn.jsx b/app/components/elements/market/ConvertAssetsBtn.jsx new file mode 100644 index 000000000..01025e4a3 --- /dev/null +++ b/app/components/elements/market/ConvertAssetsBtn.jsx @@ -0,0 +1,57 @@ +import React from 'react' +import PropTypes from 'prop-types' +import tt from 'counterpart' +import { connect } from 'react-redux' + +import Icon from 'app/components/elements/Icon' +import user from 'app/redux/User' + +class ConvertAssetsBtn extends React.Component { + static propTypes = { + sym: PropTypes.string.isRequired, + disabled: PropTypes.bool, + direction: PropTypes.string, + } + + static defaultProps = { + direction: 'sell', + } + + onClick = (e) => { + e.preventDefault() + const { direction, disabled, showModal, sym } = this.props + if (!disabled) { + let opts = { direction } + if (direction === 'sell') { + opts.sellSym = sym + } else { + opts.buySym = sym + } + showModal(opts) + } + } + + render() { + const { disabled } = this.props + const title = disabled ? tt('convert_assets_jsx.no_tradables') : tt('g.convert_assets') + return ( + + ) + } +} + +export default connect( + (state, ownProps) => { + return { + ...ownProps, + } + }, + + (dispatch) => ({ + showModal: (opts) => { + dispatch(user.actions.setConvertAssetsDefaults(opts)) + dispatch(user.actions.showConvertAssets()) + }, + }) +)(ConvertAssetsBtn) diff --git a/app/components/elements/market/ConvertAssetsBtn.scss b/app/components/elements/market/ConvertAssetsBtn.scss new file mode 100644 index 000000000..e2cd84bda --- /dev/null +++ b/app/components/elements/market/ConvertAssetsBtn.scss @@ -0,0 +1,19 @@ +.ConvertAssetsBtn { + cursor: pointer; + + .Icon { + vertical-align: top; + padding-top: 2px; + margin-right: 2px; + } + + svg { + transform: rotate(90deg); + } + + &:not(.disabled) { + path { + fill: #0078C4; + } + } +} diff --git a/app/components/elements/market/FinishedOrder.jsx b/app/components/elements/market/FinishedOrder.jsx new file mode 100644 index 000000000..65bf20761 --- /dev/null +++ b/app/components/elements/market/FinishedOrder.jsx @@ -0,0 +1,61 @@ +import React from 'react' +import tt from 'counterpart' +import { connect, } from 'react-redux' + +import user from 'app/redux/User' + +class FinishedOrder extends React.Component { + render() { + const { finished, finishedAcc, buyAmount, sellAmount, remainToReceive } = this.props + const path = buyAmount.isUIA ? 'assets' : 'transfers' + const link = + {tt('convert_assets_jsx.finished_balance')} + + const goCancel = (e) => { + e.preventDefault() + this.props.showOpenOrders({ sym: sellAmount.symbol }) + } + const cancelLink = {tt('convert_assets_jsx.partly_link')} + + if (finished === 'full') { + return (
+

{tt('convert_assets_jsx.finished')}

+
+ {tt('convert_assets_jsx.finished_desc')} + {link}. +
+
) + } else if (finished === 'not') { + return (
+

{tt('convert_assets_jsx.not')}

+
+ {tt('convert_assets_jsx.not_desc')} + {cancelLink}. +
+
) + } else { + return (
+

{tt('convert_assets_jsx.partly')}

+
+ {tt('convert_assets_jsx.partly_desc')} + {remainToReceive.floatString} + {tt('convert_assets_jsx.partly_desc2')} + {cancelLink}. + {tt('convert_assets_jsx.partly_desc3')} + {link}. +
+
) + } + } +} + +export default connect( + null, + dispatch => ({ + showOpenOrders: defaults => { + dispatch(user.actions.setOpenOrdersDefaults(defaults)) + dispatch(user.actions.showOpenOrders()) + dispatch(user.actions.hideConvertAssets()) + }, + }) +)(FinishedOrder) diff --git a/app/components/elements/market/MarketPair.jsx b/app/components/elements/market/MarketPair.jsx new file mode 100644 index 000000000..1b718b916 --- /dev/null +++ b/app/components/elements/market/MarketPair.jsx @@ -0,0 +1,153 @@ +import React from 'react' +import PropTypes from 'prop-types' +import tt from 'counterpart' + +import PagedDropdownMenu from 'app/components/elements/PagedDropdownMenu' +import Icon from 'app/components/elements/Icon' +import { getAssetMeta, getTradablesFor } from 'app/utils/market/utils' + +class MarketPair extends React.Component { + static propTypes = { + assets: PropTypes.object.isRequired, + sym1: PropTypes.string, + sym2: PropTypes.string, + compactList: PropTypes.bool, + slim: PropTypes.bool, + itemsPerPage: PropTypes.number, + linkComposer: PropTypes.func, + onChange: PropTypes.func, + } + + static defaultProps = { + itemsPerPage: 10, + linkComposer: () => '#', + onChange: () => {} + } + + state = { + } + + initLists = (sym1, sym2) => { + if (sym1 && sym2) { + const lists = getTradablesFor(this.props.assets, [sym1, sym2]) + + const hiddenAssets = $STM_Config.hidden_assets ? Object.keys($STM_Config.hidden_assets) : [] + const filter = item => !hiddenAssets.includes(item.symbol) + + this.setState({ + symbols1: lists[1].filter(filter), + symbols2: lists[0].filter(filter), + sym1, + sym2, + }) + this.onChange(null, sym1, sym2) + } else if (sym1 || sym2) { + const setSym = sym1 || sym2 + const oppoList = getTradablesFor(this.props.assets, [setSym], true) + const oppoSym = oppoList[0][0].symbol + if (sym1) { + this.initLists(sym1, oppoSym) + } else { + this.initLists(oppoSym, sym2) + } + } else { + this.initLists('GOLOS', 'GBG') + } + } + + componentDidMount() { + const { sym1, sym2 } = this.props + this.initLists(sym1, sym2) + } + + onChange = (e, sym1, sym2) => { + const { onChange, linkComposer } = this.props + if (e) this.initLists(sym1, sym2) + const link = linkComposer(sym1, sym2) + const assets = this.props.assets + const asset1 = assets[sym1] + const asset2 = assets[sym2] + onChange({ event: e, sym1, sym2, asset1, asset2, link }) + } + + onReversePair = (e) => { + this.setState({ + symbols1: this.state.symbols2, + symbols2: this.state.symbols1, + }) + this.onChange(e, this.state.sym2, this.state.sym1) + } + + render() { + const { assets, slim, compactList, itemsPerPage, linkComposer, label1, label2 } = this.props + const { sym1, sym2, symbols1, symbols2 } = this.state + + if (!symbols1 && !symbols2) return
+ + const renderSym1 = (asset) => { + const { symbol, image_url } = asset + return { + key: symbol, value: symbol, + label: (   {symbol}), + link: linkComposer(symbol, sym2), + onClick: (e) => { + this.onChange(e, symbol, sym2) + } + } + } + + const renderSym2 = (asset) => { + const { symbol, image_url } = asset + return { + key: symbol, value: symbol, + label: (   {symbol}), + link: linkComposer(sym1, symbol), + onClick: (e) => { + this.onChange(e, sym1, symbol) + } + } + } + + let left =
+
{label1}
+ + + + {sym1} + {symbols1.length > 0 && } + + +
+ + let right =
+
{label2}
+ + + + {sym2} + {symbols2.length > 0 && } + + +
+ + return (
+ {left} +   + + + +   + {right} +
) + } + +} + +export default MarketPair diff --git a/app/components/elements/market/MarketPair.scss b/app/components/elements/market/MarketPair.scss new file mode 100644 index 000000000..22d19b394 --- /dev/null +++ b/app/components/elements/market/MarketPair.scss @@ -0,0 +1,24 @@ +.MarketPair { + display: inline-block; +} + +.MarketPair.compact .VerticalMenu>li>a { + padding: .3rem 1rem; +} + +.MarketPair__label { + margin-bottom: 4px; +} + +.MarketPair__selected { + margin-bottom: 4px; + width: 22px; + height: 22px; + margin-right: 5px; +} + +.MarketPair__pagination { + display: inline-block !important; + width: 100px; + font-size: 16px; +} diff --git a/app/components/elements/Orderbook.jsx b/app/components/elements/market/OrderBook.jsx similarity index 98% rename from app/components/elements/Orderbook.jsx rename to app/components/elements/market/OrderBook.jsx index c25b62540..1a5bf28e2 100644 --- a/app/components/elements/Orderbook.jsx +++ b/app/components/elements/market/OrderBook.jsx @@ -1,6 +1,7 @@ import React from "react"; -import OrderBookRow from "./OrderbookRow"; import tt from 'counterpart'; + +import OrderBookRow from 'app/components/elements/market/OrderBookRow' import { DEBT_TOKEN_SHORT } from 'app/client_config'; export default class Orderbook extends React.Component { diff --git a/app/components/elements/OrderbookRow.jsx b/app/components/elements/market/OrderBookRow.jsx similarity index 98% rename from app/components/elements/OrderbookRow.jsx rename to app/components/elements/market/OrderBookRow.jsx index 459f8ef1e..3e1a20654 100644 --- a/app/components/elements/OrderbookRow.jsx +++ b/app/components/elements/market/OrderBookRow.jsx @@ -1,7 +1,8 @@ import React from "react"; import PropTypes from 'prop-types' import tt from 'counterpart'; -import Icon from '@elements/Icon'; + +import Icon from 'app/components/elements/Icon' export default class OrderBookRow extends React.Component { diff --git a/app/components/elements/OrderHistory.jsx b/app/components/elements/market/OrderHistory.jsx similarity index 95% rename from app/components/elements/OrderHistory.jsx rename to app/components/elements/market/OrderHistory.jsx index 5db53e1fa..30fb2bd22 100644 --- a/app/components/elements/OrderHistory.jsx +++ b/app/components/elements/market/OrderHistory.jsx @@ -1,7 +1,9 @@ import React from "react"; -import OrderHistoryRow from "./OrderhistoryRow.jsx"; import tt from 'counterpart'; -import { Order, TradeHistory } from 'app/utils/MarketClasses'; + +import Order from 'app/utils/market/Order' +import TradeHistory from 'app/utils/market/TradeHistory' +import OrderHistoryRow from 'app/components/elements/market/OrderHistoryRow' export default class OrderHistory extends React.Component { diff --git a/app/components/elements/OrderhistoryRow.jsx b/app/components/elements/market/OrderHistoryRow.jsx similarity index 95% rename from app/components/elements/OrderhistoryRow.jsx rename to app/components/elements/market/OrderHistoryRow.jsx index a6839e5cb..2e5cfb5fd 100644 --- a/app/components/elements/OrderhistoryRow.jsx +++ b/app/components/elements/market/OrderHistoryRow.jsx @@ -1,8 +1,9 @@ import React from 'react'; + import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; -import Icon from 'app/components/elements/Icon.jsx'; +import Icon from 'app/components/elements/Icon' -export default class OrderhistoryRow extends React.Component { +export default class OrderHistoryRow extends React.Component { constructor(props) { super(); diff --git a/app/components/elements/PriceChart.jsx b/app/components/elements/market/PriceChart.jsx similarity index 100% rename from app/components/elements/PriceChart.jsx rename to app/components/elements/market/PriceChart.jsx diff --git a/app/components/elements/TickerPriceStat.jsx b/app/components/elements/market/TickerPriceStat.jsx similarity index 100% rename from app/components/elements/TickerPriceStat.jsx rename to app/components/elements/market/TickerPriceStat.jsx diff --git a/app/components/elements/TickerPriceStat.scss b/app/components/elements/market/TickerPriceStat.scss similarity index 100% rename from app/components/elements/TickerPriceStat.scss rename to app/components/elements/market/TickerPriceStat.scss diff --git a/app/components/modules/ConvertAssets.jsx b/app/components/modules/ConvertAssets.jsx new file mode 100644 index 000000000..4c01b4e5f --- /dev/null +++ b/app/components/modules/ConvertAssets.jsx @@ -0,0 +1,525 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { api, } from 'golos-lib-js' +import { Asset, AssetEditor, Price } from 'golos-lib-js/lib/utils' +import tt from 'counterpart' +import { connect, } from 'react-redux' +import { Map, } from 'immutable' +import { Link } from 'react-router' + +import AssetBalance from 'app/components/elements/AssetBalance' +import CMCValue from 'app/components/elements/market/CMCValue' +import FinishedOrder from 'app/components/elements/market/FinishedOrder' +import Icon from 'app/components/elements/Icon' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import MarketPair from 'app/components/elements/market/MarketPair' +import { normalizeAssets, DEFAULT_EXPIRE, generateOrderID, + calcFeeForSell, calcFeeForBuy } from 'app/utils/market/utils' +import transaction from 'app/redux/Transaction' + +class ConvertAssets extends React.Component { + constructor(props) { + super(props) + this.state = { + loading: true, + myBalance: Asset(0, 3, 'GOLOS'), + sellAmount: AssetEditor(0, 3, 'GOLOS'), + sellError: '', + buyAmount: AssetEditor(0, 3, 'GBG'), // not includes fee + fee: Asset(0, 3, 'GBG'), + warning: '', + cmcPrice: {} + } + } + + sellSym = () => this.state.sellAmount.asset.symbol + buySym = () => this.state.buyAmount.asset.symbol + + async componentDidMount() { + let assetsRaw + try { + assetsRaw = await api.getAssetsAsync('', [], '', 5000, 'by_marketed') + } catch (err) { + console.error(err) + return + } + let assets = {} + for (const i in assetsRaw) { + const asset = assetsRaw[i] + assets[asset.supply.split(" ")[1]] = asset + } + let assetsNorm = normalizeAssets(assets) + this.setState({ + loading: false, + assets: assetsNorm + }) + } + + getBalance = async (sym1, asset1) => { + const { currentAccount } = this.props + let myBalance = this.state.myBalance + if (sym1 === 'GOLOS') { + myBalance = Asset(currentAccount.get('balance')) + } else if (sym1 === 'GBG') { + myBalance = Asset(currentAccount.get('sbd_balance')) + } else { + const res = await api.getAccountsBalances([currentAccount.get('name')]) + myBalance = res[0][sym1] ? Asset(res[0][sym1].balance) : Asset(0, asset1.precision, sym1) + } + return myBalance + } + + initOrders = async (sym1, sym2) => { + const now = Date.now() + if (!this.ordersUpdated || now - this.ordersUpdated > 3000) { + this.orders = await api.getOrderBookExtendedAsync(500, [sym1, sym2]) + this.ordersUpdated = now + } + } + + normalizeOrder = (order, sym1, sym2, prec1, prec2) => { + if (!order.asset1.symbol) + order.asset1 = Asset(order.asset1, prec1, sym1) + if (!order.asset2.symbol) + order.asset2 = Asset(order.asset2, prec1, sym2) + if (!order._price) + order._price = Price(order.order_price) + } + + calculate = async (sellAmount, buyAmount, myBalance, isSell = true) => { + let res = isSell ? buyAmount.clone() : sellAmount.clone() + res.amount = 0 + let req = isSell ? sellAmount.clone() : buyAmount.clone() + + await this.initOrders(sellAmount.symbol, buyAmount.symbol) + + this.limitPrice = null + this.bestPrice = null + + let orders = this.orders.bids + if (!orders.length) { + this.setState({ + warning: tt('convert_assets_jsx.no_orders_DIRECTION', { + DIRECTION: sellAmount.symbol + '/' + buyAmount.symbol + }) + }) + return null + } + + let warning = '' + + if (!req.amount) { + this.setState({ warning }) + return res + } + + for (let i = 0; i < orders.length; ++i) { + const order = orders[i] + this.normalizeOrder(order, sellAmount.symbol, buyAmount.symbol, sellAmount.precision, buyAmount.precision) + + const orderAmount = isSell ? order.asset1 : order.asset2 + + const amount = req.min(orderAmount) + let amount2 = amount.mul(order._price) + res = res.plus(amount2) + + this.bestPrice = this.bestPrice || order._price.clone() + this.limitPrice = order._price.clone() + + req = req.minus(orderAmount) + + if (req.amount <= 0) { + req.amount = 0 + break + } + } + + if (res.amount == 0) { + res.amount = 1 + warning = tt('convert_assets_jsx.too_low_amount') + } else if (!isSell && res.gt(myBalance)) { + res.amount = myBalance.amount + this.limitPrice = Price(req, res) + warning = tt('convert_assets_jsx.too_big_price') + } else if (req.amount > 0) { + warning = { + a1: (isSell ? sellAmount : buyAmount).minus(req).floatString, + a2: res.floatString, + remain: req.floatString, + isSell + } + if (!isSell) { + res = res.plus(req.mul(this.bestPrice)) + } + } + + this.setState({ warning }) + + return res + } + + onPairChange = async ({ event, sym1, asset1, sym2, asset2 }) => { + if (event) { + event.preventDefault() + } + if (this.state.canceled) { + this.setState({ canceled: false }) + return + } + let buyAmount = Asset(0, asset2.precision, sym2) + const myBalance = await this.getBalance(sym1, asset1) + + let sellAmount = myBalance.clone() + + this.ordersUpdated = null + + let fee = buyAmount.clone() + fee.amount = 0 + let calcBuy = await this.calculate(sellAmount, buyAmount, myBalance) + if (calcBuy) { + let calc = calcFeeForSell(calcBuy, this.state.assets[sym2].fee_percent) + calcBuy = calc.clearBuy + fee = calc.fee + } + + this.setState({ + myBalance, + sellAmount: AssetEditor(sellAmount), + sellError: '', + buyAmount: AssetEditor(calcBuy || buyAmount), + fee, + }) + } + + sellAmountUpdated = async () => { + const { sellAmount, buyAmount, myBalance, assets } = this.state + let newBuy = await this.calculate(sellAmount.asset, buyAmount.asset, myBalance) + if (newBuy) { + let calc = calcFeeForSell(newBuy, assets[buyAmount.asset.symbol].fee_percent) + newBuy = calc.clearBuy + this.setState({ + buyAmount: AssetEditor(newBuy), + fee: calc.fee + }) + } + } + + assetBalanceClick = e => { + e.preventDefault() + const { myBalance } = this.state + this.setState({ + sellAmount: AssetEditor(myBalance), + sellError: '' + }, () => { + this.sellAmountUpdated() + }) + } + + onSellAmountChange = (e) => { + let newAmount = this.state.sellAmount.withChange(e.target.value) + if (newAmount.hasChange && newAmount.asset.amount >= 0) { + let sellError = '' + if (newAmount.asset.gt(this.state.myBalance)) { + sellError = tt('transfer_jsx.insufficient_funds') + } + this.setState({ + sellAmount: newAmount, + sellError + }, () => { + this.sellAmountUpdated() + }) + } + } + + onBuyAmountChange = async (e) => { + let newAmount = this.state.buyAmount.withChange(e.target.value) + if (newAmount.hasChange && newAmount.asset.amount >= 0) { + this.setState({ + buyAmount: newAmount, + }) + + const { sellAmount, myBalance, assets } = this.state + let calc = calcFeeForBuy(newAmount.asset, assets[newAmount.asset.symbol].fee_percent) + + let newSell = await this.calculate(sellAmount.asset, calc.buyWF, myBalance, false) + if (newSell) { + this.setState({ + sellAmount: AssetEditor(newSell), + }) + } + this.setState({ + fee: calc.fee + }) + } + } + + submit = (e) => { + e.preventDefault() + const { + sellAmount: { asset: sellAmount }, + buyAmount: { asset: buyAmount } + } = this.state + let minToReceive + let confirm + if (this.limitPrice) { + minToReceive = sellAmount.mul(this.limitPrice) + + let possible = sellAmount.mul(this.bestPrice) + if (possible.minus(possible.mul(3000).div(10000)).gt(buyAmount)) { + confirm = tt('convert_assets_jsx.price_warning') + } + } else { + minToReceive = this.state.buyAmount.asset + } + + this.setState({ loading: true }) + + const { currentAccount } = this.props + this.props.placeOrder(currentAccount.get('name'), + sellAmount, minToReceive, confirm, async (orderid) => { + await new Promise(resolve => setTimeout(resolve, 4000)) + const orders = await api.getOpenOrdersAsync(currentAccount.get('name'), + [sellAmount.symbol, minToReceive.symbol]) + let found + for (let order of orders){ + if (order.orderid === orderid) { + found = order + break + } + } + const newState = { loading: false, finishedAcc: currentAccount.get('name') } + if (!found) { + this.setState({ + ...newState, + finished: 'full', + }) + } else if (found.asset1 === found.sell_price.base && found.asset2 === found.sell_price.quote) { + this.setState({ + ...newState, + finished: 'not' + }) + } else { + this.setState({ + ...newState, + finished: 'partly', + remainToReceive: Asset(found.sell_price.base) + }) + } + }, () => { + this.setState({ loading: false, canceled: true }) + }) + } + + _renderDescription() { + const { modal } = this.props + const delimiter = modal ? ' ' :
+ const lines = tt('convert_assets_jsx.description') + .reduce((prev, curr) => [prev, delimiter, curr]) + const width = modal ? ((this.sellSym().length + this.buySym().length) > 11 ? '30%' : '40%') : '50%' + return ( + {lines} + ) + } + + _renderPrice() { + const { sellAmount: {asset: sellAmount }, buyAmount: {asset: buyAmount } } = this.state + + if (sellAmount.amount == 0 || buyAmount.amount == 0) { + return null + } + + let res = (parseFloat(buyAmount.amountFloat) / parseFloat(sellAmount.amountFloat)).toString() + const dot = res.indexOf(res) + if (dot != -1) { + res = res.substring(0, dot + 9) + } + + return
+ {tt('convert_assets_jsx.price')} {'1 ' + sellAmount.symbol}:
+ {' ~' + res + ' ' + buyAmount.symbol} +
+ } + + _renderFields() { + const { direction } = this.props + const { myBalance, sellAmount, sellError, buyAmount } = this.state + const fieldSell = ( +
+ {tt('convert_assets_jsx.sell_amount')} +
+ + {this.sellSym()} +
+
+ + {sellError ?
{sellError}
: null} + {this._renderPrice()} +
) + const fieldBuy = ( +
+ {tt('convert_assets_jsx.buy_amount')} +
+ + {this.buySym()} +
+
+
{tt('convert_assets_jsx.coinmarketcap_value')}{cmc}
} + /> + {this._renderFee()} +
) + return (
+
+ {fieldSell} +
+
+ {fieldBuy} +
+
) + } + + _renderFee() { + const { buyAmount, fee, assets } = this.state + const { fee_percent } = assets[buyAmount.asset.symbol] + + let feeStr = fee.floatString + + const buyWF = buyAmount.asset.plus(fee) + + return (
+ + {tt('g.fee') + ': '} + {feeStr} + {fee_percent > 0 ? {' (' + (fee_percent / 100) + '%)'} : null} + + {fee.amount > 0 ?
+ {tt('convert_assets_jsx.amount_with_fee') + ' '} + {buyWF.amountFloat} +
: null} +
) + } + + _renderWarning() { + const { warning } = this.state + if (!warning) { + return null + } + let children = warning + if (warning.a1) { + const { a1, a2, remain, isSell } = warning + children = [ + {tt('convert_assets_jsx.too_much_amount1')}, + {a1}, + {isSell ? tt('convert_assets_jsx.too_much_amount2') : tt('convert_assets_jsx.too_much_amount2a')}, + {a2}, + {tt('convert_assets_jsx.too_much_amount3')}, + {remain}, + {tt('convert_assets_jsx.too_much_amount4')} + ] + } + return (
+ {children} +   + +
) + } + + render() { + const { direction } = this.props + const { loading, finished, assets, isSubmitting, sellAmount, sellError, buyAmount, warning } = this.state + if (loading) { + return (
+ +
) + } + if (finished) { + const { finishedAcc, remainToReceive } = this.state + return + } + const disabled = isSubmitting || !sellAmount.asset.amount || !buyAmount.asset.amount || sellError + return (
+

{tt('g.convert_assets')}

+
+ + {this._renderDescription()} +
+ {this._renderFields()} + {this._renderWarning()} +
+ + + + + {tt('filled_orders_jsx.open_market')} + + +
+
) + } +} + +export default connect( + (state, ownProps) => { + const defaults = state.user.get('convert_assets_defaults', Map()).toJS(); + + const currentUser = state.user.getIn(['current']) + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]) + + return { + ...ownProps, + sellSym: defaults.sellSym || undefined, + buySym: defaults.buySym || undefined, + direction: defaults.direction || undefined, + currentAccount, + }; + }, + dispatch => ({ + placeOrder: ( + owner, amountToSell, minToReceive, confirm, successCallback, errorCallback + ) => { + const orderid = generateOrderID() + + const operation = { + owner, + amount_to_sell: amountToSell, + min_to_receive: minToReceive, + fill_or_kill: false, + expiration: DEFAULT_EXPIRE, + orderid, + } + + dispatch( + transaction.actions.broadcastOperation({ + type: 'limit_order_create', + operation, + confirm, + successCallback: () => { + let pathname = window.location.pathname + dispatch({type: 'FETCH_STATE', payload: {pathname}}) + successCallback(orderid) + }, + errorCallback: () => { + errorCallback() + } + }) + ) + }, + showOpenOrders: defaults => { + dispatch(user.actions.setOpenOrdersDefaults(defaults)) + dispatch(user.actions.showOpenOrders()) + dispatch(user.actions.hideConvertAssets()) + }, + }) +)(ConvertAssets) diff --git a/app/components/modules/ConvertAssets.scss b/app/components/modules/ConvertAssets.scss new file mode 100644 index 000000000..850336e31 --- /dev/null +++ b/app/components/modules/ConvertAssets.scss @@ -0,0 +1,14 @@ +.ConvertAssets { + &__description { + display: inline-block; + margin-left: 1.5rem; + vertical-align: top; + margin-top: 0.1rem; + } + + .MarketLink { + svg { + fill: #0078C4; + } + } +} diff --git a/app/components/modules/Header.jsx b/app/components/modules/Header.jsx index 24fcfd164..281a5a3ca 100644 --- a/app/components/modules/Header.jsx +++ b/app/components/modules/Header.jsx @@ -144,6 +144,8 @@ class Header extends React.Component { if(route.params[1] === "posts" || route.params[1] === "comments"){ page_title = tt('header_jsx.comments_by') + " " + user_title; } + } else if (route.page === 'ConvertAssetsLoader') { + page_title = tt('g.convert_assets') } else { page_name = ''; //page_title = route.page.replace( /([a-z])([A-Z])/g, '$1 $2' ).toLowerCase(); } diff --git a/app/components/modules/Modals.jsx b/app/components/modules/Modals.jsx index e2017f7f3..ab65de4a4 100644 --- a/app/components/modules/Modals.jsx +++ b/app/components/modules/Modals.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import {connect} from 'react-redux'; import CloseButton from 'react-foundation-components/lib/global/close-button'; import Reveal from 'react-foundation-components/lib/global/reveal'; +import ConvertAssets from 'app/components/modules/ConvertAssets' import LoginForm from 'app/components/modules/LoginForm'; import ConfirmTransactionForm from 'app/components/modules/ConfirmTransactionForm'; import Transfer from 'app/components/modules/Transfer'; @@ -21,6 +22,7 @@ class Modals extends React.Component { show_login_modal: PropTypes.bool, show_confirm_modal: PropTypes.bool, show_transfer_modal: PropTypes.bool, + show_convert_assets_modal: PropTypes.bool, show_powerdown_modal: PropTypes.bool, show_signup_modal: PropTypes.bool, show_promote_post_modal: PropTypes.bool, @@ -52,10 +54,12 @@ class Modals extends React.Component { show_login_modal, show_confirm_modal, show_transfer_modal, + show_convert_assets_modal, show_powerdown_modal, show_signup_modal, hideLogin, hideTransfer, + hideConvertAssets, hidePowerdown, hideConfirm, hideSignUp, @@ -86,6 +90,10 @@ class Modals extends React.Component { } + {show_convert_assets_modal && + + + } {show_powerdown_modal && ( @@ -119,6 +127,7 @@ export default connect( loginUnclosable, show_confirm_modal: state.transaction.get('show_confirm_modal'), show_transfer_modal: state.user.get('show_transfer_modal'), + show_convert_assets_modal: state.user.get('show_convert_assets_modal'), show_promote_post_modal: state.user.get('show_promote_post_modal'), show_signup_modal: state.user.get('show_signup_modal'), show_powerdown_modal: state.user.get('show_powerdown_modal'), @@ -139,6 +148,10 @@ export default connect( if (e) e.preventDefault(); dispatch(user.actions.hideTransfer()) }, + hideConvertAssets: e => { + if (e) e.preventDefault(); + dispatch(user.actions.hideConvertAssets()) + }, hidePowerdown: e => { if (e) e.preventDefault(); dispatch(user.actions.hidePowerdown()); diff --git a/app/components/modules/OpenOrders.jsx b/app/components/modules/OpenOrders.jsx index ba0808fbb..7665227cc 100644 --- a/app/components/modules/OpenOrders.jsx +++ b/app/components/modules/OpenOrders.jsx @@ -7,7 +7,7 @@ import { api, } from 'golos-lib-js'; import { Asset, } from 'golos-lib-js/lib/utils'; import transaction from 'app/redux/Transaction'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; -import { roundDown, roundUp, } from 'app/utils/MarketUtils'; +import { roundDown, roundUp, } from 'app/utils/market/utils' class OpenOrders extends Component { state = { diff --git a/app/components/modules/Transfer.jsx b/app/components/modules/Transfer.jsx index ba7ffcbc0..f9e1a2378 100644 --- a/app/components/modules/Transfer.jsx +++ b/app/components/modules/Transfer.jsx @@ -249,7 +249,7 @@ class TransferForm extends Component { // null if fee not set, NaN or zero let fee = (withdrawal.fee && parseFloat(withdrawal.fee) )?
- {tt('asset_edit_withdrawal_jsx.fee')} + {tt('g.fee') + ': '} {withdrawal.fee.toString()} {' '} {sym} diff --git a/app/components/modules/UserWallet.jsx b/app/components/modules/UserWallet.jsx index e90911489..6dbec60c3 100644 --- a/app/components/modules/UserWallet.jsx +++ b/app/components/modules/UserWallet.jsx @@ -2,6 +2,10 @@ import React from 'react'; import {connect} from 'react-redux'; import {Link} from 'react-router'; import g from 'app/redux/GlobalReducer'; +import tt from 'counterpart'; +import {List} from 'immutable'; + +import ConvertAssetsBtn from 'app/components/elements/market/ConvertAssetsBtn' import SavingsWithdrawHistory from 'app/components/elements/SavingsWithdrawHistory'; import TransferHistoryRow from 'app/components/cards/TransferHistoryRow'; import TransactionError from 'app/components/elements/TransactionError'; @@ -14,8 +18,6 @@ import WalletSubMenu from 'app/components/elements/WalletSubMenu'; import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; import Tooltip from 'app/components/elements/Tooltip'; import Icon from 'app/components/elements/Icon'; -import tt from 'counterpart'; -import {List} from 'immutable'; import Callout from 'app/components/elements/Callout'; import { LIQUID_TICKER, VEST_TICKER, DEBT_TICKER} from 'app/client_config'; import transaction from 'app/redux/Transaction'; @@ -221,7 +223,6 @@ class UserWallet extends React.Component { { value: tt('userwallet_jsx.power_up'), link: '#', onClick: showTransfer.bind( this, VEST_TICKER, 'Transfer to Account' ) }, { value: tt('userwallet_jsx.transfer_to_savings'), link: '#', onClick: showTransfer.bind( this, LIQUID_TICKER, 'Transfer to Savings' ) }, { value: tt('userwallet_jsx.convert_to_DEBT_TOKEN', {DEBT_TOKEN}), link: '#', onClick: showConvertDialog.bind(this, LIQUID_TICKER, DEBT_TICKER) }, - { value: tt('g.buy_or_sell'), link: '/market/GOLOS/GBG' }, ] let power_menu = [ { value: tt('userwallet_jsx.power_down'), link: '#', onClick: powerDown.bind(this, false) }, @@ -237,7 +238,6 @@ class UserWallet extends React.Component { { value: tt('g.transfer'), link: '#', onClick: showTransfer.bind( this, DEBT_TICKER, 'Transfer to Account' ) }, { value: tt('userwallet_jsx.transfer_to_savings'), link: '#', onClick: showTransfer.bind( this, DEBT_TICKER, 'Transfer to Savings' ) }, { value: tt('userwallet_jsx.convert_to_LIQUID_TOKEN', {LIQUID_TOKEN}), link: '#', onClick: showConvertDialog.bind(this, DEBT_TICKER, LIQUID_TICKER) }, - { value: tt('g.buy_or_sell'), link: '/market/GBG/GOLOS' }, ] const isWithdrawScheduled = new Date(account.get('next_vesting_withdrawal') + 'Z').getTime() > Date.now() @@ -381,6 +381,10 @@ class UserWallet extends React.Component { {steemTip.split(".").map((a, index) => {if (a) {return
{a}.
;} return null;})}
+ {isMyAccount + ? 0 ? 'sell' : 'buy'} /> + : undefined + } {isMyAccount ? {sbdMessage}
+ {isMyAccount + ? 0 ? 'sell' : 'buy'} /> + : undefined + } {isMyAccount ?
- {tt('asset_edit_withdrawal_jsx.fee')} + {tt('g.fee') + ': '}
- {tt('asset_edit_withdrawal_jsx.fee')} + {tt('g.fee') + ': '}
{tt('asset_edit_withdrawal_jsx.min_amount')} {min_amount} {sym || ''}
} {fee &&
- {tt('asset_edit_withdrawal_jsx.fee')} {fee} {sym || ''}
} + {tt('g.fee') + ': '}{fee} {sym || ''}
}
; } diff --git a/app/components/modules/uia/Assets.jsx b/app/components/modules/uia/Assets.jsx index 9f201d02d..ac09b20da 100644 --- a/app/components/modules/uia/Assets.jsx +++ b/app/components/modules/uia/Assets.jsx @@ -1,20 +1,24 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' +import {connect} from 'react-redux'; +import { Link } from 'react-router'; +import tt from 'counterpart'; +import { Asset } from 'golos-lib-js/lib/utils'; + +import ConvertAssetsBtn from 'app/components/elements/market/ConvertAssetsBtn' import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' import {longToAsset} from 'app/utils/ParsersAndFormatters'; import g from 'app/redux/GlobalReducer' import user from 'app/redux/User'; -import {connect} from 'react-redux'; -import { Link } from 'react-router'; import DialogManager from 'app/components/elements/common/DialogManager'; import Icon from 'app/components/elements/Icon'; import Author from 'app/components/elements/Author'; import Button from 'app/components/elements/Button'; import FoundationDropdownMenu from 'app/components/elements/FoundationDropdownMenu'; -import tt from 'counterpart'; import AssetRules from 'app/components/modules/uia/AssetRules'; import Reveal from 'react-foundation-components/lib/global/reveal'; import Tooltip from 'app/components/elements/Tooltip'; +import { normalizeAssets, getTradablesFor } from 'app/utils/market/utils' class Assets extends Component { static propTypes = { @@ -110,9 +114,13 @@ class Assets extends Component { if (!mutedUIA) mutedUIA = []; } + const assets = this.props.assets.toJS() + + const assetsNorm = normalizeAssets(assets) + let show_load_more = false; let my_assets = []; - for (const [sym, item] of Object.entries(this.props.assets.toJS())) { + for (const [sym, item] of Object.entries(assets)) { if (!item.my) { show_load_more = true; if (!this.state.show_full_list) continue; @@ -168,6 +176,11 @@ class Assets extends Component { const ordersStr = item.market_balance; + const tradables = getTradablesFor(assetsNorm, [sym], true) + + const parsed = Asset(item.balance) + const convertDirection = parsed.amount ? 'sell' : 'buy' + my_assets.push( {description.length ? ( @@ -187,6 +200,10 @@ class Assets extends Component { : null} + {isMyAccount + ? + : undefined + } {isMyAccount ? {tt('convert_assets_jsx.please_authorize')} + } + return (
+
+ +
+
) + } +} + +module.exports = { + path: '/convert', + component: connect( + (state, ownProps) => { + const currentUser = state.user.getIn(['current']) + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]) + + return { + currentAccount, + } + }, + dispatch => ({ + }) + )(ConvertAssetsLoader), +} diff --git a/app/components/pages/Market.jsx b/app/components/pages/Market.jsx index f46ede641..9be6a4778 100644 --- a/app/components/pages/Market.jsx +++ b/app/components/pages/Market.jsx @@ -5,19 +5,23 @@ import { connect } from 'react-redux'; import { Link, browserHistory } from 'react-router'; import tt from 'counterpart'; import {api, broadcast} from 'golos-lib-js' + import transaction from 'app/redux/Transaction'; import {longToAsset} from 'app/utils/ParsersAndFormatters'; import TransactionError from 'app/components/elements/TransactionError'; import Icon from 'app/components/elements/Icon'; -import DropdownMenu from 'app/components/elements/DropdownMenu'; -import PriceChart from 'app/components/elements/PriceChart'; import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; -import Orderbook from 'app/components/elements/Orderbook'; -import OrderHistory from 'app/components/elements/OrderHistory'; -import { Order, TradeHistory } from 'app/utils/MarketClasses'; -import { roundUp, roundDown } from 'app/utils/MarketUtils'; -import TickerPriceStat from 'app/components/elements/TickerPriceStat'; +import DropdownMenu from 'app/components/elements/DropdownMenu'; import { DEBT_TOKEN_SHORT, LIQUID_TICKER } from 'app/client_config'; + +import Order from 'app/utils/market/Order' +import TradeHistory from 'app/utils/market/TradeHistory' +import { roundUp, roundDown, normalizeAssets, DEFAULT_EXPIRE, generateOrderID } from 'app/utils/market/utils' +import MarketPair from 'app/components/elements/market/MarketPair' +import OrderBook from 'app/components/elements/market/OrderBook'; +import OrderHistory from 'app/components/elements/market/OrderHistory'; +import PriceChart from 'app/components/elements/market/PriceChart'; +import TickerPriceStat from 'app/components/elements/market/TickerPriceStat'; import './Market.scss'; const BY_TYPE = 'type' @@ -42,8 +46,6 @@ class Market extends Component { sellPriceWarning: false, buySteemFeePct: '0%', sellSteemFeePct: '0%', - sym1_list_page: 0, - sym2_list_page: 0, ordersSorting: BY_TYPE }; @@ -56,19 +58,14 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) if (this.refs.buySteemPrice) { - this.refs.buySteemPrice.value = parseFloat(lowest_ask).toFixed(assets_right[sym2].precision); + this.refs.buySteemPrice.value = parseFloat(lowest_ask).toFixed(assetsNorm[sym2].precision); } if (this.refs.sellSteem_price) { - this.refs.sellSteem_price.value = parseFloat(highest_bid).toFixed(assets_right[sym2].precision); + this.refs.sellSteem_price.value = parseFloat(highest_bid).toFixed(assetsNorm[sym2].precision); } } } @@ -155,12 +152,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) const { placeOrder, user } = this.props; if (!user) return; @@ -197,12 +189,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) const { placeOrder, user } = this.props; if (!user) { @@ -280,24 +267,19 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) - this.refs.sellSteem_price.value = p.toFixed(assets_right[sym2].precision); - this.refs.buySteemPrice.value = p.toFixed(assets_right[sym2].precision); + this.refs.sellSteem_price.value = p.toFixed(assetsNorm[sym2].precision); + this.refs.buySteemPrice.value = p.toFixed(assetsNorm[sym2].precision); const samount = parseFloat(this.refs.sellSteem_amount.value); if (samount >= 0) { - this.refs.sellSteem_total.value = roundDown(p * samount, assets_right[sym1].precision).toFixed(assets_right[sym2].precision);; + this.refs.sellSteem_total.value = roundDown(p * samount, assetsNorm[sym1].precision).toFixed(assetsNorm[sym2].precision);; } const bamount = parseFloat(this.refs.buySteemAmount.value); if (bamount >= 0) { - this.refs.buySteemTotal.value = roundDown(p * bamount, assets_right[sym2].precision).toFixed(assets_right[sym2].precision);; + this.refs.buySteemTotal.value = roundDown(p * bamount, assetsNorm[sym2].precision).toFixed(assetsNorm[sym2].precision);; } this.validateBuySteem(); @@ -329,16 +311,11 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) - this.refs.buySteemFee.value = (amount * assets_right[sym1].fee_percent / 10000).toFixed(assets_right[sym1].precision) + this.refs.buySteemFee.value = (amount * assetsNorm[sym1].fee_percent / 10000).toFixed(assetsNorm[sym1].precision) this.setState( { - buySteemFeePct: longToAsset(assets_right[sym1].fee_percent, '', 2) + '%' + buySteemFeePct: longToAsset(assetsNorm[sym1].fee_percent, '', 2) + '%' }) } }); @@ -362,16 +339,11 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) - this.refs.sellSteem_fee.value = (total * assets_right[sym2].fee_percent / 10000).toFixed(assets_right[sym2].precision) + this.refs.sellSteem_fee.value = (total * assetsNorm[sym2].fee_percent / 10000).toFixed(assetsNorm[sym2].precision) this.setState( { - sellSteemFeePct: longToAsset(assets_right[sym2].fee_percent, '', 2) + '%' + sellSteemFeePct: longToAsset(assetsNorm[sym2].fee_percent, '', 2) + '%' }) } }); @@ -393,15 +365,10 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) - total = (total + parseFloat(1) / Math.pow(10, assets_right[sym2].precision)).toString(); - this.refs.buySteemTotal.value = this.toFixedAccur(total, assets_right[sym2].precision); + total = (total + parseFloat(1) / Math.pow(10, assetsNorm[sym2].precision)).toString(); + this.refs.buySteemTotal.value = this.toFixedAccur(total, assetsNorm[sym2].precision); }; fixSellTotal = () => { @@ -415,15 +382,10 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) - total = (total + parseFloat(1) / Math.pow(10, assets_right[sym2].precision)).toString(); - this.refs.sellSteem_total.value = this.toFixedAccur(total, assets_right[sym2].precision); + total = (total + parseFloat(1) / Math.pow(10, assetsNorm[sym2].precision)).toString(); + this.refs.sellSteem_total.value = this.toFixedAccur(total, assetsNorm[sym2].precision); }; fixBuyAmount = () => { @@ -437,14 +399,9 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) - amount = (total / price).toFixed(assets_right[sym1].precision); + amount = (total / price).toFixed(assetsNorm[sym1].precision); this.refs.buySteemAmount.value = amount; }; @@ -459,43 +416,12 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) - amount = (total / price).toFixed(assets_right[sym1].precision); + amount = (total / price).toFixed(assetsNorm[sym1].precision); this.refs.sellSteem_amount.value = amount; }; - nextSym1ListPage = () => { - this.setState({ - sym1_list_page: this.state.sym1_list_page+1 - }); - } - - prevSym1ListPage = () => { - if (this.state.sym1_list_page == 0) return; - this.setState({ - sym1_list_page: this.state.sym1_list_page-1 - }); - } - - nextSym2ListPage = () => { - this.setState({ - sym2_list_page: this.state.sym2_list_page+1 - }); - } - - prevSym2ListPage = () => { - if (this.state.sym2_list_page == 0) return; - this.setState({ - sym2_list_page: this.state.sym2_list_page-1 - }); - } - render() { let {sym1, sym2} = this.props.routeParams if (!sym1 || !sym2) { @@ -536,20 +462,15 @@ class Market extends Component {
) - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } - for (let [key, value] of Object.entries(assets_right)) { + let assetsNorm = normalizeAssets(assets) + for (let [key, value] of Object.entries(assetsNorm)) { if (!value.json_metadata) { return (
); } } - let prec1 = assets_right[sym1].precision - let prec2 = assets_right[sym2].precision + let prec1 = assetsNorm[sym1].precision + let prec2 = assetsNorm[sym2].precision const LIQUID_TOKEN = tt('token_names.LIQUID_TOKEN'); const LIQUID_TOKEN_UPPERCASE = tt('token_names.LIQUID_TOKEN_UPPERCASE'); @@ -573,8 +494,6 @@ class Market extends Component { sellDisabled, buyPriceWarning, sellPriceWarning, - sym1_list_page, - sym2_list_page } = this.state; let ticker = { @@ -737,7 +656,7 @@ class Market extends Component { } {tt(need_reverse ? (o.type === 'bid' ? 'g.sell' : 'g.buy') : (o.type === 'ask' ? 'g.sell' : 'g.buy'))} - {o.price.toFixed(assets_right[sym2].precision)} + {o.price.toFixed(assetsNorm[sym2].precision)} {o.asset1} {o.asset2.replace('SBD', DEBT_TOKEN_SHORT)} @@ -787,44 +706,6 @@ class Market extends Component { ); } - let symbols1 = []; - let symbols2 = []; - for (let [key, value] of Object.entries(assets_right)) { - let description = "" - let image_url = "" - if (value.json_metadata.startsWith('{')) { - let json_metadata = JSON.parse(value.json_metadata) - description = json_metadata.description - image_url = json_metadata.image_url - } - - if (sym1 !== key && sym2 !== key && (!value.symbols_whitelist.length || value.symbols_whitelist.includes(sym2)) && (!assets_right[sym2].symbols_whitelist.length || assets_right[sym2].symbols_whitelist.includes(key))) - symbols1.push({key: key, value: key, - label: (   {key}), - link: '/market/' + key + '/' + sym2, - onClick: (e) => {window.location.href = '/market/' + key + '/' + sym2}}); - - if (sym1 !== key && sym2 !== key && (!value.symbols_whitelist.length || value.symbols_whitelist.includes(sym1)) && (!assets_right[sym1].symbols_whitelist.length || assets_right[sym1].symbols_whitelist.includes(key))) - symbols2.push({key: key, value: key, - label: (   {key}), - link: '/market/' + sym1 + '/' + key, - onClick: (e) => {window.location.href = '/market/' + sym1 + '/' + key}}); - } - - let next_sym1_list = symbols1.slice(10*(sym1_list_page+1), 10*(sym1_list_page+1)+10); - symbols1 = symbols1.slice(10*sym1_list_page, 10*sym1_list_page+10); - - symbols1.push({value: -
{sym1_list_page > 0 ? '< ' + tt('g.back') : ''} - 0 ? this.nextSym1ListPage : null}>{next_sym1_list.length > 0 ? tt('g.more_list') + ' >' : ''}
}); - - let next_sym2_list = symbols2.slice(10*(sym2_list_page+1), 10*(sym2_list_page+1)+10); - symbols2 = symbols2.slice(10*sym2_list_page, 10*sym2_list_page+10); - - symbols2.push({value: - {sym2_list_page > 0 ? '< ' + tt('g.back') : ''} - 0 ? this.nextSym2ListPage : null}>{next_sym2_list.length > 0 ? tt('g.more_list') + ' >' : ''}}); - const normalizeTrades = trades => trades.map(t => new TradeHistory(t, sym1, sym2, prec1, prec2)); const trades = this.props.history ? normalizeTrades(this.props.history) : []; @@ -847,27 +728,21 @@ class Market extends Component { trades={trades} /> -

- - - {sym1 === "GOLOS" ? () : null} - {sym1 === "GBG" ? () : null} - {sym1} - {symbols1.length > 0 && } - - -   - -   - - - {sym2 === "GOLOS" ? () : null} - {sym2 === "GBG" ? () : null} - {sym2} - {symbols2.length > 0 && } - -
- +

+
+ { + return '/market/' + sym1 + '/' + sym2 + }} + onChange={({event, link}) => { + if (event) { + event.preventDefault() + window.location.href = link + } + }} + /> +
+
@@ -910,12 +785,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) const amount = parseFloat( this.refs.buySteemAmount @@ -925,7 +795,7 @@ class Market extends Component { this.refs.buySteemPrice .value ); - let new_price = price.toFixed(assets_right[sym2].precision); + let new_price = price.toFixed(assetsNorm[sym2].precision); if (new_price.length < price.toString().length) { this.refs.buySteemPrice .value = new_price; @@ -937,8 +807,8 @@ class Market extends Component { if (amount >= 0 && price >= 0) this.refs.buySteemTotal.value = roundDown( price * amount, - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision); + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision); validateBuySteem(); fixBuyTotal(); }} @@ -967,12 +837,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) const price = parseFloat( this.refs.buySteemPrice @@ -982,7 +847,7 @@ class Market extends Component { this.refs.buySteemAmount .value ); - let new_amount = amount.toFixed(assets_right[sym1].precision); + let new_amount = amount.toFixed(assetsNorm[sym1].precision); if (new_amount.length < amount.toString().length) { this.refs.buySteemAmount .value = new_amount; @@ -995,8 +860,8 @@ class Market extends Component { let res = price * amount this.refs.buySteemTotal.value = roundDown( res, - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision) + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision) } validateBuySteem(); fixBuyTotal(); @@ -1027,12 +892,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) const price = parseFloat( this.refs.buySteemPrice @@ -1042,7 +902,7 @@ class Market extends Component { this.refs.buySteemTotal .value ); - let new_total = total.toFixed(assets_right[sym2].precision); + let new_total = total.toFixed(assetsNorm[sym2].precision); if (new_total.length < total.toString().length) { this.refs.buySteemTotal .value = new_total; @@ -1054,8 +914,8 @@ class Market extends Component { if (total >= 0 && price >= 0) this.refs.buySteemAmount.value = roundUp( total / price, - assets_right[sym1].precision - ).toFixed(assets_right[sym1].precision);; + assetsNorm[sym1].precision + ).toFixed(assetsNorm[sym1].precision);; validateBuySteem(); fixBuyAmount(); }} @@ -1111,12 +971,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) e.preventDefault(); const price = parseFloat( @@ -1142,14 +997,14 @@ class Market extends Component { if (price >= 0) { let amount = roundDown( parseFloat(total) / price, - assets_right[sym1].precision + assetsNorm[sym1].precision ); - this.refs.buySteemAmount.value = amount.toFixed(assets_right[sym1].precision); + this.refs.buySteemAmount.value = amount.toFixed(assetsNorm[sym1].precision); let res = price * amount this.refs.buySteemTotal.value = roundDown( res, - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision) + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision) } validateBuySteem(); fixBuyTotal(); @@ -1178,12 +1033,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) e.preventDefault(); const amount = parseFloat( @@ -1194,18 +1044,18 @@ class Market extends Component { ticker.lowest_ask ); this.refs.buySteemPrice.value = - ticker.lowest_ask.toFixed(assets_right[sym2].precision); + ticker.lowest_ask.toFixed(assetsNorm[sym2].precision); if (amount >= 0) this.refs.buySteemTotal.value = roundDown( amount * price, - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision); + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision); validateBuySteem(); }} > {tt('market_jsx.lowest_ask')}: {' '} - {ticker.lowest_ask.toFixed(assets_right[sym2].precision)}
+ {ticker.lowest_ask.toFixed(assetsNorm[sym2].precision)}
@@ -1247,12 +1097,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) let amount = parseFloat( this.refs.sellSteem_amount @@ -1262,7 +1107,7 @@ class Market extends Component { this.refs.sellSteem_price .value ); - let new_price = price.toFixed(assets_right[sym2].precision); + let new_price = price.toFixed(assetsNorm[sym2].precision); if (new_price.length < price.toString().length) { this.refs.sellSteem_price .value = new_price; @@ -1274,8 +1119,8 @@ class Market extends Component { if (amount >= 0 && price >= 0) this.refs.sellSteem_total.value = roundDown( price * amount, - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision); + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision); validateSellSteem(); fixSellTotal(); }} @@ -1304,12 +1149,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) const price = parseFloat( this.refs.sellSteem_price @@ -1319,7 +1159,7 @@ class Market extends Component { this.refs.sellSteem_amount .value ); - let new_amount = amount.toFixed(assets_right[sym1].precision); + let new_amount = amount.toFixed(assetsNorm[sym1].precision); if (new_amount.length < amount.toString().length) { this.refs.sellSteem_amount .value = new_amount; @@ -1331,8 +1171,8 @@ class Market extends Component { if (price >= 0 && amount >= 0) this.refs.sellSteem_total.value = roundDown( price * amount, - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision); + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision); validateSellSteem(); fixSellTotal(); }} @@ -1361,12 +1201,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) const price = parseFloat( this.refs.sellSteem_price @@ -1376,7 +1211,7 @@ class Market extends Component { this.refs.sellSteem_total .value ); - let new_total = total.toFixed(assets_right[sym2].precision); + let new_total = total.toFixed(assetsNorm[sym2].precision); if (new_total.length < total.toString().length) { this.refs.sellSteem_total .value = new_total; @@ -1388,8 +1223,8 @@ class Market extends Component { if (price >= 0 && total >= 0) this.refs.sellSteem_amount.value = roundUp( total / price, - assets_right[sym1].precision - ).toFixed(assets_right[sym1].precision); + assetsNorm[sym1].precision + ).toFixed(assetsNorm[sym1].precision); validateSellSteem(); fixSellAmount(); }} @@ -1445,12 +1280,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) e.preventDefault(); const price = parseFloat( @@ -1476,8 +1306,8 @@ class Market extends Component { if (price >= 0) this.refs.sellSteem_total.value = roundDown( price * parseFloat(amount), - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision); + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision); validateSellSteem(); fixSellTotal(); }} @@ -1505,12 +1335,7 @@ class Market extends Component { sym2 = sym2.toUpperCase() let assets = this.props.assets; - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } + let assetsNorm = normalizeAssets(assets) e.preventDefault(); const amount = parseFloat( @@ -1520,20 +1345,20 @@ class Market extends Component { ); const price = ticker.highest_bid; - this.refs.sellSteem_price.value = price.toFixed(assets_right[sym2].precision); + this.refs.sellSteem_price.value = price.toFixed(assetsNorm[sym2].precision); if (amount >= 0) this.refs.sellSteem_total.value = roundDown( parseFloat(price) * amount, - assets_right[sym2].precision - ).toFixed(assets_right[sym2].precision); + assetsNorm[sym2].precision + ).toFixed(assetsNorm[sym2].precision); validateSellSteem(); fixSellTotal(); }} > {tt('market_jsx.highest_bid')}: {' '} - {ticker.highest_bid.toFixed(assets_right[sym2].precision)}
+ {ticker.highest_bid.toFixed(assetsNorm[sym2].precision)}
@@ -1544,15 +1369,15 @@ class Market extends Component {
-{assets && assets_right[sym1].allow_override_transfer && (

{tt('market_jsx.asset_') + sym1 + tt('market_jsx.asset_is_overridable')} {tt('g.more_hint')} >

)} -{assets && assets_right[sym2].allow_override_transfer && (

{tt('market_jsx.asset_') + sym2 + tt('market_jsx.asset_is_overridable')} {tt('g.more_hint')} >

)} +{assets && assetsNorm[sym1].allow_override_transfer && (

{tt('market_jsx.asset_') + sym1 + tt('market_jsx.asset_is_overridable')} {tt('g.more_hint')} >

)} +{assets && assetsNorm[sym2].allow_override_transfer && (

{tt('market_jsx.asset_') + sym2 + tt('market_jsx.asset_is_overridable')} {tt('g.more_hint')} >

)}

{tt('market_jsx.buy_orders')}

-

{tt('market_jsx.sell_orders')}

- { @@ -1737,14 +1561,9 @@ export default connect( ) => { // create_order jsc 12345 "1.000 SBD" "100.000 STEEM" true 1467122240 false - let assets_right = {} - assets_right['GOLOS'] = {supply: '0.000 GOLOS', precision: 3, fee_percent: 0, json_metadata: '{"image_url": "/images/golos.png"}'} - assets_right['GBG'] = {supply: '0.000 GBG', precision: 3, fee_percent: 0, json_metadata: '{"image_url": "/images/gold-golos.png"}'} - for (let [key, value] of Object.entries(assets)) { - assets_right[key] = value - } - let prec1 = assets_right[sym1].precision - let prec2 = assets_right[sym2].precision + let assetsNorm = normalizeAssets(assets) + let prec1 = assetsNorm[sym1].precision + let prec2 = assetsNorm[sym2].precision // Padd amounts to 3 decimal places amount_to_sell = amount_to_sell.replace( @@ -1772,14 +1591,14 @@ export default connect( { marketPrice: sym2 + ' ' + - parseFloat(marketPrice).toFixed(assets_right[sym2].precision) + + parseFloat(marketPrice).toFixed(assetsNorm[sym2].precision) + '/' + sym1, } ) : null; - const orderid = Math.floor(Date.now() / 1000); + const orderid = generateOrderID() const operation = { owner, diff --git a/app/components/pages/Market.scss b/app/components/pages/Market.scss index 3622fd3cf..3053c3862 100644 --- a/app/components/pages/Market.scss +++ b/app/components/pages/Market.scss @@ -143,13 +143,3 @@ table.Market__trade-history > tbody > tr.animate { tr { transition: background-color 750ms linear; } - -.Market__votes_pagination { - display: inline-block !important; - width: 100px; - font-size: 16px; -} - -.Market__pairs .VerticalMenu>li>a { - padding: .4rem 1rem; -} diff --git a/app/components/pages/Witnesses.jsx b/app/components/pages/Witnesses.jsx index 1235c8a65..b6d36b7ee 100644 --- a/app/components/pages/Witnesses.jsx +++ b/app/components/pages/Witnesses.jsx @@ -55,7 +55,7 @@ class Witnesses extends Component { loadMoreVotes = async ({ newPage, items }) => { const lastItem = items[items.length - 1]; let { _witness, } = lastItem; - const res = await api.getWitnessVotesAsync([_witness], 21, newPage*20, '1.000 GOLOS'); + const res = await api.getWitnessVotesAsync([_witness], 21, (newPage-1)*20, '1.000 GOLOS'); const nextItems = res[_witness]; const oneM = Math.pow(10, 6); let voteList = []; @@ -208,6 +208,7 @@ class Witnesses extends Component { item} perPage={20} onLoadMore={this.loadMoreVotes}> {formatAsset(approval + ' GOLOS', false)} diff --git a/app/locales/en.json b/app/locales/en.json index 110e078dc..55b695c02 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -110,6 +110,7 @@ "confirm_unfollow": "Are you sure you want to unfollow this user?", "continue": "continue", "convert": "Convert", + "convert_assets": "Exchange", "date": "Date", "decrypt": "Decrypt", "delete": "Delete", @@ -118,6 +119,7 @@ "edit": "Edit", "email": "Email", "expand": "Expand", + "fee": "Fee", "feed": "Feed", "feeds": "Feeds", "flag": "Downvote", @@ -752,7 +754,6 @@ "title": "Withdrawal Rules", "to": "Where to send tokens: ", "min_amount": "Min amount: ", - "fee": "Fee: ", "details": "Additional info: ", "unavailable": "Service maintenance", "way_name": "Service name: ", @@ -817,6 +818,41 @@ "APP_NAME_password_backup_required": "%(APP_NAME)s Password Backup (required)", "after_printing_write_down_your_user_name": "After printing, write down your user name" }, + "convert_assets_jsx": { + "description": [ + "Here you can sell or buy tokens.", + "Instantly and just in two clicks." + ], + "sell": "sell", + "buy": "buy", + "sell_amount": "Sell amount:", + "buy_amount": "Buy amount:", + "amount_with_fee": "Amount with fee:", + "price": "Price per ", + "no_tradables": "Cannot exchange this asset", + "please_authorize": "Please authorize.", + "too_low_amount": "Amount is too low, so order will be added into market queue.", + "too_much_amount1": "Instantly we can exchange ", + "too_much_amount2": " only (you will receive ", + "too_much_amount2a": " only (you will pay ", + "too_much_amount3": "). The rest of the funds ", + "too_much_amount4": " will be added into market queue.", + "no_orders_DIRECTION": "No tokens in %(DIRECTION)s at current moment. Order will be placed into market queue.", + "too_big_price": "No orders for that price, so exchange can take up to few days.", + "order_can_be_canceled": "If exchange will not occur even in the future, you can cancel the order, and funds will be refunded instantly.", + "coinmarketcap_value": "Value: ", + "finished": "Exchange is successful!", + "finished_desc": "Funds are on ", + "finished_balance": "your balance", + "partly": "Exchange proceed partly...", + "partly_desc": "There is remain to receive ", + "partly_desc2": ". Maybe older will proceed in few days. You can ", + "partly_link": "cancel the order.", + "partly_desc3": "Received amount is on ", + "not": "Exchange not yet proceed...", + "not_desc": "Exchange takes more time than usually. Maybe older will proceed in few days. If no, or if you cannot wait, you can ", + "price_warning": "This price is 30%% above than recommended one. Continue?" + }, "converttosteem_jsx": { "title_FROM_TO": "Convert %(FROM)s to %(TO)s", "confirm_title": "Confirm Convert", diff --git a/app/locales/ru-RU.json b/app/locales/ru-RU.json index 3cd64058b..53c825919 100644 --- a/app/locales/ru-RU.json +++ b/app/locales/ru-RU.json @@ -194,6 +194,7 @@ "confirm_unfollow": "Вы действительно хотите отменить подписку на этого пользователя?", "continue": "продолжить", "convert": "Конвертировать", + "convert_assets": "Обменять", "created": "Сначала новые", "old": "Сначала старые", "date": "Дата", @@ -204,6 +205,7 @@ "edit": "Редактировать", "email": "Электронная почта", "expand": "Развернуть", + "fee": "Комиссия", "feed": "Лента", "feeds": "Лента", "flag": "Голосовать против", @@ -823,6 +825,41 @@ "APP_NAME_password_backup_required": "%(APP_NAME)s резервное копирование пароля (обязательно!)", "after_printing_write_down_your_user_name": "После печати запишите ваше имя пользователя" }, + "convert_assets_jsx": { + "description": [ + "Здесь можно купить или продать токены.", + "В пару кликов и мгновенно." + ], + "sell": "продать", + "buy": "купить", + "sell_amount": "Сумма продажи:", + "buy_amount": "Сумма покупки:", + "amount_with_fee": "Сумма с комиссией:", + "price": "Цена за ", + "no_tradables": "Этот актив не меняется", + "please_authorize": "Пожалуйста, авторизуйтесь.", + "too_low_amount": "Сумма слишком маленькая, сделка будет поставлена в очередь на бирже.", + "too_much_amount1": "Сейчас будут обменяны ", + "too_much_amount2": " (вы получите ", + "too_much_amount2a": " (вы заплатите ", + "too_much_amount3": "). Остальные ", + "too_much_amount4": " будут поставлены в очередь на бирже.", + "no_orders_DIRECTION": "Сейчас нет токенов по направлению %(DIRECTION)s. Сделка будет поставлена в очередь на бирже.", + "too_big_price": "Пока что нет ордеров по такой цене, так что обмен займет до нескольких дней.", + "order_can_be_canceled": "Если обмена не произойдет и в дальнейшем, вы сможете отменить сделку в кошельке и средства будут мгновенно возвращены.", + "coinmarketcap_value": "Ценность: ", + "finished": "Обмен прошел успешно!", + "finished_desc": "Токены уже лежат на ", + "finished_balance": "вашем балансе", + "partly": "Обмен прошел частично...", + "partly_desc": "Осталось получить ", + "partly_desc2": ". Возможно, сделка пройдет в течение нескольких дней. Вы можете ", + "partly_link": "отменить сделку", + "partly_desc3": "Полученная часть суммы лежит на ", + "not": "Обмен пока не прошел...", + "not_desc": "Обмен идет дольше обычного. Возможно, он пройдет за несколько дней. Если этого не произойдет или если вы не можете ждать, то можно ", + "price_warning": "Эта цена на 30%% выше (менее выгодна для вас), чем рыночная. Продолжить обмен?" + }, "tips_js": { "tip_balance_hint": "Для вознаграждений пользователей, и на который получаете вознаграждения сами. Токены с него можно также отправить на увеличение Силы Голоса.", "claim_balance_hint": "Ваша доля от эмиссии токенов блокчейна, которую вы можете получить для увеличения Силы Голоса, пополнения TIP-баланса...", @@ -1009,7 +1046,6 @@ "title": "Правила вывода", "to": "Отправить токены на адрес/аккаунт: ", "min_amount": "Минимальная сумма: ", - "fee": "Комиссия: ", "details": "Дополнительно: ", "unavailable": "Шлюз на техобслуживании", "way_name": "Название сети: ", diff --git a/app/redux/User.js b/app/redux/User.js index 52942e990..ee14a1096 100644 --- a/app/redux/User.js +++ b/app/redux/User.js @@ -7,6 +7,7 @@ const defaultState = fromJS({ current: null, show_login_modal: false, show_transfer_modal: false, + show_convert_assets_modal: false, show_promote_post_modal: false, show_signup_modal: false, show_open_orders_modal: false, @@ -82,6 +83,9 @@ export default createModule({ }, { action: 'SHOW_TRANSFER', reducer: state => state.set('show_transfer_modal', true) }, { action: 'HIDE_TRANSFER', reducer: state => state.set('show_transfer_modal', false) }, + { action: 'SHOW_CONVERT_ASSETS', reducer: state => state.set('show_convert_assets_modal', true) }, + { action: 'HIDE_CONVERT_ASSETS', reducer: state => state.set('show_convert_assets_modal', false) }, + { action: 'SET_CONVERT_ASSETS_DEFAULTS', reducer: (state, {payload}) => state.set('convert_assets_defaults', fromJS(payload)) }, { action: 'SHOW_POWERDOWN', reducer: state => state.set('show_powerdown_modal', true) }, { action: 'HIDE_POWERDOWN', reducer: state => state.set('show_powerdown_modal', false) }, { action: 'SET_POWERDOWN_DEFAULTS', reducer: (state, {payload}) => state.set('powerdown_defaults', fromJS(payload)) }, diff --git a/app/utils/ApidexApiClient.js b/app/utils/ApidexApiClient.js new file mode 100644 index 000000000..21ba37347 --- /dev/null +++ b/app/utils/ApidexApiClient.js @@ -0,0 +1,62 @@ +import fetchWithTimeout from 'shared/fetchWithTimeout' + +const request_base = { + method: 'get', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } +} + +const pageBaseURL = 'https://coinmarketcap.com/currencies/' + +const getPageURL = (slug) => { + return new URL(slug + '/', pageBaseURL).toString() +} + +const apidexAvailable = () => { + return process.env.BROWSER && typeof($STM_Config) !== 'undefined' + && $STM_Config.apidex_service && $STM_Config.apidex_service.host +} + +export const apidexUrl = (pathname) => { + try { + return new URL(pathname, $STM_Config.apidex_service.host).toString(); + } catch (err) { + console.error('apidexUrl', err) + return '' + } +} + +let cached = {} + +export async function apidexGetPrices(sym) { + const empty = { + price_usd: null, + price_rub: null, + page_url: null + } + if (!apidexAvailable()) return empty + let request = Object.assign({}, request_base) + try { + const now = new Date() + const cache = cached[sym] + if (cache && (now - cache.time) < 60000) { + return cache.resp + } else { + let resp = await fetchWithTimeout(apidexUrl(`/api/v1/cmc/${sym}`), 2000, request) + resp = await resp.json() + if (resp.data && resp.data.slug) + resp['page_url'] = getPageURL(resp.data.slug) + else + resp['page_url'] = null + cached[sym] = { + resp, time: now + } + return resp + } + } catch (err) { + console.error('apidexGetPrices', err) + return empty + } +} diff --git a/app/utils/MarketUtils.js b/app/utils/MarketUtils.js deleted file mode 100644 index db0656689..000000000 --- a/app/utils/MarketUtils.js +++ /dev/null @@ -1,24 +0,0 @@ -function roundUp(num, precision) { - let satoshis = parseFloat(num) * Math.pow(10, precision); - - // Attempt to correct floating point: 1.0001 satoshis should not round up. - satoshis = satoshis - 0.0001; - - // Round up, restore precision - return Math.ceil(satoshis) / Math.pow(10, precision); -} - -function roundDown(num, precision) { - let satoshis = parseFloat(num) * Math.pow(10, precision); - - // Attempt to correct floating point: 1.9999 satoshis should not round down. - satoshis = satoshis + 0.0001; - - // Round down, restore precision - return Math.floor(satoshis) / Math.pow(10, precision); -} - -module.exports = { - roundUp, - roundDown -} diff --git a/app/utils/MarketClasses.js b/app/utils/market/Order.js similarity index 59% rename from app/utils/MarketClasses.js rename to app/utils/market/Order.js index 67e127129..f0e6d142f 100644 --- a/app/utils/MarketClasses.js +++ b/app/utils/market/Order.js @@ -1,4 +1,4 @@ -import {roundDown, roundUp} from "./MarketUtils"; +import { roundDown, roundUp } from 'app/utils/market/utils' class Order { constructor(order, side, sym1, sym2, prec1, prec2, currentSeller) { @@ -90,70 +90,4 @@ class Order { } } -class TradeHistory { - - constructor(fill, sym1, sym2, prec1, prec2) { - this.fill =fill; - this.id = fill.id; - // Norm date (FF bug) - var zdate = fill.date; - if(!/Z$/.test(zdate)) - zdate = zdate + 'Z' - - this.date = new Date(zdate); - this.type = fill.current_pays.indexOf(sym2) !== -1 ? "bid" : "ask"; - this.color = this.type == "bid" ? "buy-color" : "sell-color"; - if (this.type === "bid") { - this.asset2 = parseFloat(fill.current_pays.split(" " + sym2)[0]); - this.asset1 = parseFloat(fill.open_pays.split(" " + sym1)[0]); - } else { - this.asset2 = parseFloat(fill.open_pays.split(" " + sym2)[0]); - this.asset1 = parseFloat(fill.current_pays.split(" " + sym1)[0]); - } - - this.sym1 = sym1 - this.sym2 = sym2 - this.prec1 = prec1 - this.prec2 = prec2 - this.price = this.asset2 / this.asset1; - this.price = this.type === 'ask' ? roundUp(this.price, 8) : Math.max(roundDown(this.price, 8), 0.00000001); - this.stringPrice = this.price.toFixed(prec2); - } - - getAsset1Amount() { - return this.asset1; - } - - getStringAsset1() { - return this.getAsset1Amount().toFixed(this.prec1); - } - - getAsset2Amount() { - return this.asset2; - } - - getStringAsset2() { - return this.getAsset2Amount().toFixed(this.prec2); - } - - getPrice() { - return this.price; - } - - getStringPrice() { - return this.stringPrice; - } - - equals(order) { - return ( - this.getStringAsset2() === order.getStringAsset2() && - this.getStringAsset1() === order.getStringAsset1() && - this.getStringPrice() === order.getStringPrice() - ); - } -} - -module.exports = { - Order, - TradeHistory -} +module.exports = Order diff --git a/app/utils/market/TradeHistory.js b/app/utils/market/TradeHistory.js new file mode 100644 index 000000000..3ead35b35 --- /dev/null +++ b/app/utils/market/TradeHistory.js @@ -0,0 +1,65 @@ +import { roundDown, roundUp } from 'app/utils/market/utils' + +class TradeHistory { + constructor(fill, sym1, sym2, prec1, prec2) { + this.fill =fill; + this.id = fill.id; + // Norm date (FF bug) + var zdate = fill.date; + if(!/Z$/.test(zdate)) + zdate = zdate + 'Z' + + this.date = new Date(zdate); + this.type = fill.current_pays.indexOf(sym2) !== -1 ? "bid" : "ask"; + this.color = this.type == "bid" ? "buy-color" : "sell-color"; + if (this.type === "bid") { + this.asset2 = parseFloat(fill.current_pays.split(" " + sym2)[0]); + this.asset1 = parseFloat(fill.open_pays.split(" " + sym1)[0]); + } else { + this.asset2 = parseFloat(fill.open_pays.split(" " + sym2)[0]); + this.asset1 = parseFloat(fill.current_pays.split(" " + sym1)[0]); + } + + this.sym1 = sym1 + this.sym2 = sym2 + this.prec1 = prec1 + this.prec2 = prec2 + this.price = this.asset2 / this.asset1; + this.price = this.type === 'ask' ? roundUp(this.price, 8) : Math.max(roundDown(this.price, 8), 0.00000001); + this.stringPrice = this.price.toFixed(prec2); + } + + getAsset1Amount() { + return this.asset1; + } + + getStringAsset1() { + return this.getAsset1Amount().toFixed(this.prec1); + } + + getAsset2Amount() { + return this.asset2; + } + + getStringAsset2() { + return this.getAsset2Amount().toFixed(this.prec2); + } + + getPrice() { + return this.price; + } + + getStringPrice() { + return this.stringPrice; + } + + equals(order) { + return ( + this.getStringAsset2() === order.getStringAsset2() && + this.getStringAsset1() === order.getStringAsset1() && + this.getStringPrice() === order.getStringPrice() + ); + } +} + +module.exports = TradeHistory diff --git a/app/utils/market/utils.js b/app/utils/market/utils.js new file mode 100644 index 000000000..64fdabb70 --- /dev/null +++ b/app/utils/market/utils.js @@ -0,0 +1,124 @@ +function roundUp(num, precision) { + let satoshis = parseFloat(num) * Math.pow(10, precision); + + // Attempt to correct floating point: 1.0001 satoshis should not round up. + satoshis = satoshis - 0.0001; + + // Round up, restore precision + return Math.ceil(satoshis) / Math.pow(10, precision); +} + +function roundDown(num, precision) { + let satoshis = parseFloat(num) * Math.pow(10, precision); + + // Attempt to correct floating point: 1.9999 satoshis should not round down. + satoshis = satoshis + 0.0001; + + // Round down, restore precision + return Math.floor(satoshis) / Math.pow(10, precision); +} + +function normalizeAssets(assets) { + let assetsNorm = {} + assetsNorm['GOLOS'] = { + supply: '0.000 GOLOS', precision: 3, symbols_whitelist: [], fee_percent: 0, + json_metadata: '{"image_url": "/images/golos.png"}' + } + assetsNorm['GBG'] = { + supply: '0.000 GBG', precision: 3, symbols_whitelist: [], fee_percent: 0, + json_metadata: '{"image_url": "/images/gold-golos.png"}' + } + for (let [key, value] of Object.entries(assets)) { + assetsNorm[key] = value + } + return assetsNorm +} + +function getAssetMeta(asset) { + try { + let obj = JSON.parse(asset.json_metadata) + if (typeof(obj) !== 'object' || !obj || Array.isArray(obj)) { + return {} + } + return obj + } catch (err) { + return {} + } +} + +function getTradablesFor(assets, syms, onlyFirst = false) { + let tradableLists = syms.map(sym => []) + + const forbids = (whitelist, sym) => { + return whitelist.length && !whitelist.includes(sym) + } + + for (let [key, value] of Object.entries(assets)) { + let image_url = getAssetMeta(value).image_url || '' + + if (syms.includes(key)) { + continue + } + + for (const i in syms) { + const sym = syms[i] + + if (forbids(value.symbols_whitelist, sym)) { + continue + } + + if (forbids(assets[sym].symbols_whitelist, key)) { + continue + } + + tradableLists[i].push({ + symbol: key, image_url + }) + + if (onlyFirst) { + return tradableLists + } + } + } + + return tradableLists +} + +function generateOrderID() { + return Math.floor(Date.now() / 1000) +} + +const calcFeeForBuy = (buyAmount, fee_percent) => { + let fee = buyAmount.clone() + let buyWF = buyAmount.mul(10000).div(10000 - fee_percent) + if (buyWF.amount > buyAmount.amount) { + fee = buyWF.minus(buyAmount) + } else if (fee_percent) { + buyWF = buyWF.plus(1) + fee.amount = 1 + } else { + fee.amount = 0 + } + return { fee, buyWF } +} + +const calcFeeForSell = (buyAmount, fee_percent) => { + let fee = buyAmount.mul(fee_percent).div(10000) + if (fee.amount === 0 && fee_percent !== 0) fee.amount = 1 + let clearBuy = buyAmount.minus(fee) + return { fee, clearBuy } +} + +const DEFAULT_EXPIRE = 0xffffffff + +module.exports = { + roundUp, + roundDown, + normalizeAssets, + getAssetMeta, + getTradablesFor, + generateOrderID, + calcFeeForBuy, + calcFeeForSell, + DEFAULT_EXPIRE +} diff --git a/config/default.json b/config/default.json index 77dd07f3e..09e7e9b2d 100644 --- a/config/default.json +++ b/config/default.json @@ -48,6 +48,13 @@ "login": "golosclient", "password": "golosclient" }, + "apidex_service": { + "host": "https://devapi-dex.golos.app" + }, + "hidden_assets": { + "RUDEX": true, + "PRIZM": true + }, "forums": { "white_list": ["fm-golostalk", "fm-prizmtalk", "fm-graphenetalks"], "fm-golostalk": {"domain": "golostalk.com"}, diff --git a/package.json b/package.json index 07400f4e2..e208deb16 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "foundation-sites": "^6.4.3", "fs-extra": "^10.0.1", "git-rev-sync": "^1.12.0", - "golos-lib-js": "^0.9.25", + "golos-lib-js": "^0.9.31", "history": "^2.0.0-rc2", "immutable": "^3.8.2", "intl": "^1.2.5", diff --git a/server/index.js b/server/index.js index 7fa1fff4d..f69387dd5 100755 --- a/server/index.js +++ b/server/index.js @@ -25,6 +25,8 @@ global.$STM_Config = { auth_service: config.get('auth_service'), notify_service: config.get('notify_service'), messenger_service: config.get('messenger_service'), + apidex_service: config.get('apidex_service'), + hidden_assets: config.get('hidden_assets'), forums: config.get('forums'), gamefication: config.get('gamefication'), blocked_users: config.get('blocked_users'), diff --git a/yarn.lock b/yarn.lock index f9927707a..3fa44c635 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5861,7 +5861,7 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" -es-abstract@^1.17.2, es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1: +es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== @@ -5909,6 +5909,32 @@ es-abstract@^1.17.4, es-abstract@^1.18.0, es-abstract@^1.18.0-next.1, es-abstrac string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-abstract@^1.18.5: + version "1.19.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.2.tgz#8f7b696d8f15b167ae3640b4060670f3d054143f" + integrity sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0: version "1.12.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" @@ -7172,10 +7198,10 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" -golos-lib-js@^0.9.25: - version "0.9.25" - resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.25.tgz#352c12b829fa42e4dcd6ca3adaa564ba94b6d09e" - integrity sha512-yuxDQQ09UXDf2lpkH1Yzqq1V/poBgFpeVGA/TQ61ZorUtCoLOSISfn0LJm3fxCtPltE515DwQv3/4pjsFuAUPw== +golos-lib-js@^0.9.31: + version "0.9.31" + resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.31.tgz#785b28d3ab73523de4c3d1d10b28cc177e8bde91" + integrity sha512-pRgGNtqsuRfQCPjMqAq+TUsno4mbHm7RFMvF/IzhR4AAAPZCK9/izHama7BhJvJubed7pm06fKO6/2bv0zuA8A== dependencies: assert "^2.0.0" bigi "^1.4.2" @@ -7304,6 +7330,11 @@ has-symbols@^1.0.1, has-symbols@^1.0.2: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -8293,6 +8324,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -8491,6 +8527,13 @@ is-weakref@^1.0.1: dependencies: call-bind "^1.0.0" +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-what@^3.3.1: version "3.14.1" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" @@ -10453,6 +10496,11 @@ object-inspect@^1.11.0, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== +object-inspect@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" From be6b572c32ed74bacdc0a5d590021552351a29fc Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Wed, 27 Apr 2022 02:32:45 +0000 Subject: [PATCH 4/9] Market bug fix and refactoring, update react-redux --- app/components/all.scss | 1 + app/components/elements/market/CMCValue.jsx | 9 +- .../elements/market/MarketInput.jsx | 26 + app/components/elements/market/MarketPair.jsx | 5 +- .../elements/market/MarketPair.scss | 6 +- app/components/elements/market/OrderForm.jsx | 408 ++++++++ app/components/elements/market/OrderForm.scss | 34 + .../elements/market/TickerPriceStat.jsx | 6 +- app/components/modules/ConvertAssets.jsx | 2 +- app/components/pages/Market.jsx | 977 +----------------- app/components/pages/Market.scss | 26 - app/utils/market/Order.js | 2 +- app/utils/market/TradeHistory.js | 2 +- package.json | 2 +- yarn.lock | 53 +- 15 files changed, 559 insertions(+), 1000 deletions(-) create mode 100644 app/components/elements/market/MarketInput.jsx create mode 100644 app/components/elements/market/OrderForm.jsx create mode 100644 app/components/elements/market/OrderForm.scss diff --git a/app/components/all.scss b/app/components/all.scss index 23d521b64..4232f911e 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -47,6 +47,7 @@ @import "./elements/common/TooltipManager/index"; @import "./elements/market/ConvertAssetsBtn"; @import "./elements/market/MarketPair"; +@import "./elements/market/OrderForm"; @import "./elements/market/TickerPriceStat"; @import "./elements/postEditor/MarkdownEditor/MarkdownEditor"; @import "./elements/postEditor/MarkdownEditorToolbar/index"; diff --git a/app/components/elements/market/CMCValue.jsx b/app/components/elements/market/CMCValue.jsx index 6063af14c..273f70341 100644 --- a/app/components/elements/market/CMCValue.jsx +++ b/app/components/elements/market/CMCValue.jsx @@ -39,12 +39,17 @@ class CMCValue extends React.Component { } render() { - const { renderer } = this.props + const { renderer, compact } = this.props let cmc = null const { cmcPrice } = this.state if (cmcPrice) { const formatVal = (val, fmt) => { - return (val && val.toFixed) ? fmt(val.toFixed(2)) : null + if (val && val.toFixed) { + if (val > 1000000) return fmt((val / 1000000).toFixed(3) + 'M') + if (compact && val > 100) return fmt(Math.round(val)) + return fmt(val.toFixed(2)) + } + return null } let mainVal, altVal const price_usd = formatVal(cmcPrice.price_usd, v => `$${v}`) diff --git a/app/components/elements/market/MarketInput.jsx b/app/components/elements/market/MarketInput.jsx new file mode 100644 index 000000000..b697bec70 --- /dev/null +++ b/app/components/elements/market/MarketInput.jsx @@ -0,0 +1,26 @@ +import React from 'react' + +class MarketInput extends React.Component { + render() { + const { label, symbol, rowTitle, ...rest } = this.props + return (
+
+ +
+
+
+ + + {symbol} + +
+
+
) + } +} + +export default MarketInput diff --git a/app/components/elements/market/MarketPair.jsx b/app/components/elements/market/MarketPair.jsx index 1b718b916..c970fbfbe 100644 --- a/app/components/elements/market/MarketPair.jsx +++ b/app/components/elements/market/MarketPair.jsx @@ -11,7 +11,6 @@ class MarketPair extends React.Component { assets: PropTypes.object.isRequired, sym1: PropTypes.string, sym2: PropTypes.string, - compactList: PropTypes.bool, slim: PropTypes.bool, itemsPerPage: PropTypes.number, linkComposer: PropTypes.func, @@ -79,7 +78,7 @@ class MarketPair extends React.Component { } render() { - const { assets, slim, compactList, itemsPerPage, linkComposer, label1, label2 } = this.props + const { assets, slim, itemsPerPage, linkComposer, label1, label2 } = this.props const { sym1, sym2, symbols1, symbols2 } = this.state if (!symbols1 && !symbols2) return
@@ -134,7 +133,7 @@ class MarketPair extends React.Component {
- return (
+ return (
{left}   li>a { - padding: .3rem 1rem; + .VerticalMenu>li>a { + padding: .3rem 1rem; + } } .MarketPair__label { diff --git a/app/components/elements/market/OrderForm.jsx b/app/components/elements/market/OrderForm.jsx new file mode 100644 index 000000000..ce38fef30 --- /dev/null +++ b/app/components/elements/market/OrderForm.jsx @@ -0,0 +1,408 @@ +import React from 'react' +import PropTypes from 'prop-types' +import tt from 'counterpart' +import { connect } from 'react-redux' +import { Asset, AssetEditor } from 'golos-lib-js/lib/utils' + +import transaction from 'app/redux/Transaction'; +import CMCValue from 'app/components/elements/market/CMCValue' +import MarketInput from 'app/components/elements/market/MarketInput' +import { DEFAULT_EXPIRE, generateOrderID, roundUp, roundDown } from 'app/utils/market/utils' + +class OrderForm extends React.Component { + static propTypes = { + placeOrder: PropTypes.func.isRequired, + onCreate: PropTypes.func.isRequired, + }; + + state = { + price: AssetEditor(0, 8, 'PRICE'), + amount: AssetEditor(0, 3, 'GOLOS'), + total: AssetEditor(0, 3, 'GBG'), + fee: Asset(0, 3, 'GBG'), + feePct: Asset(0, 2, 'PCT'), + submitDisabled: true + } + + initFields = (prevProps) => { + const { sym1, sym2, assets, isSell } = this.props + if (sym1 !== prevProps.sym1 || sym2 !== prevProps.sym2 || + assets && !prevProps.assets) { + let feeSym = isSell ? sym2 : sym1 + this.setState({ + amount: AssetEditor(0, assets[sym1].precision, sym1), + total: AssetEditor(0, assets[sym2].precision, sym2), + fee: Asset(0, assets[feeSym].precision, feeSym), + feePct: Asset(assets[feeSym].fee_percent, 2, 'PCT') + }) + } + } + + componentDidMount() { + this.initFields({}) + } + + componentDidUpdate(prevProps) { + this.initFields(prevProps) + } + + updateTotal = (amount, price) => { + const priceFloat = parseFloat(price.amountFloat) + const totalFloat = parseFloat(amount.amountFloat) * priceFloat + + let total = this.state.total.asset.clone() + total.amountFloat = totalFloat.toString() + if (this.props.isSell) { + if (parseFloat(total.amountFloat) / parseFloat(amount.amountFloat) + > priceFloat) { + total = total.minus(1) + } + const integral = parseFloat(amount.amountFloat).toFixed(total.precision).length - 1 + if (integral > 8) { + const nums = 10 ** (integral - 8) + total.amount = Math.floor(total.amount / nums) * nums + } + } else { + if (parseFloat(total.amountFloat) / parseFloat(amount.amountFloat) + < priceFloat) { + total = total.plus(1) + } + const integral = parseFloat(amount.amountFloat).toFixed(total.precision).length - 1 + if (integral > 8) { + const nums = 10 ** (integral - 8) + total.amount = Math.ceil(total.amount / nums) * nums + } + } + return total + } + + onPriceChange = e => { + const price = this.state.price.withChange(e.target.value) + if (price.hasChange && price.asset.amount >= 0) { + let total = this.updateTotal(this.state.amount.asset, price.asset) + this.setState({ + price, + total: AssetEditor(total) + }, () => { + this.validate() + }) + } + } + + onAmountChange = e => { + const amount = this.state.amount.withChange(e.target.value) + if (amount.hasChange && amount.asset.amount >= 0) { + const { price } = this.state + + let total = this.updateTotal(amount.asset, price.asset) + this.setState({ + amount, + total: AssetEditor(total) + }, () => { + this.validate() + }) + } + } + + updateAmount = (total, price ) => { + const priceFloat = parseFloat(price.amountFloat) + const amountFloat = parseFloat(total.amountFloat) + / priceFloat + + let amount = this.state.amount.asset.clone() + amount.amountFloat = amountFloat.toString() + + if (this.props.isSell) { + if (parseFloat(total.amountFloat) / parseFloat(amount.amountFloat) + > priceFloat) { + amount = amount.plus(1) + } + } else { + if (parseFloat(total.amountFloat) / parseFloat(amount.amountFloat) + < priceFloat) { + amount = amount.minus(1) + } + } + return amount + } + + onTotalChange = e => { + const total = this.state.total.withChange(e.target.value) + if (total.hasChange && total.asset.amount >= 0) { + const { price } = this.state + const amount = this.updateAmount(total.asset, price.asset) + this.setState({ + total, + amount: AssetEditor(amount) + }, () => { + this.validate() + }) + } + } + + setPrice = priceFloat => { + let price = this.state.price.asset.clone() + price.amountFloat = priceFloat.toString() + let total = this.updateTotal(this.state.amount.asset, price) + this.setState({ + price: AssetEditor(price), + total: AssetEditor(total) + }, () => { + this.validate() + }) + } + + onBestPriceClick = e => { + e.preventDefault(); + const { bestPrice } = this.props + this.setPrice(bestPrice) + } + + _getBalance = () => { + const { account, assets, sym1, sym2, isSell } = this.props + let balance + const balSym = isSell ? sym1 : sym2 + if (account && assets && balSym in assets) { + if (balSym === 'GOLOS') { + balance = account.balance + } else if (balSym === 'GBG') { + balance = account.sbd_balance + } else { + balance = assets[balSym].balance + } + } + return balance + } + + onBalanceClick = e => { + e.preventDefault() + const { isSell } = this.props + let balance = this._getBalance() + balance = Asset(balance) + let amount, total + if (isSell) { + amount = balance.clone() + total = this.updateTotal(amount, this.state.price.asset) + } else { + total = balance.clone() + amount = this.updateAmount(total, this.state.price.asset) + } + this.setState({ + amount: AssetEditor(amount), + total: AssetEditor(total) + }, () => { + this.validate() + }) + } + + validate = () => { + const { isSell } = this.props + const { price, amount, total } = this.state + let valid = price.asset.amount > 0 && amount.asset.amount > 0 && total.asset.amount > 0 + const balance = this._getBalance() + if (balance) { + const suff = (isSell ? amount.asset : total.asset).lte(Asset(balance)) + if (!suff) { + this.setState({ + submitDisabled: true, + insufficient: true + }) + return + } + } + + const { sym1, sym2, assets} = this.props + let feeSym = isSell ? sym2 : sym1 + const fee = (isSell ? total.asset : amount.asset).mul(assets[feeSym].fee_percent).div(10000) + this.setState({ + submitDisabled: !valid, + insufficient: false, + priceWarning: valid && (isSell ? + this.percentDiff(price.asset) < -15 : + this.percentDiff(price.asset) > 15), + fee + }) + } + + percentDiff = (userPrice) => { + let bestPrice = parseFloat(this.props.bestPrice) + let up = parseFloat(userPrice.amountFloat) + return (100 * (up - bestPrice)) / bestPrice + } + + onSubmit = (e) => { + e.preventDefault() + const { placeOrder, account, isSell, bestPrice, + onCreate } = this.props + const { amount, total, price, priceWarning } = this.state + + let amountToSell = isSell ? amount.asset.clone() : total.asset.clone() + let minToReceive = isSell ? total.asset.clone() : amount.asset.clone() + placeOrder( + account.name, + isSell, + amountToSell, + minToReceive, + price.asset, + !!priceWarning, + bestPrice, + msg => { + onCreate(msg) + } + ) + } + + render() { + const { sym1, sym2, account, assets, bestPrice, isSell } = this.props + const { priceWarning, submitDisabled, + price, amount, total, fee, feePct, insufficient } = this.state + + const balance = this._getBalance() + + return (
+ + + + + + {tt('market_jsx.total')} + + + + } + className={"input-group-field" + + ((insufficient && !isSell) ? ' balance_warning' : '')} + type="text" + placeholder="0.0" + value={total.amountStr} + onChange={this.onTotalChange} + symbol={sym2} /> + + + +
+ + ) + } +} + +export default connect( + null, + dispatch => ({ + placeOrder: ( + owner, + isSell, + amount_to_sell, + min_to_receive, + effective_price, + priceWarning, + marketPrice, + successCallback, + fill_or_kill = false, + expiration = DEFAULT_EXPIRE + ) => { + let effectivePrice = effective_price.amountFloat + ' ' + + let confirmStr + if (isSell) { + effectivePrice += min_to_receive.symbol + '/' + amount_to_sell.symbol + confirmStr = tt('market_jsx.sell_amount_for_atleast', { + amount_to_sell, min_to_receive, effectivePrice + }) + } else { + effectivePrice += amount_to_sell.symbol + '/' + min_to_receive.symbol + confirmStr = tt('market_jsx.buy_atleast_amount_for', { + amount_to_sell, min_to_receive, effectivePrice + }) + } + + const successMessage = tt('g.order_placed') + ': ' + confirmStr; + const confirm = confirmStr + '?'; + const warning = priceWarning + ? tt('market_jsx.price_warning_' + (isSell ? 'below' : 'above'), + { + marketPrice: + parseFloat(marketPrice).toFixed(8) + ' ' + + (isSell ? min_to_receive.symbol : amount_to_sell.symbol) + + '/' + + (isSell ? amount_to_sell.symbol : min_to_receive.symbol) + }) + : null; + + const orderid = generateOrderID() + + const operation = { + owner, + amount_to_sell, + min_to_receive, + fill_or_kill, + expiration, + orderid, + } + + dispatch( + transaction.actions.broadcastOperation({ + type: 'limit_order_create', + operation, + confirm, + warning, + successCallback: () => { + successCallback(successMessage); + }, + }) + ); + } + }), null, { + forwardRef: true + } +)(OrderForm) diff --git a/app/components/elements/market/OrderForm.scss b/app/components/elements/market/OrderForm.scss new file mode 100644 index 000000000..cee560668 --- /dev/null +++ b/app/components/elements/market/OrderForm.scss @@ -0,0 +1,34 @@ +.Market__orderform { + .Market__orderform-cmc { + float: right; + margin-top: 2px; + } + + margin-bottom: 1rem; + line-height: 1; + + .input-group-label { + min-width: 6rem; + font-size: 83%; + } + + input[type='text'] { + display: inline-block; + width: auto; + height: auto; + padding: 0.1rem 0.5rem 0.1rem 0.25rem; + text-align: right; + } + + input.price_warning { + background: rgb(252,84,78); + } + + input.balance_warning { + color: rgb(252,84,78); + } +} + +.Market__balance { + margin-left: -1.5rem; +} diff --git a/app/components/elements/market/TickerPriceStat.jsx b/app/components/elements/market/TickerPriceStat.jsx index 0d9371d90..e7633dcfd 100644 --- a/app/components/elements/market/TickerPriceStat.jsx +++ b/app/components/elements/market/TickerPriceStat.jsx @@ -31,7 +31,7 @@ export default class TickerPriceStat extends React.Component {
{tt('market_jsx.last_price')} - {symbol} {latest.toFixed(precision)} ({pct_change}) + {symbol} {latest.toFixed(8)} ({pct_change})
{tt('market_jsx.24h_volume')} @@ -39,11 +39,11 @@ export default class TickerPriceStat extends React.Component {
{tt('g.bid')} - {symbol} {ticker.highest_bid.toFixed(precision)} + {symbol} {ticker.highest_bid.toFixed(8)}
{tt('g.ask')} - {symbol} {ticker.lowest_ask.toFixed(precision)} + {symbol} {ticker.lowest_ask.toFixed(8)}
{ticker.highest_bid > 0 &&
{tt('market_jsx.spread')} diff --git a/app/components/modules/ConvertAssets.jsx b/app/components/modules/ConvertAssets.jsx index 4c01b4e5f..47d2ae5fb 100644 --- a/app/components/modules/ConvertAssets.jsx +++ b/app/components/modules/ConvertAssets.jsx @@ -444,7 +444,7 @@ class ConvertAssets extends React.Component { return (

{tt('g.convert_assets')}

- { - e.preventDefault(); - - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - const { placeOrder, user } = this.props; - if (!user) return; - const amount_to_sell = parseFloat( - ReactDOM.findDOMNode(this.refs.buySteemTotal).value - ); - const min_to_receive = parseFloat( - ReactDOM.findDOMNode(this.refs.buySteemAmount).value - ); - const price = parseFloat( - ReactDOM.findDOMNode(this.refs.buySteemPrice).value - ); - const { lowest_ask } = this.props.ticker; - placeOrder( - (this.props.assets ? this.props.assets : {}), - sym1, sym2, - user, - `${amount_to_sell} ${sym2}`, - `${min_to_receive} ${sym1}`, - `${sym2} ${price}/${sym1}`, - !!this.state.buyPriceWarning, - lowest_ask, - msg => { - this.props.notify(msg); - this.props.reload(user, this.props.location.pathname); - } - ); - }; - sellSteem = e => { - e.preventDefault(); - - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - const { placeOrder, user } = this.props; - if (!user) { - return; - } - - const min_to_receive = parseFloat( - ReactDOM.findDOMNode(this.refs.sellSteem_total).value - ); - - const amount_to_sell = parseFloat( - ReactDOM.findDOMNode(this.refs.sellSteem_amount).value - ); - - const price = parseFloat( - ReactDOM.findDOMNode(this.refs.sellSteem_price).value - ); - - const { highest_bid } = this.props.ticker; - - placeOrder( - (this.props.assets ? this.props.assets : {}), - sym2, sym1, - user, - `${amount_to_sell} ${sym1}`, - `${min_to_receive} ${sym2}`, - `${sym2} ${price}/${sym1}`, - !!this.state.sellPriceWarning, - highest_bid, - msg => { - this.props.notify(msg); - this.props.reload(user, this.props.location.pathname); - } - ); - }; cancelOrderClick = (e, orderid) => { e.preventDefault(); const { cancelOrder, user } = this.props; @@ -260,166 +170,9 @@ class Market extends Component { }; setFormPrice = price => { - const p = parseFloat(price); - - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - this.refs.sellSteem_price.value = p.toFixed(assetsNorm[sym2].precision); - this.refs.buySteemPrice.value = p.toFixed(assetsNorm[sym2].precision); - - const samount = parseFloat(this.refs.sellSteem_amount.value); - if (samount >= 0) { - this.refs.sellSteem_total.value = roundDown(p * samount, assetsNorm[sym1].precision).toFixed(assetsNorm[sym2].precision);; - } - - const bamount = parseFloat(this.refs.buySteemAmount.value); - if (bamount >= 0) { - this.refs.buySteemTotal.value = roundDown(p * bamount, assetsNorm[sym2].precision).toFixed(assetsNorm[sym2].precision);; - } - - this.validateBuySteem(); - this.fixBuyTotal(); - this.validateSellSteem(); - this.fixSellTotal(); - }; - - percentDiff = (marketPrice, userPrice) => { - marketPrice = parseFloat(marketPrice); - return (100 * (userPrice - marketPrice)) / marketPrice; - }; - - validateBuySteem = () => { - const amount = parseFloat(this.refs.buySteemAmount.value); - const price = parseFloat(this.refs.buySteemPrice.value); - const total = parseFloat(this.refs.buySteemTotal.value); - - const valid = amount > 0 && price > 0 && total > 0; - let { lowest_ask } = this.props.ticker; - - this.setState({ - buyDisabled: !valid, - buyPriceWarning: valid && this.percentDiff(lowest_ask, price) > 15, - }, async () => { - if (valid) { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - this.refs.buySteemFee.value = (amount * assetsNorm[sym1].fee_percent / 10000).toFixed(assetsNorm[sym1].precision) - this.setState( { - buySteemFeePct: longToAsset(assetsNorm[sym1].fee_percent, '', 2) + '%' - }) - } - }); - }; - - validateSellSteem = () => { - const amount = parseFloat(this.refs.sellSteem_amount.value); - const price = parseFloat(this.refs.sellSteem_price.value); - const total = parseFloat(this.refs.sellSteem_total.value); - const valid = amount > 0 && price > 0 && total > 0; - let { highest_bid } = this.props.ticker; - - this.setState({ - sellDisabled: !valid, - sellPriceWarning: - valid && this.percentDiff(highest_bid, price) < -15, - }, async () => { - if (valid) { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - this.refs.sellSteem_fee.value = (total * assetsNorm[sym2].fee_percent / 10000).toFixed(assetsNorm[sym2].precision) - this.setState( { - sellSteemFeePct: longToAsset(assetsNorm[sym2].fee_percent, '', 2) + '%' - }) - } - }); - }; - - toFixedAccur = (str, decs) => { - let parts = str.split('.'); - return parts[0] + (parts[1] ? '.' + parts[1].substring(0, decs) : ''); - } - - fixBuyTotal = () => { - const amount = parseFloat(this.refs.buySteemAmount.value); - const price = parseFloat(this.refs.buySteemPrice.value); - let total = parseFloat(this.refs.buySteemTotal.value); - if (isNaN(total)) return; - - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - total = (total + parseFloat(1) / Math.pow(10, assetsNorm[sym2].precision)).toString(); - this.refs.buySteemTotal.value = this.toFixedAccur(total, assetsNorm[sym2].precision); - }; - - fixSellTotal = () => { - const amount = parseFloat(this.refs.sellSteem_amount.value); - const price = parseFloat(this.refs.sellSteem_price.value); - let total = parseFloat(this.refs.sellSteem_total.value); - if (isNaN(total)) return; - - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - total = (total + parseFloat(1) / Math.pow(10, assetsNorm[sym2].precision)).toString(); - this.refs.sellSteem_total.value = this.toFixedAccur(total, assetsNorm[sym2].precision); - }; - - fixBuyAmount = () => { - let amount = parseFloat(this.refs.buySteemAmount.value); - if (isNaN(amount)) return; - const price = parseFloat(this.refs.buySteemPrice.value); - let total = parseFloat(this.refs.buySteemTotal.value); - - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - amount = (total / price).toFixed(assetsNorm[sym1].precision); - this.refs.buySteemAmount.value = amount; - }; - - fixSellAmount = () => { - let amount = parseFloat(this.refs.sellSteem_amount.value); - if (isNaN(amount)) return; - const price = parseFloat(this.refs.sellSteem_price.value); - let total = parseFloat(this.refs.sellSteem_total.value); - - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - amount = (total / price).toFixed(assetsNorm[sym1].precision); - this.refs.sellSteem_amount.value = amount; + const p = parseFloat(price) + this.buyForm.current.setPrice(p) + this.sellForm.current.setPrice(p) }; render() { @@ -481,21 +234,8 @@ class Market extends Component { cancelOrderClick, cancelOrdersClick, setFormPrice, - validateBuySteem, - fixBuyTotal, - fixSellTotal, - fixBuyAmount, - fixSellAmount, - validateSellSteem, } = this; - const { - buyDisabled, - sellDisabled, - buyPriceWarning, - sellPriceWarning, - } = this.state; - let ticker = { latest1: 0, latest2: 0, @@ -659,7 +399,7 @@ class Market extends Component { {o.price.toFixed(assetsNorm[sym2].precision)} {o.asset1} - {o.asset2.replace('SBD', DEBT_TOKEN_SHORT)} + {o.asset2}    
({tt('market_jsx.market_depth_') + ': '}{ticker.asset2_depth + ' ' + sym2})
-
-
-
- -
-
-
- { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - const amount = parseFloat( - this.refs.buySteemAmount - .value - ); - let price = parseFloat( - this.refs.buySteemPrice - .value - ); - let new_price = price.toFixed(assetsNorm[sym2].precision); - if (new_price.length < price.toString().length) { - this.refs.buySteemPrice - .value = new_price; - price = parseFloat( - this.refs.buySteemPrice - .value - ); - } - if (amount >= 0 && price >= 0) - this.refs.buySteemTotal.value = roundDown( - price * amount, - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision); - validateBuySteem(); - fixBuyTotal(); - }} - /> - - {`${sym2}/${sym1}`} - -
-
-
- -
-
- -
-
-
- { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - const price = parseFloat( - this.refs.buySteemPrice - .value - ) - let amount = parseFloat( - this.refs.buySteemAmount - .value - ); - let new_amount = amount.toFixed(assetsNorm[sym1].precision); - if (new_amount.length < amount.toString().length) { - this.refs.buySteemAmount - .value = new_amount; - amount = parseFloat( - this.refs.buySteemAmount - .value - ); - } - if (price >= 0 && amount >= 0) { - let res = price * amount - this.refs.buySteemTotal.value = roundDown( - res, - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision) - } - validateBuySteem(); - fixBuyTotal(); - }} - /> - - {' '} - {sym1} - -
-
-
- -
-
- -
-
-
- { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - const price = parseFloat( - this.refs.buySteemPrice - .value - ); - let total = parseFloat( - this.refs.buySteemTotal - .value - ); - let new_total = total.toFixed(assetsNorm[sym2].precision); - if (new_total.length < total.toString().length) { - this.refs.buySteemTotal - .value = new_total; - total = parseFloat( - this.refs.buySteemTotal - .value - ); - } - if (total >= 0 && price >= 0) - this.refs.buySteemAmount.value = roundUp( - total / price, - assetsNorm[sym1].precision - ).toFixed(assetsNorm[sym1].precision);; - validateBuySteem(); - fixBuyAmount(); - }} - /> - - {sym2} - -
-
-
- -
-
- -
-
-
- - - {sym1} - -
-
-
- -
-
-
- - -
- {(((sym2 === "GBG" || sym2 === "GOLOS") && account) || (assets && sym2 in assets)) && ( - - { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - e.preventDefault(); - const price = parseFloat( - this.refs.buySteemPrice.value - ); - let total = ''; - if (sym2 === "GBG") { - total = account.sbd_balance.split( - ' ' - )[0]; - } - else if (sym2 === "GOLOS") { - total = account.balance.split( - ' ' - )[0]; - } - else { - total = assets[sym2].balance.split( - ' ' - )[0]; - } - this.refs.buySteemTotal.value = total; - if (price >= 0) { - let amount = roundDown( - parseFloat(total) / price, - assetsNorm[sym1].precision - ); - this.refs.buySteemAmount.value = amount.toFixed(assetsNorm[sym1].precision); - let res = price * amount - this.refs.buySteemTotal.value = roundDown( - res, - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision) - } - validateBuySteem(); - fixBuyTotal(); - }} - > - {tt('market_jsx.available')}: - {' '} - {sym2 === "GBG" && account.sbd_balance.replace( - 'GBG', - DEBT_TOKEN_SHORT - )} - {sym2 === "GOLOS" && account.balance.replace( - LIQUID_TICKER, - LIQUID_TOKEN_UPPERCASE - )} - {sym2 !== "GOLOS" && sym2 !== "GBG" && assets[sym2].balance} - - )} -
- - { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - e.preventDefault(); - const amount = parseFloat( - this.refs.buySteemAmount - .value - ); - const price = parseFloat( - ticker.lowest_ask - ); - this.refs.buySteemPrice.value = - ticker.lowest_ask.toFixed(assetsNorm[sym2].precision); - if (amount >= 0) - this.refs.buySteemTotal.value = roundDown( - amount * price, - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision); - validateBuySteem(); - }} - > - {tt('market_jsx.lowest_ask')}: - {' '} - {ticker.lowest_ask.toFixed(assetsNorm[sym2].precision)}
-
-
-
-
- + { + this.props.notify(msg); + this.props.reload(user, this.props.location.pathname); + }} />
@@ -1069,301 +516,12 @@ class Market extends Component { LIQUID_TOKEN: sym1 })}    
({tt('market_jsx.market_depth_') + ': '} {ticker.asset1_depth + ' ' + sym1})
- -
-
-
- -
- -
-
- { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - let amount = parseFloat( - this.refs.sellSteem_amount - .value - ); - let price = parseFloat( - this.refs.sellSteem_price - .value - ); - let new_price = price.toFixed(assetsNorm[sym2].precision); - if (new_price.length < price.toString().length) { - this.refs.sellSteem_price - .value = new_price; - price = parseFloat( - this.refs.sellSteem_price - .value - ); - } - if (amount >= 0 && price >= 0) - this.refs.sellSteem_total.value = roundDown( - price * amount, - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision); - validateSellSteem(); - fixSellTotal(); - }} - /> - - {`${sym2}/${sym1}`} - -
-
-
- -
-
- -
-
-
- { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - const price = parseFloat( - this.refs.sellSteem_price - .value - ); - let amount = parseFloat( - this.refs.sellSteem_amount - .value - ); - let new_amount = amount.toFixed(assetsNorm[sym1].precision); - if (new_amount.length < amount.toString().length) { - this.refs.sellSteem_amount - .value = new_amount; - amount = parseFloat( - this.refs.sellSteem_amount - .value - ); - } - if (price >= 0 && amount >= 0) - this.refs.sellSteem_total.value = roundDown( - price * amount, - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision); - validateSellSteem(); - fixSellTotal(); - }} - /> - - {sym1} - -
-
-
- -
-
- -
-
-
- { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - const price = parseFloat( - this.refs.sellSteem_price - .value - ); - let total = parseFloat( - this.refs.sellSteem_total - .value - ); - let new_total = total.toFixed(assetsNorm[sym2].precision); - if (new_total.length < total.toString().length) { - this.refs.sellSteem_total - .value = new_total; - total = parseFloat( - this.refs.sellSteem_total - .value - ); - } - if (price >= 0 && total >= 0) - this.refs.sellSteem_amount.value = roundUp( - total / price, - assetsNorm[sym1].precision - ).toFixed(assetsNorm[sym1].precision); - validateSellSteem(); - fixSellAmount(); - }} - /> - - {sym2} - -
-
-
- -
-
- -
-
-
- - - {sym2} - -
-
-
- -
-
-
- - -
- {(((sym1 === "GBG" || sym1 === "GOLOS") && account) || (assets && sym1 in assets)) && ( - - { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - e.preventDefault(); - const price = parseFloat( - this.refs.sellSteem_price.value - ); - let amount = ''; - if (sym1 === "GBG") { - amount = account.sbd_balance.split( - ' ' - )[0]; - } - else if (sym1 === "GOLOS") { - amount = account.balance.split( - ' ' - )[0]; - } - else { - amount = assets[sym1].balance.split( - ' ' - )[0]; - } - this.refs.sellSteem_amount.value = amount; - if (price >= 0) - this.refs.sellSteem_total.value = roundDown( - price * parseFloat(amount), - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision); - validateSellSteem(); - fixSellTotal(); - }} - > - {tt('market_jsx.available')}: - {' '} - {sym1 === "GBG" && account.sbd_balance.replace( - 'GBG', - DEBT_TOKEN_SHORT - )} - {sym1 === "GOLOS" && account.balance.replace( - LIQUID_TICKER, - LIQUID_TOKEN_UPPERCASE - )} - {sym1 !== "GOLOS" && sym1 !== "GBG" && assets[sym1].balance} - - )} -
- - { - let {sym1, sym2} = this.props.routeParams - sym1 = sym1.toUpperCase() - sym2 = sym2.toUpperCase() - - let assets = this.props.assets; - let assetsNorm = normalizeAssets(assets) - - e.preventDefault(); - const amount = parseFloat( - this.refs - .sellSteem_amount - .value - ); - const price = - ticker.highest_bid; - this.refs.sellSteem_price.value = price.toFixed(assetsNorm[sym2].precision); - if (amount >= 0) - this.refs.sellSteem_total.value = roundDown( - parseFloat(price) * - amount, - assetsNorm[sym2].precision - ).toFixed(assetsNorm[sym2].precision); - validateSellSteem(); - fixSellTotal(); - }} - > - {tt('market_jsx.highest_bid')}: - {' '} - {ticker.highest_bid.toFixed(assetsNorm[sym2].precision)}
-
-
-
-
- + { + this.props.notify(msg); + this.props.reload(user, this.props.location.pathname); + }} />
@@ -1546,80 +704,5 @@ export default connect( }) ); }, - placeOrder: ( - assets, - sym1, sym2, - owner, - amount_to_sell, - min_to_receive, - effectivePrice, - priceWarning, - marketPrice, - successCallback, - fill_or_kill = false, - expiration = DEFAULT_EXPIRE - ) => { - // create_order jsc 12345 "1.000 SBD" "100.000 STEEM" true 1467122240 false - - let assetsNorm = normalizeAssets(assets) - let prec1 = assetsNorm[sym1].precision - let prec2 = assetsNorm[sym2].precision - - // Padd amounts to 3 decimal places - amount_to_sell = amount_to_sell.replace( - amount_to_sell.split(' ')[0], - String(parseFloat(amount_to_sell).toFixed(prec2)) - ); - min_to_receive = min_to_receive.replace( - min_to_receive.split(' ')[0], - String(parseFloat(min_to_receive).toFixed(prec1)) - ); - - const isSell = amount_to_sell.indexOf(sym1) > 0; - const confirmStr = tt( - isSell - ? 'market_jsx.sell_amount_for_atleast' - : 'market_jsx.buy_atleast_amount_for', - { amount_to_sell, min_to_receive, effectivePrice } - ); - const successMessage = tt('g.order_placed') + ': ' + confirmStr; - const confirm = confirmStr + '?'; - const warning = priceWarning - ? tt( - 'market_jsx.price_warning_' + - (isSell ? 'below' : 'above'), - { - marketPrice: - sym2 + ' ' + - parseFloat(marketPrice).toFixed(assetsNorm[sym2].precision) + - '/' + - sym1, - } - ) - : null; - - const orderid = generateOrderID() - - const operation = { - owner, - amount_to_sell, - min_to_receive, - fill_or_kill, - expiration, - orderid, - } - - dispatch( - transaction.actions.broadcastOperation({ - type: 'limit_order_create', - operation, - confirm, - warning, - successCallback: () => { - successCallback(successMessage); - }, - }) - ); - }, }) )(Market); diff --git a/app/components/pages/Market.scss b/app/components/pages/Market.scss index 3053c3862..70b228fa6 100644 --- a/app/components/pages/Market.scss +++ b/app/components/pages/Market.scss @@ -75,32 +75,6 @@ input.sell-color:hover { } } -.Market__orderform { - margin-bottom: 1rem; - line-height: 1; - - .input-group-label { - min-width: 6rem; - font-size: 83%; - } - - input[type='text'] { - display: inline-block; - width: auto; - height: auto; - padding: 0.1rem 0.5rem 0.1rem 0.25rem; - text-align: right; - } - - input.price_warning { - background: rgb(252,84,78); - } -} - -.Market__balance { - margin-left: -1.5rem; -} - .Market { &__ticker-pct-up { color: #080; diff --git a/app/utils/market/Order.js b/app/utils/market/Order.js index f0e6d142f..ceb6c0ea7 100644 --- a/app/utils/market/Order.js +++ b/app/utils/market/Order.js @@ -5,7 +5,7 @@ class Order { this.side = side; this.price = parseFloat(order.real_price); this.price = side === 'asks' ? roundUp(this.price, 8) : Math.max(roundDown(this.price, 8), 0.00000001); - this.stringPrice = this.price.toFixed(prec2); + this.stringPrice = this.price.toFixed(8); this.asset1 = parseInt(order.asset1, 10); this.asset2 = parseInt(order.asset2, 10); this.sym1 = sym1 diff --git a/app/utils/market/TradeHistory.js b/app/utils/market/TradeHistory.js index 3ead35b35..935a47b18 100644 --- a/app/utils/market/TradeHistory.js +++ b/app/utils/market/TradeHistory.js @@ -26,7 +26,7 @@ class TradeHistory { this.prec2 = prec2 this.price = this.asset2 / this.asset1; this.price = this.type === 'ask' ? roundUp(this.price, 8) : Math.max(roundDown(this.price, 8), 0.00000001); - this.stringPrice = this.price.toFixed(prec2); + this.stringPrice = this.price.toFixed(8); } getAsset1Amount() { diff --git a/package.json b/package.json index e208deb16..f8aab3c00 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "react-intl": "^2.4.0", "react-notification": "6.8.2", "react-portal": "^2.2.1", - "react-redux": "^5.1.2", + "react-redux": "^8.0.1", "react-router": "3.2.6", "react-router-redux": "^4.0.8", "react-router-scroll": "^0.4.4", diff --git a/yarn.lock b/yarn.lock index 3fa44c635..83512576f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1209,7 +1209,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== @@ -1223,6 +1223,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.1": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -2160,6 +2167,14 @@ dependencies: "@types/unist" "*" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^5.0.0": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57" @@ -2324,6 +2339,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/webpack-env@^1.16.0": version "1.16.3" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a" @@ -12251,6 +12271,11 @@ react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== + react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -12303,18 +12328,17 @@ react-portal@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-2.2.1.tgz#0cde8c35eeb0cce9a67b1e1255d5e4a2d147d1cf" -react-redux@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" - integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== +react-redux@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.1.tgz#2bc029f5ada9b443107914c373a2750f6bc0f40c" + integrity sha512-LMZMsPY4DYdZfLJgd7i79n5Kps5N9XVLCJJeWAaPYTV+Eah2zTuBjTxKtNEbjiyitbq80/eIkm55CYSLqAub3w== dependencies: - "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" - loose-envify "^1.1.0" - prop-types "^15.6.1" - react-is "^16.6.0" - react-lifecycles-compat "^3.0.0" + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" react-refresh@^0.8.3: version "0.8.3" @@ -14912,6 +14936,11 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.0.0" +use-sync-external-store@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82" + integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ== + use@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" From 959725185c5584e1066e2c2e5f605404c2c8159a Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Wed, 27 Apr 2022 06:42:51 +0000 Subject: [PATCH 5/9] CoinMarketCap widget --- app/components/all.scss | 1 + app/components/elements/market/CMCWidget.jsx | 87 +++++++++++++++++++ app/components/elements/market/CMCWidget.scss | 62 +++++++++++++ app/components/pages/PostsIndex.jsx | 24 +---- app/components/pages/PostsIndex.scss | 5 -- 5 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 app/components/elements/market/CMCWidget.jsx create mode 100644 app/components/elements/market/CMCWidget.scss diff --git a/app/components/all.scss b/app/components/all.scss index 4232f911e..3fc91cc29 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -45,6 +45,7 @@ @import "./elements/common/HintIcon/HintIcon"; @import "./elements/common/DialogManager/index"; @import "./elements/common/TooltipManager/index"; +@import "./elements/market/CMCWidget"; @import "./elements/market/ConvertAssetsBtn"; @import "./elements/market/MarketPair"; @import "./elements/market/OrderForm"; diff --git a/app/components/elements/market/CMCWidget.jsx b/app/components/elements/market/CMCWidget.jsx new file mode 100644 index 000000000..369176a15 --- /dev/null +++ b/app/components/elements/market/CMCWidget.jsx @@ -0,0 +1,87 @@ +import React from 'react' + +import Icon from 'app/components/elements/Icon' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' + + +import { apidexGetPrices } from 'app/utils/ApidexApiClient' + +class CMCWidget extends React.Component { + state = { + loaded: false + } + + getPriceChange = (res) => { + let price_change = null + try { + price_change = res.data.quote.RUB.percent_change_24h + } catch (err) {} + if (price_change) { + price_change = parseFloat(price_change) + if (isNaN(price_change)) { + price_change = null + } + } + return price_change + } + + async componentDidMount() { + let res = await apidexGetPrices('GOLOS') + if (res.price_rub) { + const price_change = this.getPriceChange(res) + this.setState({ + loaded: true, + price_usd: res.price_usd, + price_rub: res.price_rub, + page_url: res.page_url, + price_change, + }) + } else { + this.setState({ + failed: true + }) + } + } + + render() { + const { loaded, failed, price_usd, price_rub, page_url, price_change } = this.state + if (!loaded) { + return (
+
+
+ {!failed ? + : + null} +
+
+
) + } + return (
+
+
+
+ +
+
+ + Golos Blockchain +
+ + {price_rub ? price_rub.toFixed(6) : null} +  RUB + + {(price_change && price_change.toFixed) ? +
({price_change.toFixed(2)}%) +
: null} +
+ {price_usd ? price_usd.toFixed(6) + ' USD' : null} +
+
+
+
+
+
) + } +} + +export default CMCWidget diff --git a/app/components/elements/market/CMCWidget.scss b/app/components/elements/market/CMCWidget.scss new file mode 100644 index 000000000..dcc7ed19b --- /dev/null +++ b/app/components/elements/market/CMCWidget.scss @@ -0,0 +1,62 @@ +.CMCWidget { + margin-bottom: 10px; + width: 12rem; + + .CMCWidget__inner { + border: 2px solid #e1e5ea; + border-radius: 10px; + font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; + min-width: 285px; + min-height: 126px; + + .CMCWidget__inner2 { + display: flex; + padding: 12px 0px; + + .CMCWidget__icon-parent { + width: 33%; + display: flex; + justify-content: center; + align-items: center; + + .CMCWidget__icon { + width: 46px; + height: 46px; + } + } + + .CMCWidget__main-parent { + width:67%; + border: none; + text-align:left; + line-height:1.4; + + .CMCWidget__link { + text-decoration: none; + color: rgb(16, 112, 224); + } + + .CMCWidget__main-val { + font-size: 20px; + font-weight: 500; + } + + .CMCWidget__main-cur { + font-size: 14px; + font-weight: 500; + } + + .CMCWidget__sub-parent { + margin-left:6px; + font-weight: 500; + + .CMCWidget__sub { + font-size: 14px; + color: rgba(39, 52, 64, 0.5); + } + } + } + } + } +} + diff --git a/app/components/pages/PostsIndex.jsx b/app/components/pages/PostsIndex.jsx index 6d9386d34..ee1f91fd3 100644 --- a/app/components/pages/PostsIndex.jsx +++ b/app/components/pages/PostsIndex.jsx @@ -11,7 +11,8 @@ import {Link} from 'react-router'; import MarkNotificationRead from 'app/components/elements/MarkNotificationRead'; import tt from 'counterpart'; import Immutable from "immutable"; -import Callout from 'app/components/elements/Callout'; +import Callout from 'app/components/elements/Callout' +import CMCWidget from 'app/components/elements/market/CMCWidget' import { APP_NAME, SELECT_TAGS_KEY } from 'app/client_config'; import cookie from "react-cookie"; import transaction from 'app/redux/Transaction' @@ -48,15 +49,6 @@ class PostsIndex extends React.Component { } } - componentDidMount () { - const script = document.createElement("script"); - - script.src = "https://files.coinmarketcap.com/static/widget/currency.js"; - script.async = true; - - document.body.appendChild(script); - } - getPosts(order, category) { let select_tags = cookie.load(SELECT_TAGS_KEY); select_tags = typeof select_tags === 'object' ? select_tags.sort().join('/') : ''; @@ -212,17 +204,7 @@ class PostsIndex extends React.Component {
-
-
+ Date: Wed, 27 Apr 2022 07:25:56 +0000 Subject: [PATCH 6/9] Desktop - fix updating reliability --- app/appUpdater.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/appUpdater.js b/app/appUpdater.js index 57f61505f..7a994b357 100644 --- a/app/appUpdater.js +++ b/app/appUpdater.js @@ -1,12 +1,14 @@ import semver from 'semver' import tt from 'counterpart' +import fetchWithTimeout from 'shared/fetchWithTimeout' + export async function checkUpdates() { const url = new URL( '/blogs-' + ($STM_Config.platform === 'linux' ? 'linux' : 'win'), $STM_Config.app_updater.host ).toString() - let res = await fetch(url) + let res = await fetchWithTimeout(url, 3000) res = await res.text() const doc = document.createElement('html') doc.innerHTML = res From 2554b7e32147400a0a1f0fc2304a6e449c4bdb2f Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Wed, 27 Apr 2022 07:29:35 +0000 Subject: [PATCH 7/9] Desktop - bump version to 1.1.0, update config --- build_app.js | 2 ++ config/desktop.json | 7 +++++++ electron/README.md | 2 ++ package.json | 2 +- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/build_app.js b/build_app.js index 0891b9f10..024e77ab3 100644 --- a/build_app.js +++ b/build_app.js @@ -45,6 +45,8 @@ copyKey('auth_service') copyKey('notify_service') copyKey('messenger_service') copyKey('elastic_search') +copyKey('apidex_service') +copyKey('hidden_assets') copyKey('app_updater') copyKey('forums') copyKey('gamefication') diff --git a/config/desktop.json b/config/desktop.json index 09e1bb961..9909da8a9 100644 --- a/config/desktop.json +++ b/config/desktop.json @@ -33,6 +33,13 @@ "login": "golosclient", "password": "golosclient" }, + "apidex_service": { + "host": "https://api-dex.golos.app" + }, + "hidden_assets": { + "RUDEX": true, + "PRIZM": true + }, "app_updater": { "host": "https://files.golos.app" }, diff --git a/electron/README.md b/electron/README.md index 25b5643d9..50c8727e7 100644 --- a/electron/README.md +++ b/electron/README.md @@ -31,6 +31,8 @@ npx yarn global add electron@17.1.2 electron-builder@22.14.13 - notify_service - messenger_service - elastic_search +- apidex_service +- hidden_assets - app_updater - forums - gamefication diff --git a/package.json b/package.json index f8aab3c00..8de17823b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "git", "url": "https://github.com/golos-blockchain/ui-blogs.git" }, - "version": "1.0.1", + "version": "1.1.0", "description": "Голос - социальная сеть, построенная на публичном блокчейне.", "main": "dist/electron/electron.js", "scripts": { From a19ba0987ea6f56d4022413b186730fcc7cde3f9 Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Wed, 27 Apr 2022 10:32:26 +0000 Subject: [PATCH 8/9] Desktop - Fix UIA address bug --- app/components/modules/uia/AssetRules.jsx | 36 +++++++++++-- electron/electron.js | 62 +++++++++++++++++------ shared/getUIAAddress.js | 57 +++++++++++++++++++++ 3 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 shared/getUIAAddress.js diff --git a/app/components/modules/uia/AssetRules.jsx b/app/components/modules/uia/AssetRules.jsx index 1a38b34fa..319e22767 100644 --- a/app/components/modules/uia/AssetRules.jsx +++ b/app/components/modules/uia/AssetRules.jsx @@ -5,12 +5,14 @@ import { Map, } from 'immutable'; import { api, } from 'golos-lib-js'; import { Asset, } from 'golos-lib-js/lib/utils'; import CloseButton from 'react-foundation-components/lib/global/close-button'; + import Author from 'app/components/elements/Author' import LoadingIndicator from 'app/components/elements/LoadingIndicator'; import Icon from 'app/components/elements/Icon'; import Memo from 'app/components/elements/Memo'; import transaction from 'app/redux/Transaction'; import { clearOldAddresses, loadAddress, saveAddress, } from 'app/utils/UIA'; +import getUIAAddress from 'shared/getUIAAddress' const TransferState = { initial: 0, @@ -54,23 +56,47 @@ class AssetRules extends Component { } } + doReq = async (acc, sym) => { + if (!process.env.IS_APP) { + const url = '/api/v1/uia_address/' + sym + '/' + acc + let res = await fetch(url) + res = await res.json() + return res + } else { + let res + await getUIAAddress(acc, sym, (address) => { + res = { + status: 'ok', + address + } + }, (errorName, logData, errorData) => { + console.error(...logData) + res = { + status: 'err', + error: errorName, + error_data: errorData, + } + }) + return res + } + } + async doAPI() { const { rules, sym, currentAccount, } = this.props try { const acc = currentAccount.get('name') if (!acc) return - const url = '/api/v1/uia_address/' + sym + '/' + acc - const retried = 0 + let retried = 0 const retryReq = async () => { - let res = await fetch(url) - res = await res.json() + let res = await this.doReq(acc, sym) if (res.status === 'err') { if (retried < 3 && (res.error === 'too_many_requests' || res.error === 'cannot_connect_gateway')) { console.error('Repeating /uia_address', res) ++retried - setTimeout(retryReq, 1100) + await new Promise(resolve => setTimeout(resolve, 1100)) + await retryReq() return } throw new APIError(res.error, res.error_data) diff --git a/electron/electron.js b/electron/electron.js index 805fa5fa0..aafd17871 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -127,26 +127,56 @@ protocol.registerSchemesAsPrivileged([ app.whenReady().then(() => { try { - let notify_service = new URL('*', appSet.notify_service.host).toString() - let auth_service = new URL('*', appSet.auth_service.host).toString() - let app_updater = new URL('*', appSet.app_updater.host).toString() - const filter = { - urls: [ - auth_service, - notify_service, - app_updater - ] + const notify_service = new URL(appSet.notify_service.host) + const auth_service = new URL(appSet.auth_service.host) + const app_updater = new URL(appSet.app_updater.host) + const isSecureURL = (url) => { + try { + url = new URL(url) + if (url.host === notify_service.host || + url.host === auth_service.host || + url.host === app_updater.host) { + return true + } + return false + } catch (err) { + console.error('CORS bypassing - cannot check URI', err) + return false + } + } + const isTrustedPage = (wc) => { + if (!wc) { + return false + } + try { + let url = wc.getURL() + if (!url) { + return false + } + url = new URL(url) + return url.pathname.endsWith('/assets') + } catch (err) { + console.log(wc.getURL()) + console.error('CORS bypassing - cannot get page url', err) + return false + } } - session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => { - upsertHeader(details.requestHeaders, 'Origin', httpsUrl) - callback({ requestHeaders: details.requestHeaders }) + session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { + const { url, webContents, requestHeaders} = details + if (isSecureURL(url) || isTrustedPage(webContents)) { + upsertHeader(requestHeaders, 'Origin', httpsUrl) + } + callback({ requestHeaders }) }) - session.defaultSession.webRequest.onHeadersReceived(filter, (details, callback) => { - upsertHeader(details.responseHeaders, 'Access-Control-Allow-Origin', appUrl) - callback({ responseHeaders: details.responseHeaders }) + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + const { url, webContents, responseHeaders} = details + if (isSecureURL(url)|| isTrustedPage(webContents)) { + upsertHeader(responseHeaders, 'Access-Control-Allow-Origin', appUrl) + } + callback({ responseHeaders }) }) } catch (err) { - console.error('Auth/Notify error:', err) + console.error('CORS bypassing error:', err) } protocol.registerFileProtocol('app', (request, callback) => { diff --git a/shared/getUIAAddress.js b/shared/getUIAAddress.js new file mode 100644 index 000000000..55b08c9d4 --- /dev/null +++ b/shared/getUIAAddress.js @@ -0,0 +1,57 @@ +import { api } from 'golos-lib-js' + +import fetchWithTimeout from 'shared/fetchWithTimeout' + +export default async function getUIAAddress(accName, symbol, okResp, errResp) { + try { + let assets; + try { + assets = await api.getAssetsAsync('', [ + symbol, + ], '', '20', 'by_symbol_name') + } catch (err) { + return errResp('blockchain_unavailable', [err]) + } + if (!assets[0]) + return errResp('no_such_asset') + + let meta = assets[0].json_metadata + try { + meta = JSON.parse(meta) + } catch (err) { + return errResp('your_asset_has_wrong_json_metadata', [meta]) + } + + let apiURL = meta.deposit && meta.deposit.to_api + if (!apiURL) + return errResp('no_deposit_settings_in_your_asset', [meta]) + if (!apiURL.includes('')) + return errResp('url_template_not_contains_place_for_account_name', [meta]) + apiURL = apiURL.replace(//g, accName) + + let resp + try { + resp = await fetchWithTimeout(apiURL, 10000) + } catch (err) { + return errResp('cannot_connect_gateway', [meta.deposit, err]) + } + try { + resp = await resp.text() + } catch (err) { + return errResp('cannot_get_address_from_gateway', [meta.deposit, err]) + } + try { + resp = JSON.parse(resp) + } catch (err) { + resp = resp.substring(0, 100) + return errResp('invalid_json_from_gateway', [meta.deposit, err], resp) + } + + if (!resp.address) + return errResp('no_address_field_in_response', [resp], resp) + + return okResp(resp.address) + } catch (err) { + return errResp('internal_error', err) + } +} From bed88f1fa6c8d5e8c40c2756965cb8d0db7ffabc Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Wed, 27 Apr 2022 11:04:16 +0000 Subject: [PATCH 9/9] Desktop - fix links navigation --- electron/electron.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/electron/electron.js b/electron/electron.js index aafd17871..e2c89d650 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -18,8 +18,11 @@ try { const appUrl = 'app://' + site_domain const httpsUrl = 'https://' + site_domain +const isHttpsURL = (url) => { + return url.startsWith(httpsUrl) +} const isOwnUrl = (url) => { - return url.startsWith(httpsUrl) || url.startsWith(appUrl) + return isHttpsURL(url) || url.startsWith(appUrl) } // events which need to be set for main window and for child windows @@ -40,6 +43,9 @@ const setCommonWindowEvents = (win) => { if (!isOwnUrl(url)) { e.preventDefault() shell.openExternal(url) + } else if (isHttpsURL(url)) { + e.preventDefault() + win.loadURL(url.replace(httpsUrl, appUrl)) } }) @@ -57,7 +63,7 @@ const setCommonWindowEvents = (win) => { } } } else { - win.loadURL(url) + win.loadURL(url.replace(httpsUrl, appUrl)) } return { action: 'deny' } })