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 @@
+
+
+
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 }