Merge pull request #1763 from RafaelTaranto/backport/market-currency-selector

LAM-551 backport: market currency selector
This commit is contained in:
Rafael Taranto 2024-11-29 13:48:38 +00:00 committed by GitHub
commit b2a28d4fa9
28 changed files with 725 additions and 350 deletions

View file

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

View file

@ -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,

View file

@ -0,0 +1,9 @@
const exchange = require('../../../exchange')
const resolvers = {
Query: {
getMarkets: () => exchange.getMarkets()
}
}
module.exports = resolvers

View file

@ -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,

View file

@ -0,0 +1,9 @@
const { gql } = require('apollo-server-express')
const typeDef = gql`
type Query {
getMarkets: JSONObject @auth
}
`
module.exports = typeDef

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()
}

View file

@ -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 = (
<Box
width={18}
height={18}
borderRadius={6}
bgcolor={warningColors[props.warning]}
/>
)
return (
<Box
width="100%"
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center">
<Box>{R.path([labelProp])(props)}</Box>
<HoverableTooltip parentElements={hoverableElement} width={250}>
<P>{props.warningMessage}</P>
</HoverableTooltip>
</Box>
)
}}
/>
)
}

View file

@ -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 (
<div className={classes.wrapper}>
<TitleSection title="Third-Party services" />
<Grid container spacing={4}>
{R.values(schemas).map(schema => (
<Grid item key={schema.code}>
<SingleRowTable
editMessage={'Configure ' + schema.title}
title={schema.title}
onEdit={() => setEditingSchema(schema)}
items={getItems(schema.code, schema.elements)}
!loading && (
<div className={classes.wrapper}>
<TitleSection title="Third-Party services" />
<Grid container spacing={4}>
{R.values(schemas).map(schema => (
<Grid item key={schema.code}>
<SingleRowTable
editMessage={'Configure ' + schema.title}
title={schema.title}
onEdit={() => setEditingSchema(schema)}
items={getItems(schema.code, schema.elements)}
/>
</Grid>
))}
</Grid>
{editingSchema && (
<Modal
title={`Edit ${editingSchema.name}`}
width={525}
handleClose={() => setEditingSchema(null)}
open={true}>
<FormRenderer
save={it =>
saveAccount({
variables: { accounts: { [editingSchema.code]: it } }
})
}
elements={getElements(editingSchema)}
validationSchema={getValidationSchema(editingSchema)}
value={getAccounts(editingSchema)}
/>
</Grid>
))}
</Grid>
{editingSchema && (
<Modal
title={`Edit ${editingSchema.name}`}
width={525}
handleClose={() => setEditingSchema(null)}
open={true}>
<FormRenderer
save={it =>
saveAccount({
variables: { accounts: { [editingSchema.code]: it } }
})
}
elements={getElements(editingSchema)}
validationSchema={getValidationSchema(editingSchema)}
value={getAccounts(editingSchema)}
/>
</Modal>
)}
</div>
</Modal>
)}
</div>
)
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,