diff --git a/lib/exchange.js b/lib/exchange.js index 0431a7d5..f9811bb8 100644 --- a/lib/exchange.js +++ b/lib/exchange.js @@ -1,6 +1,10 @@ +const _ = require('lodash/fp') +const { ALL_CRYPTOS } = require('@lamassu/coins') + const configManager = require('./new-config-manager') const ccxt = require('./plugins/exchange/ccxt') const mockExchange = require('./plugins/exchange/mock-exchange') +const accounts = require('./new-admin/config/accounts') function lookupExchange (settings, cryptoCode) { const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange @@ -45,8 +49,26 @@ function active (settings, cryptoCode) { return !!lookupExchange(settings, cryptoCode) } +function getMarkets () { + const filterExchanges = _.filter(it => it.class === 'exchange') + const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST)) + + return _.reduce( + (acc, value) => + Promise.all([acc, ccxt.getMarkets(value, ALL_CRYPTOS)]) + .then(([a, markets]) => Promise.resolve({ + ...a, + [value]: markets + })), + Promise.resolve({}), + availableExchanges + ) +} + module.exports = { + fetchExchange, buy, sell, - active + active, + getMarkets } diff --git a/lib/new-admin/graphql/resolvers/index.js b/lib/new-admin/graphql/resolvers/index.js index a20d9216..ea3cb3fa 100644 --- a/lib/new-admin/graphql/resolvers/index.js +++ b/lib/new-admin/graphql/resolvers/index.js @@ -11,6 +11,7 @@ const funding = require('./funding.resolver') const log = require('./log.resolver') const loyalty = require('./loyalty.resolver') const machine = require('./machine.resolver') +const market = require('./market.resolver') const notification = require('./notification.resolver') const pairing = require('./pairing.resolver') const rates = require('./rates.resolver') @@ -35,6 +36,7 @@ const resolvers = [ log, loyalty, machine, + market, notification, pairing, rates, diff --git a/lib/new-admin/graphql/resolvers/market.resolver.js b/lib/new-admin/graphql/resolvers/market.resolver.js new file mode 100644 index 00000000..49864417 --- /dev/null +++ b/lib/new-admin/graphql/resolvers/market.resolver.js @@ -0,0 +1,9 @@ +const exchange = require('../../../exchange') + +const resolvers = { + Query: { + getMarkets: () => exchange.getMarkets() + } +} + +module.exports = resolvers diff --git a/lib/new-admin/graphql/types/index.js b/lib/new-admin/graphql/types/index.js index f4794b67..e33c50b5 100644 --- a/lib/new-admin/graphql/types/index.js +++ b/lib/new-admin/graphql/types/index.js @@ -11,6 +11,7 @@ const funding = require('./funding.type') const log = require('./log.type') const loyalty = require('./loyalty.type') const machine = require('./machine.type') +const market = require('./market.type') const notification = require('./notification.type') const pairing = require('./pairing.type') const rates = require('./rates.type') @@ -35,6 +36,7 @@ const types = [ log, loyalty, machine, + market, notification, pairing, rates, diff --git a/lib/new-admin/graphql/types/market.type.js b/lib/new-admin/graphql/types/market.type.js new file mode 100644 index 00000000..2413a9fe --- /dev/null +++ b/lib/new-admin/graphql/types/market.type.js @@ -0,0 +1,9 @@ +const { gql } = require('apollo-server-express') + +const typeDef = gql` + type Query { + getMarkets: JSONObject @auth + } +` + +module.exports = typeDef diff --git a/lib/plugins.js b/lib/plugins.js index d5bfcb4f..157c67ed 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -475,25 +475,28 @@ function plugins (settings, deviceId) { function buyAndSell (rec, doBuy, tx) { const cryptoCode = rec.cryptoCode - const fiatCode = rec.fiatCode - const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() + return exchange.fetchExchange(settings, cryptoCode) + .then(_exchange => { + const fiatCode = _exchange.account.currencyMarket + const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() - const market = [fiatCode, cryptoCode].join('') + const market = [fiatCode, cryptoCode].join('') - if (!exchange.active(settings, cryptoCode)) return + if (!exchange.active(settings, cryptoCode)) return - const direction = doBuy ? 'cashIn' : 'cashOut' - const internalTxId = tx ? tx.id : rec.id - logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) - if (!tradesQueues[market]) tradesQueues[market] = [] - tradesQueues[market].push({ - direction, - internalTxId, - fiatCode, - cryptoAtoms, - cryptoCode, - timestamp: Date.now() - }) + const direction = doBuy ? 'cashIn' : 'cashOut' + const internalTxId = tx ? tx.id : rec.id + logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) + if (!tradesQueues[market]) tradesQueues[market] = [] + tradesQueues[market].push({ + direction, + internalTxId, + fiatCode, + cryptoAtoms, + cryptoCode, + timestamp: Date.now() + }) + }) } function consolidateTrades (cryptoCode, fiatCode) { @@ -550,19 +553,22 @@ function plugins (settings, deviceId) { const deviceIds = devices.map(device => device.deviceId) const lists = deviceIds.map(deviceId => { const localeConfig = configManager.getLocale(deviceId, settings.config) - const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies - return cryptoCodes.map(cryptoCode => ({ - fiatCode, - cryptoCode + return Promise.all(cryptoCodes.map(cryptoCode => { + return exchange.fetchExchange(settings, cryptoCode) + .then(exchange => ({ + fiatCode: exchange.account.currencyMarket, + cryptoCode + })) })) }) - - const tradesPromises = _.uniq(_.flatten(lists)) - .map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)) - - return Promise.all(tradesPromises) + + return Promise.all(lists) + }) + .then(lists => { + return Promise.all(_.uniq(_.flatten(lists)) + .map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))) }) .catch(logger.error) } diff --git a/lib/plugins/common/ccxt.js b/lib/plugins/common/ccxt.js index db98b460..1acdaa95 100644 --- a/lib/plugins/common/ccxt.js +++ b/lib/plugins/common/ccxt.js @@ -33,11 +33,8 @@ function buildMarket (fiatCode, cryptoCode, serviceName) { if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) { throw new Error('Unsupported crypto: ' + cryptoCode) } - const fiatSupported = ALL[serviceName].FIAT - if (fiatSupported !== 'ALL_CURRENCIES' && !_.includes(fiatCode, fiatSupported)) { - logger.info('Building a market for an unsupported fiat. Defaulting to EUR market') - return cryptoCode + '/' + 'EUR' - } + + if (_.isNil(fiatCode)) throw new Error('Market pair building failed: Missing fiat code') return cryptoCode + '/' + fiatCode } diff --git a/lib/plugins/exchange/binance.js b/lib/plugins/exchange/binance.js index 8a45723c..47c498e7 100644 --- a/lib/plugins/exchange/binance.js +++ b/lib/plugins/exchange/binance.js @@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, BCH, XMR, ETH, LTC, ZEC, LN } = COINS const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN] const FIAT = ['USD', 'EUR'] -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const DEFAULT_FIAT_MARKET = 'EUR' +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -17,4 +18,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } diff --git a/lib/plugins/exchange/binanceus.js b/lib/plugins/exchange/binanceus.js index ecf058b6..e8f0c371 100644 --- a/lib/plugins/exchange/binanceus.js +++ b/lib/plugins/exchange/binanceus.js @@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN] const FIAT = ['USD'] -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const DEFAULT_FIAT_MARKET = 'USD' +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -17,4 +18,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } diff --git a/lib/plugins/exchange/bitfinex.js b/lib/plugins/exchange/bitfinex.js index 4feccb0c..4e4d85ce 100644 --- a/lib/plugins/exchange/bitfinex.js +++ b/lib/plugins/exchange/bitfinex.js @@ -7,6 +7,7 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, ETH, LTC, BCH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN] const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' const AMOUNT_PRECISION = 8 const REQUIRED_CONFIG_FIELDS = ['key', 'secret'] @@ -18,4 +19,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, DEFAULT_FIAT_MARKET, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/lib/plugins/exchange/bitstamp.js b/lib/plugins/exchange/bitstamp.js index 5494ff1c..bd745d49 100644 --- a/lib/plugins/exchange/bitstamp.js +++ b/lib/plugins/exchange/bitstamp.js @@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, ETH, LTC, BCH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN] const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' const AMOUNT_PRECISION = 8 -const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId'] +const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -19,4 +20,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/lib/plugins/exchange/ccxt.js b/lib/plugins/exchange/ccxt.js index 5de324f5..63b57fa9 100644 --- a/lib/plugins/exchange/ccxt.js +++ b/lib/plugins/exchange/ccxt.js @@ -1,9 +1,13 @@ const { utils: coinUtils } = require('@lamassu/coins') const _ = require('lodash/fp') const ccxt = require('ccxt') +const mem = require('mem') const { buildMarket, ALL, isConfigValid } = require('../common/ccxt') const { ORDER_TYPES } = require('./consts') +const logger = require('../../logger') +const { currencies } = require('../../new-admin/config') +const T = require('../../time') const DEFAULT_PRICE_PRECISION = 2 const DEFAULT_AMOUNT_PRECISION = 8 @@ -18,7 +22,8 @@ function trade (side, account, tradeEntry, exchangeName) { const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config') - const symbol = buildMarket(fiatCode, cryptoCode, exchangeName) + const selectedFiatMarket = account.currencyMarket + const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName) const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION) const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision) const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {} @@ -50,4 +55,36 @@ function calculatePrice (side, amount, orderBook) { throw new Error('Insufficient market depth') } -module.exports = { trade } +function _getMarkets (exchangeName, availableCryptos) { + try { + const exchange = new ccxt[exchangeName]() + const cryptosToQuoteAgainst = ['USDT'] + const currencyCodes = _.concat(_.map(it => it.code, currencies), cryptosToQuoteAgainst) + + return exchange.fetchMarkets() + .then(_.filter(it => (it.type === 'spot' || it.spot))) + .then(res => + _.reduce((acc, value) => { + if (_.includes(value.base, availableCryptos) && _.includes(value.quote, currencyCodes)) { + if (value.quote === value.base) return acc + + if (_.isNil(acc[value.quote])) { + return { ...acc, [value.quote]: [value.base] } + } + + acc[value.quote].push(value.base) + } + return acc + }, {}, res) + ) + } catch (e) { + logger.debug(`No CCXT exchange found for ${exchangeName}`) + } +} + +const getMarkets = mem(_getMarkets, { + maxAge: T.week, + cacheKey: (exchangeName, availableCryptos) => exchangeName +}) + +module.exports = { trade, getMarkets } diff --git a/lib/plugins/exchange/cex.js b/lib/plugins/exchange/cex.js index 525eb427..b9687e15 100644 --- a/lib/plugins/exchange/cex.js +++ b/lib/plugins/exchange/cex.js @@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN] const FIAT = ['USD', 'EUR'] -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const DEFAULT_FIAT_MARKET = 'EUR' +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -17,4 +18,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } diff --git a/lib/plugins/exchange/itbit.js b/lib/plugins/exchange/itbit.js index 02572335..d80268e1 100644 --- a/lib/plugins/exchange/itbit.js +++ b/lib/plugins/exchange/itbit.js @@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.LIMIT const { BTC, ETH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, USDT, LN] const FIAT = ['USD'] +const DEFAULT_FIAT_MARKET = 'USD' const AMOUNT_PRECISION = 4 -const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId'] +const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -21,4 +22,4 @@ const loadConfig = (account) => { } const loadOptions = ({ walletId }) => ({ walletId }) -module.exports = { loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/lib/plugins/exchange/kraken.js b/lib/plugins/exchange/kraken.js index 849af0e5..0f050ccf 100644 --- a/lib/plugins/exchange/kraken.js +++ b/lib/plugins/exchange/kraken.js @@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN] const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' const AMOUNT_PRECISION = 6 -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const USER_REF = 'userref' const loadConfig = (account) => { @@ -26,4 +27,4 @@ const loadConfig = (account) => { const loadOptions = () => ({ expiretm: '+60' }) -module.exports = { USER_REF, loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { USER_REF, loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/migrations/1732874039534-market-currency.js b/migrations/1732874039534-market-currency.js new file mode 100644 index 00000000..359db4bd --- /dev/null +++ b/migrations/1732874039534-market-currency.js @@ -0,0 +1,30 @@ +const _ = require('lodash/fp') +const { loadLatest, saveAccounts } = require('../lib/new-settings-loader') +const { ACCOUNT_LIST } = require('../lib/new-admin/config/accounts') +const { ALL } = require('../lib/plugins/common/ccxt') + +exports.up = function (next) { + return loadLatest() + .then(({ accounts }) => { + const allExchanges = _.map(it => it.code)(_.filter(it => it.class === 'exchange', ACCOUNT_LIST)) + const configuredExchanges = _.intersection(allExchanges, _.keys(accounts)) + + const newAccounts = _.reduce( + (acc, value) => { + if (!_.isNil(accounts[value].currencyMarket)) return acc + if (_.includes('EUR', ALL[value].FIAT)) return { ...acc, [value]: { currencyMarket: 'EUR' } } + return { ...acc, [value]: { currencyMarket: ALL[value].DEFAULT_FIAT_CURRENCY } } + }, + {}, + configuredExchanges + ) + + return saveAccounts(newAccounts) + }) + .then(next) + .catch(next) +} + +module.exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/src/components/inputs/base/Autocomplete.js b/new-lamassu-admin/src/components/inputs/base/Autocomplete.js index 996fb909..e5f9b941 100644 --- a/new-lamassu-admin/src/components/inputs/base/Autocomplete.js +++ b/new-lamassu-admin/src/components/inputs/base/Autocomplete.js @@ -1,8 +1,13 @@ +import { Box } from '@material-ui/core' import MAutocomplete from '@material-ui/lab/Autocomplete' import sort from 'match-sorter' import * as R from 'ramda' import React from 'react' +import { HoverableTooltip } from 'src/components/Tooltip' +import { P } from 'src/components/typography' +import { errorColor, orangeYellow, spring4 } from 'src/styling/variables' + import TextInput from './TextInput' const Autocomplete = ({ @@ -95,6 +100,39 @@ const Autocomplete = ({ /> ) }} + renderOption={props => { + if (!props.warning && !props.warningMessage) + return R.path([labelProp])(props) + + const warningColors = { + clean: spring4, + partial: orangeYellow, + important: errorColor + } + + const hoverableElement = ( + + ) + + return ( + + {R.path([labelProp])(props)} + +

{props.warningMessage}

+
+
+ ) + }} /> ) } diff --git a/new-lamassu-admin/src/pages/Services/Services.js b/new-lamassu-admin/src/pages/Services/Services.js index 72eab97b..c1d5b408 100644 --- a/new-lamassu-admin/src/pages/Services/Services.js +++ b/new-lamassu-admin/src/pages/Services/Services.js @@ -12,7 +12,7 @@ import SingleRowTable from 'src/components/single-row-table/SingleRowTable' import { formatLong } from 'src/utils/string' import FormRenderer from './FormRenderer' -import schemas from './schemas' +import _schemas from './schemas' const GET_INFO = gql` query getData { @@ -21,6 +21,12 @@ const GET_INFO = gql` } ` +const GET_MARKETS = gql` + query getMarkets { + getMarkets + } +` + const SAVE_ACCOUNT = gql` mutation Save($accounts: JSONObject) { saveAccounts(accounts: $accounts) @@ -40,12 +46,17 @@ const useStyles = makeStyles(styles) const Services = () => { const [editingSchema, setEditingSchema] = useState(null) - const { data } = useQuery(GET_INFO) + const { data, loading: configLoading } = useQuery(GET_INFO) + const { data: marketsData, loading: marketsLoading } = useQuery(GET_MARKETS) const [saveAccount] = useMutation(SAVE_ACCOUNT, { onCompleted: () => setEditingSchema(null), refetchQueries: ['getData'] }) + const markets = marketsData?.getMarkets + + const schemas = _schemas(markets) + const classes = useStyles() const accounts = data?.accounts ?? {} @@ -101,40 +112,44 @@ const Services = () => { const getValidationSchema = ({ code, getValidationSchema }) => getValidationSchema(accounts[code]) + const loading = marketsLoading || configLoading + return ( -
- - - {R.values(schemas).map(schema => ( - - setEditingSchema(schema)} - items={getItems(schema.code, schema.elements)} + !loading && ( +
+ + + {R.values(schemas).map(schema => ( + + setEditingSchema(schema)} + items={getItems(schema.code, schema.elements)} + /> + + ))} + + {editingSchema && ( + setEditingSchema(null)} + open={true}> + + saveAccount({ + variables: { accounts: { [editingSchema.code]: it } } + }) + } + elements={getElements(editingSchema)} + validationSchema={getValidationSchema(editingSchema)} + value={getAccounts(editingSchema)} /> - - ))} - - {editingSchema && ( - setEditingSchema(null)} - open={true}> - - saveAccount({ - variables: { accounts: { [editingSchema.code]: it } } - }) - } - elements={getElements(editingSchema)} - validationSchema={getValidationSchema(editingSchema)} - value={getAccounts(editingSchema)} - /> - - )} -
+ + )} +
+ ) ) } diff --git a/new-lamassu-admin/src/pages/Services/schemas/binance.js b/new-lamassu-admin/src/pages/Services/schemas/binance.js index 6be4be26..faec0e35 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/binance.js +++ b/new-lamassu-admin/src/pages/Services/schemas/binance.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'binance', - name: 'Binance', - title: 'Binance (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'binance', + name: 'Binance', + title: 'Binance (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/binanceus.js b/new-lamassu-admin/src/pages/Services/schemas/binanceus.js index 7afd724b..74795e24 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/binanceus.js +++ b/new-lamassu-admin/src/pages/Services/schemas/binanceus.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'binanceus', - name: 'Binance.us', - title: 'Binance.us (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'binanceus', + name: 'Binance.us', + title: 'Binance.us (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js b/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js index 0609807a..c0485af1 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js +++ b/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'bitfinex', - name: 'Bitfinex', - title: 'Bitfinex (Exchange)', - elements: [ - { - code: 'key', - display: 'API Key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'secret', - display: 'API Secret', - component: SecretInputFormik +const schema = markets => { + return { + code: 'bitfinex', + name: 'Bitfinex', + title: 'Bitfinex (Exchange)', + elements: [ + { + code: 'key', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'secret', + display: 'API secret', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency Market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + key: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + secret: Yup.string('The API secret must be a string') + .max(100, 'The API secret is too long') + .test(secretTest(account?.secret, 'API secret')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - key: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - secret: Yup.string('The API secret must be a string') - .max(100, 'The API secret is too long') - .test(secretTest(account?.secret, 'API secret')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js b/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js index 431fcfb5..e9061e9e 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js +++ b/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js @@ -1,46 +1,67 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'bitstamp', - name: 'Bitstamp', - title: 'Bitstamp (Exchange)', - elements: [ - { - code: 'clientId', - display: 'Client ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'key', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'secret', - display: 'API secret', - component: SecretInputFormik +const schema = markets => { + return { + code: 'bitstamp', + name: 'Bitstamp', + title: 'Bitstamp (Exchange)', + elements: [ + { + code: 'clientId', + display: 'Client ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'key', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'secret', + display: 'API secret', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + clientId: Yup.string('The client ID must be a string') + .max(100, 'The client ID is too long') + .required('The client ID is required'), + key: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + secret: Yup.string('The API secret must be a string') + .max(100, 'The API secret is too long') + .test(secretTest(account?.secret, 'API secret')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - clientId: Yup.string('The client ID must be a string') - .max(100, 'The client ID is too long') - .required('The client ID is required'), - key: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - secret: Yup.string('The API secret must be a string') - .max(100, 'The API secret is too long') - .test(secretTest(account?.secret, 'API secret')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/cex.js b/new-lamassu-admin/src/pages/Services/schemas/cex.js index f8374c6f..b887db93 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/cex.js +++ b/new-lamassu-admin/src/pages/Services/schemas/cex.js @@ -1,46 +1,67 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'cex', - name: 'CEX.IO', - title: 'CEX.IO (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'uid', - display: 'User ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'cex', + name: 'CEX.IO', + title: 'CEX.IO (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'uid', + display: 'User ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency Market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + uid: Yup.string('The User ID must be a string') + .max(100, 'The User ID is too long') + .required('The User ID is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - uid: Yup.string('The User ID must be a string') - .max(100, 'The User ID is too long') - .required('The User ID is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/helper.js b/new-lamassu-admin/src/pages/Services/schemas/helper.js index ccb49a79..c1c97870 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/helper.js +++ b/new-lamassu-admin/src/pages/Services/schemas/helper.js @@ -1,5 +1,12 @@ +import { ALL_CRYPTOS } from '@lamassu/coins' import * as R from 'ramda' +const WARNING_LEVELS = { + CLEAN: 'clean', + PARTIAL: 'partial', + IMPORTANT: 'important' +} + const secretTest = (secret, message) => ({ name: 'secret-test', message: message ? `The ${message} is invalid` : 'Invalid field', @@ -21,4 +28,35 @@ const leadingZerosTest = (value, context) => { return true } -export { secretTest, leadingZerosTest } +const buildCurrencyOptions = markets => { + return R.map(it => { + const unavailableCryptos = R.difference(ALL_CRYPTOS, markets[it]) + const unavailableCryptosFiltered = R.difference(unavailableCryptos, [it]) // As the markets can have stablecoins to trade against other crypto, filter them out, as there can't be pairs such as USDT/USDT + + const unavailableMarketsStr = + R.length(unavailableCryptosFiltered) > 1 + ? `${R.join( + ', ', + R.slice(0, -1, unavailableCryptosFiltered) + )} and ${R.last(unavailableCryptosFiltered)}` + : unavailableCryptosFiltered[0] + + const warningLevel = R.isEmpty(unavailableCryptosFiltered) + ? WARNING_LEVELS.CLEAN + : !R.isEmpty(unavailableCryptosFiltered) && + R.length(unavailableCryptosFiltered) < R.length(ALL_CRYPTOS) + ? WARNING_LEVELS.PARTIAL + : WARNING_LEVELS.IMPORTANT + + return { + code: R.toUpper(it), + display: R.toUpper(it), + warning: warningLevel, + warningMessage: !R.isEmpty(unavailableCryptosFiltered) + ? `No market pairs available for ${unavailableMarketsStr}` + : `All market pairs are available` + } + }, R.keys(markets)) +} + +export { secretTest, leadingZerosTest, buildCurrencyOptions } diff --git a/new-lamassu-admin/src/pages/Services/schemas/index.js b/new-lamassu-admin/src/pages/Services/schemas/index.js index 22368537..e952771b 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/index.js +++ b/new-lamassu-admin/src/pages/Services/schemas/index.js @@ -1,16 +1,16 @@ -import binance from './binance' -import binanceus from './binanceus' -import bitfinex from './bitfinex' +import _binance from './binance' +import _binanceus from './binanceus' +import _bitfinex from './bitfinex' import bitgo from './bitgo' -import bitstamp from './bitstamp' +import _bitstamp from './bitstamp' import blockcypher from './blockcypher' -import cex from './cex' +import _cex from './cex' import elliptic from './elliptic' import galoy from './galoy' import inforu from './inforu' import infura from './infura' -import itbit from './itbit' -import kraken from './kraken' +import _itbit from './itbit' +import _kraken from './kraken' import mailgun from './mailgun' import scorechain from './scorechain' import sumsub from './sumsub' @@ -19,25 +19,37 @@ import trongrid from './trongrid' import twilio from './twilio' import vonage from './vonage' -export default { - [bitgo.code]: bitgo, - [galoy.code]: galoy, - [bitstamp.code]: bitstamp, - [blockcypher.code]: blockcypher, - [elliptic.code]: elliptic, - [inforu.code]: inforu, - [infura.code]: infura, - [itbit.code]: itbit, - [kraken.code]: kraken, - [mailgun.code]: mailgun, - [telnyx.code]: telnyx, - [vonage.code]: vonage, - [twilio.code]: twilio, - [binanceus.code]: binanceus, - [cex.code]: cex, - [scorechain.code]: scorechain, - [trongrid.code]: trongrid, - [binance.code]: binance, - [bitfinex.code]: bitfinex, - [sumsub.code]: sumsub +const schemas = (markets = {}) => { + const binance = _binance(markets?.binance) + const bitfinex = _bitfinex(markets?.bitfinex) + const binanceus = _binanceus(markets?.binanceus) + const bitstamp = _bitstamp(markets?.bitstamp) + const cex = _cex(markets?.cex) + const itbit = _itbit(markets?.itbit) + const kraken = _kraken(markets?.kraken) + + return { + [bitgo.code]: bitgo, + [galoy.code]: galoy, + [bitstamp.code]: bitstamp, + [blockcypher.code]: blockcypher, + [elliptic.code]: elliptic, + [inforu.code]: inforu, + [infura.code]: infura, + [itbit.code]: itbit, + [kraken.code]: kraken, + [mailgun.code]: mailgun, + [telnyx.code]: telnyx, + [vonage.code]: vonage, + [twilio.code]: twilio, + [binanceus.code]: binanceus, + [cex.code]: cex, + [scorechain.code]: scorechain, + [trongrid.code]: trongrid, + [binance.code]: binance, + [bitfinex.code]: bitfinex, + [sumsub.code]: sumsub + } } + +export default schemas diff --git a/new-lamassu-admin/src/pages/Services/schemas/itbit.js b/new-lamassu-admin/src/pages/Services/schemas/itbit.js index 949ba692..d6607461 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/itbit.js +++ b/new-lamassu-admin/src/pages/Services/schemas/itbit.js @@ -1,54 +1,75 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { buildCurrencyOptions, secretTest } from './helper' -export default { - code: 'itbit', - name: 'itBit', - title: 'itBit (Exchange)', - elements: [ - { - code: 'userId', - display: 'User ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'walletId', - display: 'Wallet ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'clientKey', - display: 'Client key', - component: TextInputFormik - }, - { - code: 'clientSecret', - display: 'Client secret', - component: SecretInputFormik +const schema = markets => { + return { + code: 'itbit', + name: 'itBit', + title: 'itBit (Exchange)', + elements: [ + { + code: 'userId', + display: 'User ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'walletId', + display: 'Wallet ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'clientKey', + display: 'Client key', + component: TextInput + }, + { + code: 'clientSecret', + display: 'Client secret', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + userId: Yup.string('The user ID must be a string') + .max(100, 'The user ID is too long') + .required('The user ID is required'), + walletId: Yup.string('The wallet ID must be a string') + .max(100, 'The wallet ID is too long') + .required('The wallet ID is required'), + clientKey: Yup.string('The client key must be a string') + .max(100, 'The client key is too long') + .required('The client key is required'), + clientSecret: Yup.string('The client secret must be a string') + .max(100, 'The client secret is too long') + .test(secretTest(account?.clientSecret, 'client secret')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - userId: Yup.string('The user ID must be a string') - .max(100, 'The user ID is too long') - .required('The user ID is required'), - walletId: Yup.string('The wallet ID must be a string') - .max(100, 'The wallet ID is too long') - .required('The wallet ID is required'), - clientKey: Yup.string('The client key must be a string') - .max(100, 'The client key is too long') - .required('The client key is required'), - clientSecret: Yup.string('The client secret must be a string') - .max(100, 'The client secret is too long') - .test(secretTest(account?.clientSecret, 'client secret')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/kraken.js b/new-lamassu-admin/src/pages/Services/schemas/kraken.js index 733cebe4..2c0ee271 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/kraken.js +++ b/new-lamassu-admin/src/pages/Services/schemas/kraken.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'kraken', - name: 'Kraken', - title: 'Kraken (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'kraken', + name: 'Kraken', + title: 'Kraken (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/styling/variables.js b/new-lamassu-admin/src/styling/variables.js index 2cb84f7f..63289223 100644 --- a/new-lamassu-admin/src/styling/variables.js +++ b/new-lamassu-admin/src/styling/variables.js @@ -32,6 +32,9 @@ const mistyRose = '#ffeceb' const pumpkin = '#ff7311' const linen = '#fbf3ec' +// Warning +const orangeYellow = '#ffcc00' + // Color Variables const primaryColor = zodiac @@ -136,6 +139,7 @@ export { java, neon, linen, + orangeYellow, // named colors primaryColor, secondaryColor,