Skip to content

Commit

Permalink
Merge pull request #1763 from RafaelTaranto/backport/market-currency-…
Browse files Browse the repository at this point in the history
…selector

LAM-551 backport: market currency selector
  • Loading branch information
RafaelTaranto authored Nov 29, 2024
2 parents 7e80193 + ab9e754 commit 0471d34
Show file tree
Hide file tree
Showing 28 changed files with 728 additions and 353 deletions.
24 changes: 23 additions & 1 deletion lib/exchange.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions lib/new-admin/graphql/resolvers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -35,6 +36,7 @@ const resolvers = [
log,
loyalty,
machine,
market,
notification,
pairing,
rates,
Expand Down
9 changes: 9 additions & 0 deletions lib/new-admin/graphql/resolvers/market.resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const exchange = require('../../../exchange')

const resolvers = {
Query: {
getMarkets: () => exchange.getMarkets()
}
}

module.exports = resolvers
2 changes: 2 additions & 0 deletions lib/new-admin/graphql/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -35,6 +36,7 @@ const types = [
log,
loyalty,
machine,
market,
notification,
pairing,
rates,
Expand Down
9 changes: 9 additions & 0 deletions lib/new-admin/graphql/types/market.type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const { gql } = require('apollo-server-express')

const typeDef = gql`
type Query {
getMarkets: JSONObject @auth
}
`

module.exports = typeDef
62 changes: 34 additions & 28 deletions lib/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
7 changes: 2 additions & 5 deletions lib/plugins/common/ccxt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
5 changes: 3 additions & 2 deletions lib/plugins/exchange/binance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 }
5 changes: 3 additions & 2 deletions lib/plugins/exchange/binanceus.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 }
3 changes: 2 additions & 1 deletion lib/plugins/exchange/bitfinex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand All @@ -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 }
5 changes: 3 additions & 2 deletions lib/plugins/exchange/bitstamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 }
41 changes: 39 additions & 2 deletions lib/plugins/exchange/ccxt.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) : {}
Expand Down Expand Up @@ -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 }
5 changes: 3 additions & 2 deletions lib/plugins/exchange/cex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 }
5 changes: 3 additions & 2 deletions lib/plugins/exchange/itbit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 }
5 changes: 3 additions & 2 deletions lib/plugins/exchange/kraken.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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 }
30 changes: 30 additions & 0 deletions migrations/1732874039534-market-currency.js
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit 0471d34

Please sign in to comment.