From 06a6dd7a0dc2eb631aa4bcc4d3e4a9370f090f7f Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Sun, 29 Oct 2023 02:12:57 +0000 Subject: [PATCH] UIA register --- config/default.json | 7 +- db/app.lua | 2 + db/reg_pollers.lua | 63 ++++++++++ public/icons/copy.svg | 3 + public/icons/copy_ok.svg | 22 ++++ src/locales/en.json | 2 + src/locales/ru-RU.json | 2 + src/modules/register/UIARegister.jsx | 165 ++++++--------------------- src/pages/api/reg/[...all].js | 144 ++++++++++++++++++++--- src/server/reg.js | 3 + src/utils/ApidexApiClient.js | 84 ++++++++++++++ 11 files changed, 348 insertions(+), 149 deletions(-) create mode 100644 db/reg_pollers.lua create mode 100644 public/icons/copy.svg create mode 100644 public/icons/copy_ok.svg create mode 100644 src/utils/ApidexApiClient.js 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..c0a41cf --- /dev/null +++ b/db/reg_pollers.lua @@ -0,0 +1,63 @@ +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 + }) +end + +function get_free_reg_poller(amount, sym) + local rps = nil + repeat + if rps ~= nil then + amount = amount + 1 + end + rps = box.space.reg_pollers.index.by_amount:select{sym, amount} + until #rps == 0 + return amount +end + +function upsert_reg_poller(amount, sym, uid, init_bal) + local now = fiber.clock() + local rps = box.space.reg_pollers.index.by_amount:select{sym, amount} + if #rps ~= 0 then + local rp = rps[1] + if rp['uid'] ~= uid then + return { err = 'Someone already waits for such transfer.', res = nil } + end + if (now - rp['created']) >= 15*60 then + box.space.reg_pollers:delete(rp['id']) + else + return { err = nil, res = rp } + end + end + local rp = box.space.reg_pollers:insert(nil, sym, amount, uid, now, init_bal) + return { err = nil, res = 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..ff84c8b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -98,6 +98,8 @@ "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", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index b6b56eb..ba646cb 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -98,6 +98,8 @@ "register_with": "Регистрация с помощью...", "no_such_asset": " - такого UIA не существует.", "deposit_unavailable": " - депозит временно недоступен.", + "cmc_error": " - не удается получить минимальную сумму с CoinMarketCap.", + "transfer_not_supported": " - пока не поддерживается.", "min_amount": "Минимальная сумма", "fee": "Комиссия", "memo_fixed": "Заметка/memo", diff --git a/src/modules/register/UIARegister.jsx b/src/modules/register/UIARegister.jsx index e702cbb..91e284e 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,6 +11,7 @@ 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 { emptyAuthority } from '@/utils/RecoveryUtils' import { callApi, } from '@/utils/RegApiClient' @@ -39,14 +40,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 +61,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 +77,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 @@ -98,21 +93,35 @@ class UIARegister extends React.Component { const { to_type, to_api, } = deposit 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 } this.setState({ - transferState: TransferState.initial, rules: { ...deposit, creator: asset.creator, telegram }, + minAmount, registrar, sym, copied_addr: false, @@ -222,55 +231,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) @@ -298,13 +258,16 @@ class UIARegister extends React.Component { if (memo_fixed) { details = details.split('').join(username) } + let minAmount = parseFloat(min_amount) + const minAmountForUsd = parseFloat(this.state.minAmount.floatString) + minAmount = Math.max(minAmount, minAmountForUsd) return

{details &&
{details}

} - {min_amount &&
- {tt('uia_register_jsx.min_amount')} {min_amount} {sym || ''}
} + {minAmount &&
+ {tt('uia_register_jsx.min_amount')} {minAmount} {sym || ''}
} {fee &&
{tt('uia_register_jsx.fee') + ': '}{fee} {sym || ''}
}
; @@ -351,60 +314,6 @@ 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 if (!onTransfer) { @@ -465,12 +374,10 @@ class UIARegister extends React.Component { } else { const { rules, registrar, } = this.state - const { to, to_type, to_fixed, to_transfer, + const { to, to_type, to_fixed, min_amount, fee, 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..e79e682 100644 --- a/src/pages/api/reg/[...all].js +++ b/src/pages/api/reg/[...all].js @@ -16,7 +16,7 @@ import Tarantool from '@/server/tarantool'; initGolos(); -global.pollIntervals = {} +global.pollers = {} let handler = nextConnect({ attachParams: true, }) .use(regSessionMiddleware) @@ -307,24 +307,80 @@ 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) => { + const amountStr = req.params.amount.split('%20').join(' ') + let amount + try { + amount = 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 + ) + freeAmount = freeAmount[0][0] + } catch (err) { + console.error('ERROR: cannot logout all server tokens', err); + } + + if (freeAmount) { + const ret = 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) => { + const amountStr = req.params.amount + const amount = Asset(amountStr) + + const now = Date.now() + 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 poller = global.pollers[amountStr] + if (poller) { + if (poller.uid !== uid) { + throwErr(req, 400, ['Someone already waits for the transfer with ' + amountStr]) + } + if ((now - poller.created) > 15*60*1000) { + delete global.pollers[amountStr] + poller = null + } + } 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) } 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.amount = 0 @@ -333,18 +389,29 @@ let handler = nextConnect({ attachParams: true, }) } const stop = () => { - if (global.pollIntervals[sym]) { - clearInterval(global.pollIntervals[sym].interval) - delete global.pollIntervals[sym] + if (global.pollers[amountStr]) { + delete global.pollers[amountStr] } } - const initBal = await getBalance() + if (!poller) { + let initBal + try { + initBal = await getBalance() + } catch (err) { + throwErr(req, 400, ['Blockchain unavailable']) + } + + global.pollers[amountStr] = { + created: now, + initBal, + uid + } + } 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 +420,58 @@ let handler = nextConnect({ attachParams: true, }) } ++tries - const bal = await getBalance() + let bal + try { + bal = await getBalance() + } catch (err) { + throwErr(req, 400, ['Blockchain unavailable']) + } + console.log('wait_for_transfer', initBal.toString(), bal.toString()) - if (bal.amount > initBal.amount) { + const delta = bal.amount.minus(initBal.amount) + if (delta.gte(amount)) { + let stopMe = false + + let hist + try { + hist = await api.getAccountHistoryAsync(username, -1, 1000, {select_ops: ['transfer']}) + } catch (err) { + throwErr(req, 400, ['Blockchain unavailable']) + } + + const created = global.pollers[amountStr].created + for (let i = hist.length - 1; i >= 0; --i) { + const timestamp = +new Date(hist[i][1].timestamp) + 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 (!req.session.deposited) { + req.session.deposited = {} + } + req.session.deposited[amount.symbol] = delta.toString() + await req.session.save() + stop() - const delta = Asset(bal.amount - initBal.amount, bal.precision, bal.symbol) + res.json({ status: 'ok', - delta: delta.toString() + deposited: amountStr }) + return } - }, pollMsec) } + + await new Promise(resolve => setTimeout(resolve, pollMsec)) + } }) handler = addModalRoutes(handler); 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 + } +}