diff --git a/config/default.json b/config/default.json index ab880c9..04758b8 100644 --- a/config/default.json +++ b/config/default.json @@ -14,7 +14,7 @@ "signing_key": "5K67PNheLkmxkgJ5UjvR8Nyt3GVPoLEN1dMZjFuNETzrNyMecPG", "delegation": "75000.000000 GESTS", "free_regs_per_day": 10, - "uias": ["YMUSDT", "YMPZM", "YMHIVE"] + "uias": ["YMUSDT", "YMPZM", "YMHIVE", "TESTF"] }, "server_session_secret": "exiKdyF+IwRIXJDmtGIl4vWUz4i3eVSISpfZoeYc0s4=", "session_cookie_key": "X-Reg-ISession", @@ -39,7 +39,7 @@ "styleSrc": "'self' 'unsafe-inline' fonts.googleapis.com", "imgSrc": "* data:", "fontSrc": "data: fonts.gstatic.com", - "connectSrc": "'self' apibeta.golos.today wss://apibeta.golos.today", + "connectSrc": "'self' apibeta.golos.today wss://apibeta.golos.today *.golos.app", "frameAncestors": "'none'" } }, @@ -76,6 +76,9 @@ "bot_token": "6390290192:AAEGipeCa4P0V4HZzoXvITCij8on6OItsGM" } }, + "apidex_service": { + "host": "https://api-dex.golos.app" + }, "default_client": "blogs", "clients": { "blogs": { diff --git a/db/app.lua b/db/app.lua index 9cae217..7cc8e18 100644 --- a/db/app.lua +++ b/db/app.lua @@ -3,6 +3,7 @@ require 'guid' require 'oauth' require 'server_tokens' require 'cryptostore' +require 'reg_pollers' io.output():setvbuf('no') @@ -91,4 +92,5 @@ box.once('bootstrap', function() oauth_bootstrap() server_tokens_bootstrap() cryptostore_bootstrap() + reg_pollers_bootstrap() end) diff --git a/db/reg_pollers.lua b/db/reg_pollers.lua new file mode 100644 index 0000000..6d6ce75 --- /dev/null +++ b/db/reg_pollers.lua @@ -0,0 +1,92 @@ +fiber = require 'fiber' + +function reg_pollers_bootstrap() + box.schema.sequence.create('reg_pollers') + reg_pollers = box.schema.create_space('reg_pollers', { + format = { + {name = 'id', type = 'unsigned'}, + {name = 'sym', type = 'STR'}, + {name = 'amount', type = 'number'}, + {name = 'uid', type = 'STR'}, + {name = 'created', type = 'unsigned'}, + {name = 'init_balance', type = 'number'}, + } + }) + reg_pollers:create_index('primary', { + sequence='reg_pollers' + }) + reg_pollers:create_index('by_amount', { + type = 'tree', parts = { + 'sym', + 'amount' + }, unique = true + }) + reg_pollers:create_index('by_created', { + type = 'tree', parts = { + 'created' + }, unique = false + }) +end + +local function wrap_rp(rp) + return { + id = rp[1], + sym = rp[2], + amount = rp[3], + uid = rp[4], + created = rp[5], + init_bal = rp[6], + } +end + +function get_free_reg_poller(amount, sym, now) + local rps = nil + repeat + if rps ~= nil then + local rp = wrap_rp(rps[1]) + if (now - rp['created']) >= 20*60*1000 then + box.space.reg_pollers:delete(rp['id']) + break + end + amount = amount + 1 + end + rps = box.space.reg_pollers.index.by_amount:select{sym, amount} + until #rps == 0 + return amount +end + +local function clean_reg_pollers(now) + for i,rp in box.space.reg_pollers.index.by_created:pairs(0, {iterator = 'GT', limit = 100}) do + local rp = wrap_rp(rp) + if (now - rp['created']) > 20*60*1000 then + box.space.reg_pollers:delete(rp['id']) + else + break + end + end +end + +function upsert_reg_poller(amount, sym, uid, init_bal, now) + clean_reg_pollers(now) + local rps = box.space.reg_pollers.index.by_amount:select{sym, amount} + if #rps ~= 0 then + local rp = rps[1] + rp = wrap_rp(rp) + if rp['uid'] ~= uid then + return { err = 'Someone already waits for such transfer.', res = nil } + end + return { err = nil, res = rp } + end + local rp = box.space.reg_pollers:insert{nil, sym, amount, uid, now, init_bal} + return { err = nil, res = wrap_rp(rp) } +end + +function delete_reg_poller(amount, sym) + local rps = box.space.reg_pollers.index.by_amount:select{sym, amount} + if #rps ~= 0 then + local rp = rps[1] + box.space.reg_pollers:delete(rp['id']) + return true + end + return false +end diff --git a/public/icons/copy.svg b/public/icons/copy.svg new file mode 100644 index 0000000..5babb4b --- /dev/null +++ b/public/icons/copy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/icons/copy_ok.svg b/public/icons/copy_ok.svg new file mode 100644 index 0000000..978d011 --- /dev/null +++ b/public/icons/copy_ok.svg @@ -0,0 +1,22 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 04df70c..fde8728 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -98,12 +98,18 @@ "register_with": "Register with...", "no_such_asset": " - no such UIA.", "deposit_unavailable": " - deposit temporarily unavailable.", + "cmc_error": " - cannot get min amount from CoinMarketCap.", + "transfer_not_supported": " - not yet supported.", "min_amount": "Min amount", "fee": "Fee", "memo_fixed": "Memo", "to": "Send tokens to address/account", "api_error": "Cannot get address. Try again later. If problem still occurs, contact the issuer of ", - "api_error_details": "and send the error details:" + "api_error_details": "and send the error details:", + "free_poller": "Cannot calculate minimal amount. Try again later, please. Or try another currency.", + "deposited": "Received ", + "deposited2": ". Placing limit order to exchange it to GOLOS to register your account.", + "cannot_place_order": "Cannot place order: " }, "invites_jsx": { "claim_wrong_secret": "Wrong secret", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index b6b56eb..c0bb103 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -98,12 +98,18 @@ "register_with": "Регистрация с помощью...", "no_such_asset": " - такого UIA не существует.", "deposit_unavailable": " - депозит временно недоступен.", + "cmc_error": " - не удается получить минимальную сумму с CoinMarketCap.", + "transfer_not_supported": " - пока не поддерживается.", "min_amount": "Минимальная сумма", "fee": "Комиссия", "memo_fixed": "Заметка/memo", "to": "Отправьте токены на адрес/аккаунт", "api_error": "Не удается получить адрес. Попробуйте позднее. Если проблема сохраняется, свяжитесь с эмитентом ", - "api_error_details": "и сообщите подробности ошибки:" + "api_error_details": "и сообщите подробности ошибки:", + "free_poller": "Не удается рассчитать минимальную сумму. Попробуйте позже, или другую валюту.", + "deposited": "Получено ", + "deposited2": ". Размещаем ордер для обмена их на GOLOS для регистрации вашего аккаунта...", + "cannot_place_order": "Не удается разместить ордер: " }, "invites_jsx": { "claim_wrong_secret": "Неверно указан ключ", diff --git a/src/modules/register/TransferWaiter.jsx b/src/modules/register/TransferWaiter.jsx index 1804d00..31f64a2 100644 --- a/src/modules/register/TransferWaiter.jsx +++ b/src/modules/register/TransferWaiter.jsx @@ -3,6 +3,7 @@ import tt from 'counterpart' import { Asset } from 'golos-lib-js/lib/utils'; import LoadingIndicator from '@/elements/LoadingIndicator' +import { delay, } from '@/utils/misc' import { callApi, } from '@/utils/RegApiClient' class TransferWaiter extends React.Component { @@ -13,18 +14,20 @@ class TransferWaiter extends React.Component { super(props) } - poll = async (sym) => { + poll = async (minAmount) => { const retry = async () => { - await new Promise(resolve => setTimeout(resolve, 1000)) - if (!this.state.stopped) - this.poll(sym) + await delay(1000) + if (!this.stopped) + this.poll(minAmount) + else + this.stoppedPolling = true } try { - let res = await callApi('/api/reg/wait_for_transfer/' + sym) + let res = await callApi('/api/reg/wait_for_transfer/' + minAmount.toString()) res = await res.json() if (res.status === 'ok') { const { onTransfer } = this.props - onTransfer(Asset(res.delta)) + onTransfer(Asset(res.deposited)) } else { console.error(res) await retry() @@ -36,9 +39,10 @@ class TransferWaiter extends React.Component { } start = async () => { + this.stopped = false + this.stoppedPolling = false this.setState({ - seconds: 30*60, - stopped: false + seconds: 15*60, }) this.countdown = setInterval(() => { @@ -53,29 +57,34 @@ class TransferWaiter extends React.Component { }) }, 1000) - const { sym, } = this.props + const { minAmount, } = this.props - this.poll(sym) + this.poll(minAmount) } componentDidMount() { this.start() } - stop = () => { + stop = async () => { if (this.countdown) clearInterval(this.countdown) - this.setState({ - stopped: true - }) + this.stopped = true + await delay(1000) + if (!this.stoppedPolling) { + await delay(3000) + } } componentWillUnmount() { this.stop() } - componentDidUpdate(prevProps) { - if (this.props.sym !== prevProps.sym) { - this.stop() + async componentDidUpdate(prevProps) { + const { minAmount } = this.props + if (minAmount && (!prevProps.minAmount || + minAmount.symbol !== prevProps.minAmount.symbol || + minAmount.amount !== prevProps.minAmount.amount)) { + await this.stop() this.start() } } @@ -86,7 +95,7 @@ class TransferWaiter extends React.Component { const min = Math.floor(seconds / 60) const sec = seconds % 60 const remaining = min.toString().padStart(2, '0') + ':' + sec.toString().padStart(2, '0') - const { sym, title } = this.props + const { title } = this.props return
{title}
diff --git a/src/modules/register/UIARegister.jsx b/src/modules/register/UIARegister.jsx index e702cbb..a1d589e 100644 --- a/src/modules/register/UIARegister.jsx +++ b/src/modules/register/UIARegister.jsx @@ -2,7 +2,7 @@ import React from 'react' import CopyToClipboard from 'react-copy-to-clipboard' import tt from 'counterpart' import cn from 'classnames' -import golos, { api, broadcast } from 'golos-lib-js' +import golos, { api, } from 'golos-lib-js' import { Asset } from 'golos-lib-js/lib/utils'; import { key_utils, PrivateKey } from 'golos-lib-js/lib/auth/ecc' import Link from 'next/link' @@ -11,7 +11,9 @@ import LoadingIndicator from '@/elements/LoadingIndicator' import AccountName from '@/elements/register/AccountName' import VerifyWayTabs from '@/elements/register/VerifyWayTabs' import TransferWaiter from '@/modules/register/TransferWaiter' +import { apidexGetPrices, } from '@/utils/ApidexApiClient' import KeyFile from '@/utils/KeyFile' +import { delay, } from '@/utils/misc' import { emptyAuthority } from '@/utils/RecoveryUtils' import { callApi, } from '@/utils/RegApiClient' import { withRouterHelpers, } from '@/utils/routing' @@ -39,14 +41,6 @@ function getAssetMeta(asset) { return res } -const TransferState = { - initial: 0, - transferring: 1, - waiting: 2, - received: 3, - timeouted: 4, -}; - class APIError extends Error { constructor(errReason, errData) { super('API Error') @@ -68,6 +62,7 @@ class UIARegister extends React.Component { if (clientCfg.config.chain_id) golos.config.set('chain_id', clientCfg.config.chain_id) + const { apidex_service } = clientCfg.config const { uias } = clientCfg.config.registrar const path = this.getPath() @@ -83,7 +78,8 @@ class UIARegister extends React.Component { error = sym + tt('uia_register_jsx.no_such_asset') } else { for (const asset of assets) { - const symbol = Asset(asset.supply).symbol + const supply = Asset(asset.supply) + const symbol = supply.symbol if (sym === symbol) { const meta = getAssetMeta(asset) const { deposit, telegram } = meta @@ -96,23 +92,67 @@ class UIARegister extends React.Component { let registrar = await golos.api.getAccounts([accName]) registrar = registrar[0] - const { to_type, to_api, } = deposit + const { to_type, to_api, fee, } = deposit + if (fee && (isNaN(fee) || parseFloat(fee) !== 0)) { + error = sym + tt('uia_register_jsx.transfer_not_supported') + break + } if (to_type === 'transfer') { - /*clearOldAddresses(); - const addr = loadAddress(sym, asset.creator); - if (addr) { - this.setState({ - transferState: TransferState.received, - receivedTransfer: { - memo: addr, - }, - }); - }*/ + error = sym + tt('uia_register_jsx.transfer_not_supported') + break + } + + let minAmount = Asset(0, supply.precision, supply.symbol) + + let cmc + try { + cmc = await apidexGetPrices(apidex_service, sym) + if (!cmc.price_usd) { + console.error('Cannot obtain price_usd', cmc) + throw Error('Cannot obtain price_usd') + } + const priceUsd = parseFloat(cmc.price_usd) + let minFloat = 1 / priceUsd + if (minFloat >= 0.99 && minFloat < 1) { + minFloat = 1 + } + + minAmount.amountFloat = minFloat.toString() + } catch (err) { + console.error(err) + error = sym + tt('uia_register_jsx.cmc_error') + break + } + + const min_amount = parseFloat(deposit.min_amount) + if (min_amount != NaN) { + const minAmountRules = Asset(0, supply.precision, supply.symbol) + minAmountRules.amountFloat = min_amount.toString() + if (minAmountRules.gt(minAmount)) { + minAmount = minAmountRules + } + } + + for (let i = 0; i < 3; ++i) { + try { + let fp = await callApi('/api/reg/get_free_poller/' + minAmount.toString()) + fp = await fp.json() + if (fp.error) { + throw new Error(fp.error) + } + minAmount = Asset(fp.amount) + error = null + break + } catch (err) { + console.error('get_free_poller', err) + error = tt('uia_register_jsx.free_poller') + } + await delay(2000) } this.setState({ - transferState: TransferState.initial, rules: { ...deposit, creator: asset.creator, telegram }, + minAmount, registrar, sym, copied_addr: false, @@ -177,7 +217,7 @@ class UIARegister extends React.Component { || res.error === 'cannot_connect_gateway')) { console.error('Repeating /uia_address', res) ++retried - await new Promise(resolve => setTimeout(resolve, 1100)) + await delay(1100) await retryReq() return } @@ -222,55 +262,6 @@ class UIARegister extends React.Component { return Asset(this.balanceValue()).gte(Asset('0.001 GOLOS')); } - transfer = async () => { - this.setState({ - transferState: TransferState.transferring, - }, () => { - this.transferAndWait() - }) - } - - waitingTimeout = (10 + 1) * 60 * 1000 - - transferAndWait = async () => { - const { sym, rules, registrar } = this.state - const { to_transfer, memo_transfer, } = rules - let stopper - let stopStream = api.streamOperations((err, op) => { - if (op[0] === 'transfer' && op[1].from === to_transfer - && op[1].to === registrar.name) { - stopStream(); - clearTimeout(stopper); - saveAddress(sym, rules.creator, op[1].memo); - this.setState({ - transferState: TransferState.received, - receivedTransfer: op[1], - }); - } - }) - - try { - const res = await broadcast.transferAsync(registrar.name, to_transfer, '0.001 GOLOS', memo_transfer) - } catch (err) { - console.error(err) - this.setState({ - transferState: TransferState.initial, - }) - stopStream() - return - } - - this.setState({ - transferState: TransferState.waiting, - }); - stopper = setTimeout(() => { - if (stopStream) stopStream(); - this.setState({ - transferState: TransferState.timeouted, - }) - }, this.waitingTimeout) - } - _renderTo = (to, to_fixed, username) => { let addr = to || to_fixed; if (username) @@ -291,9 +282,9 @@ class UIARegister extends React.Component { } _renderParams = () => { - const { rules, sym, registrar } = this.state + const { rules, sym, registrar, minAmount } = this.state const username = registrar.name - const { min_amount, fee, memo_fixed } = rules + const { memo_fixed } = rules let details = rules.details if (memo_fixed) { details = details.split('').join(username) @@ -303,10 +294,8 @@ class UIARegister extends React.Component { {details &&
{details}

} - {min_amount &&
- {tt('uia_register_jsx.min_amount')} {min_amount} {sym || ''}
} - {fee &&
- {tt('uia_register_jsx.fee') + ': '}{fee} {sym || ''}
} + {minAmount &&
+ {tt('uia_register_jsx.min_amount')} {minAmount.floatString}
}
; } @@ -351,72 +340,41 @@ class UIARegister extends React.Component {
) } - _renderTransfer = () => { - const { rules, sym, transferState, receivedTransfer, } = this.state - const { to_transfer, memo_transfer, } = rules - - const transferring = transferState === TransferState.transferring - - const enough = this.enoughBalance() - - if (transferState === TransferState.received) { - const { registrar, } = this.state - const { memo, } = receivedTransfer; - return (
- {this._renderTo(receivedTransfer.memo, null, registrar.name)} - {this._renderParams(false)} -
); - } - - if (transferState === TransferState.timeouted) { - return (
- {tt('asset_edit_deposit_jsx.timeouted')} - {sym || ''} - . -
); - } - - if (transferState === TransferState.waiting) { - return (
- {tt('asset_edit_deposit_jsx.waiting')} -
-
-
- -
-
-
); - } - - return (
- {tt('uia_register_jsx.transfer_desc')} - {to_transfer || ''} - {tt('uia_register_jsx.transfer_desc_2')} - {memo_transfer || ''} - {transferring ? - : null} - - {!enough ?
- {tt('transfer_jsx.insufficient_funds')} -
: null} - {this._renderParams()} -
); - } - _renderWaiter = () => { - const { sym, registrar, onTransfer } = this.state + const { minAmount, registrar, onTransfer } = this.state if (!onTransfer) { - onTransfer = (delta) => { + onTransfer = async (deposited) => { + this.setState({ + deposited + }) + + let error + for (let tr = 1; tr <= 10; ++tr) { + try { + let orr = await callApi('/api/reg/place_order/' + deposited.toString(), {}) + orr = await orr.json() + if (orr.status === 'ok') { + console.log(orr) + this.props.updateApiState(orr) + return + } + console.error('place_order', orr) + throw new Error(orr.error) + } catch (err) { + console.error('place_order throws', err) + error = (err && err.toString) && err.toString() + } + await delay(3000) + } + this.setState({ - deposited: delta + error: tt('uia_register_jsx.cannot_place_order') + error }) } } return + minAmount={minAmount} title={''} onTransfer={onTransfer} /> } render() { @@ -436,10 +394,11 @@ class UIARegister extends React.Component { const meta = getAssetMeta(asset) if (meta.deposit) { const symbol = Asset(asset.supply).symbol + const displaySym = symbol.startsWith('YM') ? symbol.substring(2) : symbol syms.push( - {symbol} + {displaySym} ) } @@ -453,24 +412,18 @@ class UIARegister extends React.Component { if (deposited) { form =
- {!embed ?

- {tt('asset_edit_deposit_jsx.transfer_title_SYM', { - SYM: sym || ' ', - })} -

: null} -

- {tt('asset_edit_deposit_jsx.you_received')} - {deposited.toString()}. {tt('asset_edit_deposit_jsx.you_received2')} + {tt('uia_register_jsx.deposited')} + {deposited.floatString} + {tt('uia_register_jsx.deposited2')} +
+
} else { const { rules, registrar, } = this.state - const { to, to_type, to_fixed, to_transfer, - min_amount, fee, details, } = rules + const { to, to_type, to_fixed, details, } = rules if (to_type === 'api') { form = this._renderApi() - } else if (to_type === 'transfer') { - form = this._renderTransfer() } else { let memo_fixed = rules.memo_fixed if (memo_fixed) { diff --git a/src/pages/api/reg/[...all].js b/src/pages/api/reg/[...all].js index 2447f4c..fdccc0b 100644 --- a/src/pages/api/reg/[...all].js +++ b/src/pages/api/reg/[...all].js @@ -1,23 +1,23 @@ import config from 'config'; import gmailSend from 'gmail-send'; -import golos, { api } from 'golos-lib-js' +import golos, { api, broadcast } from 'golos-lib-js' import { hash, } from 'golos-lib-js/lib/auth/ecc'; import { Asset } from 'golos-lib-js/lib/utils' import secureRandom from 'secure-random'; + import nextConnect from '@/server/nextConnect'; import { throwErr, } from '@/server/error'; import { initGolos, } from '@/server/initGolos'; -import { getVersion, rateLimitReq, getRemoteIp, +import { getVersion, rateLimitReq, slowDownLimitReq, getRemoteIp, noBodyParser, bodyParams, } from '@/server/misc'; import passport, { addModalRoutes, checkAlreadyUsed } from '@/server/passport'; import { getDailyLimit, obtainUid, getClientCfg, } from '@/server/reg'; import { regSessionMiddleware, } from '@/server/regSession'; import Tarantool from '@/server/tarantool'; +import { delay, } from '@/utils/misc' initGolos(); -global.pollIntervals = {} - let handler = nextConnect({ attachParams: true, }) .use(regSessionMiddleware) .use(passport.initialize()) @@ -307,44 +307,129 @@ let handler = nextConnect({ attachParams: true, }) }); }) - .get('/api/reg/wait_for_transfer/:sym', async (req, res) => { - const { sym } = req.params + .get('/api/reg/get_free_poller/:amount', async (req, res) => { + await slowDownLimitReq(req) + + const amountStr = req.params.amount.split('%20').join(' ') + let amount + try { + amount = await Asset(amountStr) + } catch (err) { + res.json({ + status: 'err', + error: 'Asset parse error' + }) + return + } + + let freeAmount + try { + freeAmount = await Tarantool.instance('tarantool') + .call('get_free_reg_poller', + parseFloat(amount.amountFloat), + amount.symbol, + Date.now() + ) + freeAmount = freeAmount[0][0] + } catch (err) { + console.error('ERROR: cannot get_free_reg_poller', err); + } + + if (freeAmount) { + const ret = await Asset(0, amount.precision, amount.symbol) + ret.amountFloat = freeAmount.toString() + res.json({ + status: 'ok', + amount: ret.toString() + }) + return + } + + res.json({ + status: 'err', + error: 'Tarantool error' + }) + }) + + .get('/api/reg/wait_for_transfer/:amount', async (req, res) => { + await slowDownLimitReq(req) + + const amountStr = req.params.amount.split('%20').join(' ') + let amount + try { + amount = await Asset(amountStr) + } catch (err) { + res.json({ + status: 'err', + error: 'Asset parse error' + }) + return + } + + const uid = req.session.uid - if (global.pollIntervals[sym]) throwErr(req, 400, ['Someone already waits for the transfer']) + if (!uid) throwErr(rew, 400, ['Not authorized - no uid in session']) const username = config.get('registrar.account') if (!username) throwErr(req, 400, ['No registrar.account in config']) const getBalance = async () => { const balances = await api.getAccountsBalancesAsync([username], { - symbols: [sym] + symbols: [amount.symbol] }) - let bal = balances[0][sym] + let bal = balances[0][amount.symbol] if (bal) { bal = bal.balance - return Asset(bal) + return await Asset(bal) } else { - const assets = await api.getAssetsAsync('', [sym]) + const assets = await api.getAssetsAsync('', [amount.symbol]) if (!assets[0]) throwErr(req, 400, ['No such asset']) - bal = Asset(assets[0].supply) + bal = await Asset(assets[0].supply) bal.amount = 0 return bal } } - const stop = () => { - if (global.pollIntervals[sym]) { - clearInterval(global.pollIntervals[sym].interval) - delete global.pollIntervals[sym] + const stop = async () => { + let delRes + try { + delRes = await Tarantool.instance('tarantool') + .call('delete_reg_poller', + parseFloat(amount.amountFloat), amount.symbol + ) + delRes = delRes[0][0] + } catch (err) { + console.error('ERROR: cannot delete reg poller', err); } } - const initBal = await getBalance() + let initBal + try { + initBal = await getBalance() + } catch (err) { + console.error('wait_for_transfer getBalance', err) + throwErr(req, 400, ['Blockchain unavailable']) + } + + let pollerRes + try { + pollerRes = await Tarantool.instance('tarantool') + .call('upsert_reg_poller', + parseFloat(amount.amountFloat), amount.symbol, uid, parseFloat(initBal.amountFloat), Date.now() + ) + pollerRes = pollerRes[0][0] + } catch (err) { + console.error('ERROR: cannot upsert_reg_poller', err); + } + + if (pollerRes.err) { + throwErr(req, 400, [pollerRes.err]) + } + const pollMsec = process.env.NODE_ENV === 'development' ? 1000 : 30000 let tries = 0 - global.pollIntervals[sym] = { created: Date.now(), interval: setInterval(async () => { + for ( ;; ) { if (tries > 2) { - stop() res.json({ status: 'err', error: 'Timeouted' @@ -353,17 +438,175 @@ let handler = nextConnect({ attachParams: true, }) } ++tries - const bal = await getBalance() + let bal + try { + bal = await getBalance() + } catch (err) { + throwErr(req, 400, ['Blockchain unavailable']) + } + + initBal.amountFloat = pollerRes.res.init_bal.toString() + console.log('wait_for_transfer', initBal.toString(), bal.toString()) - if (bal.amount > initBal.amount) { - stop() - const delta = Asset(bal.amount - initBal.amount, bal.precision, bal.symbol) - res.json({ - status: 'ok', - delta: delta.toString() - }) + const delta = bal.minus(initBal) + if (delta.gte(amount)) { + let stopMe = false + + let hist + try { + hist = await api.getAccountHistoryAsync(username, -1, 1000, {select_ops: ['transfer']}) + } catch (err) { + console.error('/api/reg/wait_for_transfer - getAccountHistoryAsync', err) + throwErr(req, 400, ['Blockchain unavailable']) + } + + const created = pollerRes.res.created + for (let i = hist.length - 1; i >= 0; --i) { + const timestamp = +new Date(hist[i][1].timestamp + 'Z') + if (timestamp < created) { + break + } + + const [ opType, opData ] = hist[i][1].op + if (opType === 'transfer') { + if (opData.to === username && opData.amount === amountStr) { + stopMe = true + break + } + } + } + + if (stopMe) { + if (!req.session.deposited) { + req.session.deposited = {} + } + req.session.deposited[amount.symbol] = delta.toString() + await req.session.save() + + await stop() + + res.json({ + status: 'ok', + deposited: delta.toString() + }) + return + } + } + + await delay(pollMsec) + } + }) + + .post('/api/reg/place_order/:amount', async (req, res) => { + await slowDownLimitReq(req) + + const amountStr = req.params.amount.split('%20').join(' ') + let amount + try { + amount = await Asset(amountStr) + } catch (err) { + res.json({ + status: 'err', + error: 'Asset parse error' + }) + return + } + + if (!req.session.deposited) { + throwErr(req, 400, ['You have no deposited']) + } + const deposited = req.session.deposited[amount.symbol] + if (!deposited) { + throwErr(req, 400, ['You have no deposited in ' + amount.symbol]) + } + if (deposited !== amountStr) { + throwErr(req, 400, ['You have wrong deposited in ' + amount.symbol]) + } + + let chainProps + for (let i = 0; i < 3; ++i) { + try { + chainProps = await api.getChainPropertiesAsync() + break + } catch (err) { + console.error('/api/reg/place_order - getChainPropertiesAsync', err) + await delay(3000) + } + } + if (!chainProps) { + throwErr(req, 503, ['/api/reg/place_order - Blockchain node unavailable - cannot getChainPropertiesAsync']) + } + if (!chainProps.chain_status) { + throwErr(req, 503, ['/api/reg/place_order - Blockchain node is stopped - chain_status is false']) + } + + const username = config.get('registrar.account') + + const signingKey = config.get('registrar.signing_key') + const orderid = Math.floor(Date.now() / 1000) + let operations = [['limit_order_create', { + owner: username, + orderid, + amount_to_sell: amountStr, + min_to_receive: chainProps.create_account_min_golos_fee, + fill_or_kill: true, + expiration: 0xffffffff + }]] + try { + await broadcast.sendAsync({ + extensions: [], + operations + }, [signingKey]) + } catch (err) { + console.error('/api/reg/place_order - Cannot sell tokens', err) + throwErr(req, 400, ['Cannot sell tokens']) + } + + delete req.session.deposited[amount.symbol] + await req.session.save() + + await delay(1500) + + let hist + try { + hist = await api.getAccountHistoryAsync(username, -1, 1000, {select_ops: ['fill_order']}) + } catch (err) { + console.error('/api/reg/place_order - getAccountHistoryAsync', err) + throwErr(req, 400, ['Blockchain unavailable']) + } + + let golos_received + for (let i = hist.length - 1; i >= 0; --i) { + const timestamp = +new Date(hist[i][1].timestamp + 'Z') / 1000 + if (orderid - timestamp > 10) { + break + } + + const [ opType, opData ] = hist[i][1].op + if (opType === 'fill_order') { + if (opData.current_orderid == orderid || opData.open_orderid == orderid) { + const golosAmount = await Asset(opData.current_orderid == orderid ? + opData.open_pays : opData.current_pays) + + golos_received = golos_received || await Asset('0.000 GOLOS') + golos_received = golos_received.plus(golosAmount) + req.session.golos_received = golos_received.toString() + await req.session.save() + } } - }, pollMsec) } + } + + if (golos_received) { + res.json({ + status: 'ok', + golos_received, + step: 'verified', + verification_way: 'uia' + }) + return + } + + throwErr(req, 400, ['Cannot find fill_order operation']) }) handler = addModalRoutes(handler); diff --git a/src/pages/api/reg/submit.js b/src/pages/api/reg/submit.js index 9bdd0c3..52af79f 100644 --- a/src/pages/api/reg/submit.js +++ b/src/pages/api/reg/submit.js @@ -1,6 +1,5 @@ import config from 'config'; import { api, broadcast, } from 'golos-lib-js'; -import { Signature, } from 'golos-lib-js/lib/auth/ecc'; import { checkCaptcha } from '@/server/captcha' import nextConnect from '@/server/nextConnect'; @@ -235,4 +234,3 @@ var createAccount = async function createAccount({ operations }, [signingKey]) } -const parseSig = hexSig => {try {return Signature.fromHex(hexSig)} catch(e) {return null}} diff --git a/src/pages/api/reg/submit_uia.js b/src/pages/api/reg/submit_uia.js new file mode 100644 index 0000000..1e730f8 --- /dev/null +++ b/src/pages/api/reg/submit_uia.js @@ -0,0 +1,77 @@ +import config from 'config' +import { api, broadcast, } from 'golos-lib-js' +import { Asset } from 'golos-lib-js/lib/utils' + +import { checkCaptcha } from '@/server/captcha' +import nextConnect from '@/server/nextConnect'; +import { throwErr, } from '@/server/error'; +import { rateLimitReq, + noBodyParser, bodyParams, } from '@/server/misc'; +import { useDailyLimit } from '@/server/reg' +import { regSessionMiddleware, } from '@/server/regSession'; + +let handler = nextConnect() + .use(regSessionMiddleware) + + .post('/api/reg/submit_uia', async (req, res) => { + let state = { + step: 'verified', + }; + + rateLimitReq(req, state); + + const { golos_received } = req.session + if (!golos_received) { + throwErr(req, 400, ['You have no golos_received in session']) + } + let fee + try { + fee = await Asset(golos_received) + } catch (err) { + throwErr(req, 400, ['Cannot parse golos_received']) + } + + const cp = await api.getChainPropertiesAsync() + const account_creation_fee = await Asset(cp.account_creation_fee) + + if (fee.lt(account_creation_fee)) { + throwErr(req, 400, ['Your deposited is to low - ' + fee.toString() + ' when minimum fee is ' + account_creation_fee.toString()]) + } + + const account = await bodyParams(req) + + if (!checkCaptcha(account.recaptcha_v2)) { + console.error('-- /submit: try to register without ReCaptcha v2 solving, data:', res.data, ', form fields:', account) + throwErr(req, 403, ['recaptcha_v2_failed'], null, state) + } + + const signingKey = config.get('registrar.signing_key') + const operations = [['account_create', { + fee: golos_received, + creator: config.registrar.account, + new_account_name: account.name, + json_metadata: '{}', + owner: {weight_threshold: 1, account_auths: [], key_auths: [[account.owner_key, 1]]}, + active: {weight_threshold: 1, account_auths: [], key_auths: [[account.active_key, 1]]}, + posting: {weight_threshold: 1, account_auths: [], key_auths: [[account.posting_key, 1]]}, + memo_key: account.memo_key + }]] + await broadcast.sendAsync({ + extensions: [], + operations + }, [signingKey]) + + delete req.session.golos_received + await req.session.save() + + state.status = 'ok'; + res.json({ + ...state, + }); + }) + +export default handler + +export { + noBodyParser as config, +} diff --git a/src/pages/register/[[...client]].jsx b/src/pages/register/[[...client]].jsx index 0f8b07f..3f54206 100644 --- a/src/pages/register/[[...client]].jsx +++ b/src/pages/register/[[...client]].jsx @@ -404,10 +404,11 @@ class Register extends React.Component { } - } else if (state.verificationWay === 'uia') { + } else if (state.verificationWay === 'uia' && state.step !== 'verified') { form = } @@ -685,17 +686,29 @@ class Register extends React.Component { const keyFile = new KeyFile(name, {password, ...privateKeys}); try { + let res // create account - const res = await callApi('/api/reg/submit', { - invite_code: verificationWay === 'invite_code' ? invite_code : undefined, - name, - owner_key: publicKeys[0], - active_key: publicKeys[1], - posting_key: publicKeys[2], - memo_key: publicKeys[3], - referrer, - recaptcha_v2, - }); + if (this.state.verificationWay === 'uia') { + res = await callApi('/api/reg/submit_uia', { + name, + owner_key: publicKeys[0], + active_key: publicKeys[1], + posting_key: publicKeys[2], + memo_key: publicKeys[3], + recaptcha_v2, + }) + } else { + res = await callApi('/api/reg/submit', { + invite_code: verificationWay === 'invite_code' ? invite_code : undefined, + name, + owner_key: publicKeys[0], + active_key: publicKeys[1], + posting_key: publicKeys[2], + memo_key: publicKeys[3], + referrer, + recaptcha_v2, + }) + } const data = await res.json(); @@ -768,7 +781,7 @@ class Register extends React.Component { this.setState({ inviteError, inviteHint }); }; - updateApiState(res, after) { + updateApiState = (res, after) => { const { step, verification_way, error_str, } = res; let newState = {}; diff --git a/src/pages/sign/delegate_vs.jsx b/src/pages/sign/delegate_vs.jsx index 8dc7de0..3706011 100644 --- a/src/pages/sign/delegate_vs.jsx +++ b/src/pages/sign/delegate_vs.jsx @@ -10,8 +10,8 @@ import LoginForm from '@/modules/LoginForm'; import { getOAuthCfg, getChainData, } from '@/server/oauth'; import { getOAuthSession, } from '@/server/oauthSession'; import { withSecureHeadersSSR, } from '@/server/security'; +import { steemToVests, } from '@/utils/misc' import { callApi, } from '@/utils/OAuthClient'; -import { steemToVests, } from '@/utils/State'; import validate_account_name from '@/utils/validate_account_name'; function calcMaxInterest(cprops) { diff --git a/src/server/reg.js b/src/server/reg.js index 698d418..e260fd4 100644 --- a/src/server/reg.js +++ b/src/server/reg.js @@ -134,6 +134,9 @@ export function getClientCfg(req, params, locale = '') { if (!cfg.registrar.uias) cfg.registrar.uias = [] + cfg.apidex_service = config.has('apidex_service') + && config.get('apidex_service') + let data = { config: cfg, oauthEnabled: config.has('oauth'), diff --git a/src/utils/ApidexApiClient.js b/src/utils/ApidexApiClient.js new file mode 100644 index 0000000..d82bd3c --- /dev/null +++ b/src/utils/ApidexApiClient.js @@ -0,0 +1,84 @@ +import { fetchEx } from 'golos-lib-js/lib/utils' + +const request_base = { + timeout: 2000, + 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() +} + +export const apidexUrl = (apidex_service, pathname) => { + try { + return new URL(pathname, apidex_service.host).toString(); + } catch (err) { + console.error('apidexUrl', err) + return '' + } +} + +let cached = {} + +export async function apidexGetPrices(apidex_service, sym) { + const empty = { + price_usd: null, + price_rub: null, + page_url: null + } + if (!apidex_service || !apidex_service.host) 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 fetchEx(apidexUrl(apidex_service, `/api/v1/cmc/${sym}`), 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 + } +} + +let cachedAll = {} + +export async function apidexGetAll(apidex_service) { + const empty = { + data: {} + } + if (!apidex_service || !apidex_service.host) return empty + let request = Object.assign({}, request_base) + try { + const now = new Date() + if (cachedAll && (now - cachedAll.time) < 60000) { + return cachedAll.resp + } else { + let resp = await fetchEx(apidexUrl(apidex_service, `/api/v1/cmc`), request) + resp = await resp.json() + cachedAll = { + resp, time: now + } + return resp + } + } catch (err) { + console.error('apidexGetAll', err) + return empty + } +} diff --git a/src/utils/State.js b/src/utils/misc.js similarity index 90% rename from src/utils/State.js rename to src/utils/misc.js index 350396b..4797f65 100644 --- a/src/utils/State.js +++ b/src/utils/misc.js @@ -1,3 +1,7 @@ +export async function delay(msec) { + await new Promise(resolve => setTimeout(resolve, msec)) +} + export const toAsset = (value) => { const [ amount, symbol ] = value.split(' ') return { amount: parseFloat(amount), symbol }