From 9ae08196d31c878fea158d4e396fc9cc15c21b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 13 Jul 2022 15:55:45 +0100 Subject: [PATCH 1/5] fix: improve HoverableTooltip behavior and UX feat: add market currency selector for exchange 3rd party services --- lib/exchange.js | 23 +++- lib/new-admin/graphql/resolvers/index.js | 2 + .../graphql/resolvers/market.resolver.js | 9 ++ lib/new-admin/graphql/types/index.js | 2 + lib/new-admin/graphql/types/market.type.js | 9 ++ lib/plugins/common/ccxt.js | 10 +- lib/plugins/exchange/binance.js | 2 +- lib/plugins/exchange/binanceus.js | 2 +- lib/plugins/exchange/bitstamp.js | 2 +- lib/plugins/exchange/ccxt.js | 38 +++++- lib/plugins/exchange/cex.js | 2 +- lib/plugins/exchange/itbit.js | 2 +- lib/plugins/exchange/kraken.js | 2 +- .../components/inputs/base/Autocomplete.js | 38 ++++++ .../src/pages/Services/Services.js | 83 +++++++------ .../src/pages/Services/schemas/binance.js | 79 +++++++----- .../src/pages/Services/schemas/binanceus.js | 79 +++++++----- .../src/pages/Services/schemas/bitfinex.js | 79 +++++++----- .../src/pages/Services/schemas/bitstamp.js | 99 +++++++++------ .../src/pages/Services/schemas/cex.js | 99 +++++++++------ .../src/pages/Services/schemas/helper.js | 31 ++++- .../src/pages/Services/schemas/index.js | 68 ++++++----- .../src/pages/Services/schemas/itbit.js | 115 +++++++++++------- .../src/pages/Services/schemas/kraken.js | 79 +++++++----- new-lamassu-admin/src/styling/variables.js | 4 + new-lamassu-admin/src/utils/constants.js | 8 +- 26 files changed, 646 insertions(+), 320 deletions(-) create mode 100644 lib/new-admin/graphql/resolvers/market.resolver.js create mode 100644 lib/new-admin/graphql/types/market.type.js diff --git a/lib/exchange.js b/lib/exchange.js index 0431a7d5d..94083882e 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,25 @@ 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 = { buy, sell, - active + active, + getMarkets } diff --git a/lib/new-admin/graphql/resolvers/index.js b/lib/new-admin/graphql/resolvers/index.js index a20d92163..ea3cb3fa7 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 000000000..498644179 --- /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 f4794b673..e33c50b5c 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 000000000..2413a9fe5 --- /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/common/ccxt.js b/lib/plugins/common/ccxt.js index db98b4607..b822bb707 100644 --- a/lib/plugins/common/ccxt.js +++ b/lib/plugins/common/ccxt.js @@ -29,15 +29,13 @@ const ALL = { bitfinex: bitfinex } -function buildMarket (fiatCode, cryptoCode, serviceName) { +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)) logger.debug('Missing fiat code information, defaulting to EUR markets') + const fiatCode = _fiatCode ?? 'EUR' return cryptoCode + '/' + fiatCode } diff --git a/lib/plugins/exchange/binance.js b/lib/plugins/exchange/binance.js index 8a45723cb..97c90c263 100644 --- a/lib/plugins/exchange/binance.js +++ b/lib/plugins/exchange/binance.js @@ -7,7 +7,7 @@ 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 REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { diff --git a/lib/plugins/exchange/binanceus.js b/lib/plugins/exchange/binanceus.js index ecf058b6d..59290fc6c 100644 --- a/lib/plugins/exchange/binanceus.js +++ b/lib/plugins/exchange/binanceus.js @@ -7,7 +7,7 @@ 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 REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { diff --git a/lib/plugins/exchange/bitstamp.js b/lib/plugins/exchange/bitstamp.js index 5494ff1c4..859ca7433 100644 --- a/lib/plugins/exchange/bitstamp.js +++ b/lib/plugins/exchange/bitstamp.js @@ -8,7 +8,7 @@ const { BTC, ETH, LTC, BCH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN] const FIAT = ['USD', '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 = { diff --git a/lib/plugins/exchange/ccxt.js b/lib/plugins/exchange/ccxt.js index 5de324f56..85e353cf0 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,33 @@ 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 currencyCodes = _.map(it => it.code, currencies) + + 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 (_.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 525eb427e..94a178111 100644 --- a/lib/plugins/exchange/cex.js +++ b/lib/plugins/exchange/cex.js @@ -7,7 +7,7 @@ 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 REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { diff --git a/lib/plugins/exchange/itbit.js b/lib/plugins/exchange/itbit.js index 02572335b..8bf4ada52 100644 --- a/lib/plugins/exchange/itbit.js +++ b/lib/plugins/exchange/itbit.js @@ -8,7 +8,7 @@ const { BTC, ETH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, USDT, LN] const FIAT = ['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 = { diff --git a/lib/plugins/exchange/kraken.js b/lib/plugins/exchange/kraken.js index 849af0e59..f14408bba 100644 --- a/lib/plugins/exchange/kraken.js +++ b/lib/plugins/exchange/kraken.js @@ -8,7 +8,7 @@ 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 AMOUNT_PRECISION = 6 -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const USER_REF = 'userref' const loadConfig = (account) => { diff --git a/new-lamassu-admin/src/components/inputs/base/Autocomplete.js b/new-lamassu-admin/src/components/inputs/base/Autocomplete.js index 996fb909f..e5f9b941c 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 72eab97b4..c1d5b408c 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 6be4be268..faec0e351 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 7afd724b4..74795e24a 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 0609807a1..c0485af1e 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 431fcfb55..e9061e9ea 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 f8374c6f8..b887db93d 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 ccb49a79f..a82c28215 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/helper.js +++ b/new-lamassu-admin/src/pages/Services/schemas/helper.js @@ -1,5 +1,8 @@ +import { ALL_CRYPTOS } from '@lamassu/coins' import * as R from 'ramda' +import { WARNING_LEVELS } from 'src/utils/constants' + const secretTest = (secret, message) => ({ name: 'secret-test', message: message ? `The ${message} is invalid` : 'Invalid field', @@ -21,4 +24,30 @@ 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 unavailableMarkets = R.join( + ', ', + R.map(ite => `${ite}/${it}`, unavailableCryptos) + ) + + const warningLevel = R.isEmpty(unavailableCryptos) + ? WARNING_LEVELS.CLEAN + : !R.isEmpty(unavailableCryptos) && + R.length(unavailableCryptos) < R.length(ALL_CRYPTOS) + ? WARNING_LEVELS.PARTIAL + : WARNING_LEVELS.IMPORTANT + + return { + code: R.toUpper(it), + display: R.toUpper(it), + warning: warningLevel, + warningMessage: !R.isEmpty(unavailableMarkets) + ? `No market pairs available for ${unavailableMarkets}` + : `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 22368537d..e952771b7 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 949ba6921..d66074613 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 733cebe4a..2c0ee271f 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 2cb84f7f9..632892230 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, diff --git a/new-lamassu-admin/src/utils/constants.js b/new-lamassu-admin/src/utils/constants.js index 9d829d3c6..f5516b995 100644 --- a/new-lamassu-admin/src/utils/constants.js +++ b/new-lamassu-admin/src/utils/constants.js @@ -9,6 +9,11 @@ const MANUAL = 'manual' const IP_CHECK_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ const SWEEPABLE_CRYPTOS = ['ETH'] +const WARNING_LEVELS = { + CLEAN: 'clean', + PARTIAL: 'partial', + IMPORTANT: 'important' +} export { CURRENCY_MAX, @@ -18,5 +23,6 @@ export { MANUAL, WALLET_SCORING_DEFAULT_THRESHOLD, IP_CHECK_REGEX, - SWEEPABLE_CRYPTOS + SWEEPABLE_CRYPTOS, + WARNING_LEVELS } From c7c6219e5230f4138994cf90d6c95fde9915a253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Thu, 14 Jul 2022 15:43:27 +0100 Subject: [PATCH 2/5] feat: add currency market migration --- migrations/1732874039534-market-currency.js | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 migrations/1732874039534-market-currency.js diff --git a/migrations/1732874039534-market-currency.js b/migrations/1732874039534-market-currency.js new file mode 100644 index 000000000..5f0b71356 --- /dev/null +++ b/migrations/1732874039534-market-currency.js @@ -0,0 +1,31 @@ +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(({ config, accounts }) => { + const allExchanges = _.map(it => it.code)(_.filter(it => it.class === 'exchange', ACCOUNT_LIST)) + const configuredExchanges = _.intersection(allExchanges, _.keys(accounts)) + const localeCurrency = config.locale_fiatCurrency + + const newAccounts = _.reduce( + (acc, value) => { + if (!_.isNil(accounts[value].currencyMarket)) return acc + if (_.includes(localeCurrency, ALL[value].FIAT)) return { ...acc, [value]: { currencyMarket: localeCurrency } } + return { ...acc, [value]: { currencyMarket: _.head(ALL[value].FIAT) } } + }, + {}, + configuredExchanges + ) + + return saveAccounts(newAccounts) + }) + .then(next) + .catch(next) +} + +module.exports.down = function (next) { + next() +} From fc3f83c22d712ab10ba36a844dbafa45e6d2678e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Thu, 1 Sep 2022 17:03:28 +0100 Subject: [PATCH 3/5] feat: add USDT as a trading market --- lib/plugins/exchange/ccxt.js | 5 +++- .../src/pages/Services/schemas/helper.js | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/plugins/exchange/ccxt.js b/lib/plugins/exchange/ccxt.js index 85e353cf0..63b57fa94 100644 --- a/lib/plugins/exchange/ccxt.js +++ b/lib/plugins/exchange/ccxt.js @@ -58,13 +58,16 @@ function calculatePrice (side, amount, orderBook) { function _getMarkets (exchangeName, availableCryptos) { try { const exchange = new ccxt[exchangeName]() - const currencyCodes = _.map(it => it.code, currencies) + 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] } } diff --git a/new-lamassu-admin/src/pages/Services/schemas/helper.js b/new-lamassu-admin/src/pages/Services/schemas/helper.js index a82c28215..7d71e79f6 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/helper.js +++ b/new-lamassu-admin/src/pages/Services/schemas/helper.js @@ -27,15 +27,20 @@ const leadingZerosTest = (value, context) => { const buildCurrencyOptions = markets => { return R.map(it => { const unavailableCryptos = R.difference(ALL_CRYPTOS, markets[it]) - const unavailableMarkets = R.join( - ', ', - R.map(ite => `${ite}/${it}`, unavailableCryptos) - ) + 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 warningLevel = R.isEmpty(unavailableCryptos) + 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(unavailableCryptos) && - R.length(unavailableCryptos) < R.length(ALL_CRYPTOS) + : !R.isEmpty(unavailableCryptosFiltered) && + R.length(unavailableCryptosFiltered) < R.length(ALL_CRYPTOS) ? WARNING_LEVELS.PARTIAL : WARNING_LEVELS.IMPORTANT @@ -43,8 +48,8 @@ const buildCurrencyOptions = markets => { code: R.toUpper(it), display: R.toUpper(it), warning: warningLevel, - warningMessage: !R.isEmpty(unavailableMarkets) - ? `No market pairs available for ${unavailableMarkets}` + warningMessage: !R.isEmpty(unavailableCryptosFiltered) + ? `No market pairs available for ${unavailableMarketsStr}` : `All market pairs are available` } }, R.keys(markets)) From f1a798a4e1f3eccddc45c84617d3f98bfd7ee36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 7 Oct 2022 02:02:11 +0100 Subject: [PATCH 4/5] fix: market currency migration --- lib/plugins/common/ccxt.js | 5 ++--- lib/plugins/exchange/binance.js | 3 ++- lib/plugins/exchange/binanceus.js | 3 ++- lib/plugins/exchange/bitfinex.js | 3 ++- lib/plugins/exchange/bitstamp.js | 3 ++- lib/plugins/exchange/cex.js | 3 ++- lib/plugins/exchange/itbit.js | 3 ++- lib/plugins/exchange/kraken.js | 3 ++- migrations/1732874039534-market-currency.js | 7 +++---- new-lamassu-admin/src/pages/Services/schemas/helper.js | 6 +++++- new-lamassu-admin/src/utils/constants.js | 8 +------- 11 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/plugins/common/ccxt.js b/lib/plugins/common/ccxt.js index b822bb707..1acdaa954 100644 --- a/lib/plugins/common/ccxt.js +++ b/lib/plugins/common/ccxt.js @@ -29,13 +29,12 @@ const ALL = { bitfinex: bitfinex } -function buildMarket (_fiatCode, cryptoCode, serviceName) { +function buildMarket (fiatCode, cryptoCode, serviceName) { if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) { throw new Error('Unsupported crypto: ' + cryptoCode) } - if (_.isNil(_fiatCode)) logger.debug('Missing fiat code information, defaulting to EUR markets') - const fiatCode = _fiatCode ?? '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 97c90c263..47c498e7b 100644 --- a/lib/plugins/exchange/binance.js +++ b/lib/plugins/exchange/binance.js @@ -7,6 +7,7 @@ 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 DEFAULT_FIAT_MARKET = 'EUR' const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { @@ -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 59290fc6c..e8f0c3711 100644 --- a/lib/plugins/exchange/binanceus.js +++ b/lib/plugins/exchange/binanceus.js @@ -7,6 +7,7 @@ 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 DEFAULT_FIAT_MARKET = 'USD' const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { @@ -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 4feccb0cc..4e4d85ce5 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 859ca7433..bd745d49d 100644 --- a/lib/plugins/exchange/bitstamp.js +++ b/lib/plugins/exchange/bitstamp.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', 'clientId', 'currencyMarket'] @@ -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/cex.js b/lib/plugins/exchange/cex.js index 94a178111..b9687e151 100644 --- a/lib/plugins/exchange/cex.js +++ b/lib/plugins/exchange/cex.js @@ -7,6 +7,7 @@ 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 DEFAULT_FIAT_MARKET = 'EUR' const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { @@ -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 8bf4ada52..d80268e12 100644 --- a/lib/plugins/exchange/itbit.js +++ b/lib/plugins/exchange/itbit.js @@ -7,6 +7,7 @@ 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', 'currencyMarket'] @@ -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 f14408bba..0f050ccf0 100644 --- a/lib/plugins/exchange/kraken.js +++ b/lib/plugins/exchange/kraken.js @@ -7,6 +7,7 @@ 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', 'currencyMarket'] const USER_REF = 'userref' @@ -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 index 5f0b71356..359db4bd6 100644 --- a/migrations/1732874039534-market-currency.js +++ b/migrations/1732874039534-market-currency.js @@ -5,16 +5,15 @@ const { ALL } = require('../lib/plugins/common/ccxt') exports.up = function (next) { return loadLatest() - .then(({ config, accounts }) => { + .then(({ accounts }) => { const allExchanges = _.map(it => it.code)(_.filter(it => it.class === 'exchange', ACCOUNT_LIST)) const configuredExchanges = _.intersection(allExchanges, _.keys(accounts)) - const localeCurrency = config.locale_fiatCurrency const newAccounts = _.reduce( (acc, value) => { if (!_.isNil(accounts[value].currencyMarket)) return acc - if (_.includes(localeCurrency, ALL[value].FIAT)) return { ...acc, [value]: { currencyMarket: localeCurrency } } - return { ...acc, [value]: { currencyMarket: _.head(ALL[value].FIAT) } } + if (_.includes('EUR', ALL[value].FIAT)) return { ...acc, [value]: { currencyMarket: 'EUR' } } + return { ...acc, [value]: { currencyMarket: ALL[value].DEFAULT_FIAT_CURRENCY } } }, {}, configuredExchanges diff --git a/new-lamassu-admin/src/pages/Services/schemas/helper.js b/new-lamassu-admin/src/pages/Services/schemas/helper.js index 7d71e79f6..c1c97870c 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/helper.js +++ b/new-lamassu-admin/src/pages/Services/schemas/helper.js @@ -1,7 +1,11 @@ import { ALL_CRYPTOS } from '@lamassu/coins' import * as R from 'ramda' -import { WARNING_LEVELS } from 'src/utils/constants' +const WARNING_LEVELS = { + CLEAN: 'clean', + PARTIAL: 'partial', + IMPORTANT: 'important' +} const secretTest = (secret, message) => ({ name: 'secret-test', diff --git a/new-lamassu-admin/src/utils/constants.js b/new-lamassu-admin/src/utils/constants.js index f5516b995..9d829d3c6 100644 --- a/new-lamassu-admin/src/utils/constants.js +++ b/new-lamassu-admin/src/utils/constants.js @@ -9,11 +9,6 @@ const MANUAL = 'manual' const IP_CHECK_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ const SWEEPABLE_CRYPTOS = ['ETH'] -const WARNING_LEVELS = { - CLEAN: 'clean', - PARTIAL: 'partial', - IMPORTANT: 'important' -} export { CURRENCY_MAX, @@ -23,6 +18,5 @@ export { MANUAL, WALLET_SCORING_DEFAULT_THRESHOLD, IP_CHECK_REGEX, - SWEEPABLE_CRYPTOS, - WARNING_LEVELS + SWEEPABLE_CRYPTOS } From ab9e75480d6da56a229afca941d1f0a4303b1ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 31 Oct 2022 18:30:14 +0000 Subject: [PATCH 5/5] fix: use selected fiat currency on exchange to store queued trades on the database --- lib/exchange.js | 1 + lib/plugins.js | 62 +++++++++++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/lib/exchange.js b/lib/exchange.js index 94083882e..f9811bb8a 100644 --- a/lib/exchange.js +++ b/lib/exchange.js @@ -66,6 +66,7 @@ function getMarkets () { } module.exports = { + fetchExchange, buy, sell, active, diff --git a/lib/plugins.js b/lib/plugins.js index d5bfcb4fe..157c67ed9 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() - - const market = [fiatCode, cryptoCode].join('') - - 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() - }) + 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('') + + 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() + }) + }) } 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) }