WIP -- refactoring
This commit is contained in:
parent
8d44c1d383
commit
f55e9355ef
3 changed files with 84 additions and 82 deletions
141
lib/plugins.js
141
lib/plugins.js
|
|
@ -11,18 +11,10 @@ var argv = require('minimist')(process.argv.slice(2))
|
||||||
|
|
||||||
var tradeInterval = null
|
var tradeInterval = null
|
||||||
|
|
||||||
var SATOSHI_FACTOR = 1e8
|
|
||||||
var POLLING_RATE = 60 * 1000 // poll each minute
|
var POLLING_RATE = 60 * 1000 // poll each minute
|
||||||
var REAP_RATE = 2 * 1000
|
var REAP_RATE = 2 * 1000
|
||||||
var PENDING_TIMEOUT = 70 * 1000
|
var PENDING_TIMEOUT = 70 * 1000
|
||||||
|
|
||||||
var BTC_COIN = {
|
|
||||||
unitCode: 'BTC',
|
|
||||||
displayCode: 'mBTC',
|
|
||||||
unitScale: 8,
|
|
||||||
displayScale: 5
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv.timeout) PENDING_TIMEOUT = argv.timeout / 1000
|
if (argv.timeout) PENDING_TIMEOUT = argv.timeout / 1000
|
||||||
|
|
||||||
// TODO: might have to update this if user is allowed to extend monitoring time
|
// TODO: might have to update this if user is allowed to extend monitoring time
|
||||||
|
|
@ -30,10 +22,10 @@ var DEPOSIT_TIMEOUT = 130 * 1000
|
||||||
|
|
||||||
var db = null
|
var db = null
|
||||||
|
|
||||||
var cryptoCoins = null
|
var cryptoCodes = null
|
||||||
|
|
||||||
var tickerPlugins = {}
|
var tickerPlugins = {}
|
||||||
var traderPlugin = null
|
var traderPlugins = {}
|
||||||
var walletPlugins = {}
|
var walletPlugins = {}
|
||||||
var idVerifierPlugin = null
|
var idVerifierPlugin = null
|
||||||
var infoPlugin = null
|
var infoPlugin = null
|
||||||
|
|
@ -46,7 +38,7 @@ var deviceCurrency = 'USD'
|
||||||
var lastBalances = {}
|
var lastBalances = {}
|
||||||
var lastRates = {}
|
var lastRates = {}
|
||||||
|
|
||||||
var tradesQueue = []
|
var tradesQueues = {}
|
||||||
|
|
||||||
// that's basically a constructor
|
// that's basically a constructor
|
||||||
exports.init = function init (databaseHandle) {
|
exports.init = function init (databaseHandle) {
|
||||||
|
|
@ -119,9 +111,9 @@ function loadPlugin (name, config) {
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadOrConfigPlugin (pluginHandle, pluginType, cryptoCoin, currency,
|
function loadOrConfigPlugin (pluginHandle, pluginType, cryptoCode, currency,
|
||||||
onChangeCallback) {
|
onChangeCallback) {
|
||||||
var cryptoCode = cryptoCoin ? cryptoCoin.unitCode : 'any'
|
cryptoCode = cryptoCode || 'BTC'
|
||||||
|
|
||||||
var currentName = cryptoCode === 'any' || cryptoCode === 'BTC'
|
var currentName = cryptoCode === 'any' || cryptoCode === 'BTC'
|
||||||
? cachedConfig.exchanges.plugins.current[pluginType]
|
? cachedConfig.exchanges.plugins.current[pluginType]
|
||||||
|
|
@ -157,19 +149,18 @@ exports.configure = function configure (config) {
|
||||||
|
|
||||||
cachedConfig = config
|
cachedConfig = config
|
||||||
deviceCurrency = config.exchanges.settings.currency
|
deviceCurrency = config.exchanges.settings.currency
|
||||||
cryptoCoins = config.exchanges.settings.coins || [BTC_COIN]
|
cryptoCodes = config.exchanges.settings.coins || 'BTC'
|
||||||
|
|
||||||
cryptoCoins.forEach(function (cryptoCoin) {
|
cryptoCodes.forEach(function (cryptoCode) {
|
||||||
// TICKER [required] configure (or load)
|
// TICKER [required] configure (or load)
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
|
||||||
loadOrConfigPlugin(
|
loadOrConfigPlugin(
|
||||||
tickerPlugins[cryptoCode],
|
tickerPlugins[cryptoCode],
|
||||||
'ticker',
|
'ticker',
|
||||||
cryptoCoin,
|
cryptoCode,
|
||||||
deviceCurrency, // device currency
|
deviceCurrency, // device currency
|
||||||
function onTickerChange (newTicker) {
|
function onTickerChange (newTicker) {
|
||||||
tickerPlugins[cryptoCode] = newTicker
|
tickerPlugins[cryptoCode] = newTicker
|
||||||
pollRate(cryptoCoin)
|
pollRate(cryptoCode)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -177,28 +168,29 @@ exports.configure = function configure (config) {
|
||||||
loadOrConfigPlugin(
|
loadOrConfigPlugin(
|
||||||
walletPlugins[cryptoCode],
|
walletPlugins[cryptoCode],
|
||||||
'transfer',
|
'transfer',
|
||||||
cryptoCoin,
|
cryptoCode,
|
||||||
null,
|
null,
|
||||||
function onWalletChange (newWallet) {
|
function onWalletChange (newWallet) {
|
||||||
walletPlugins[cryptoCode] = newWallet
|
walletPlugins[cryptoCode] = newWallet
|
||||||
pollBalance(cryptoCoin)
|
pollBalance(cryptoCode)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
tradesQueues[cryptoCode] = []
|
||||||
|
|
||||||
|
loadOrConfigPlugin(
|
||||||
|
traderPlugins[cryptoCode],
|
||||||
|
'trade',
|
||||||
|
cryptoCode,
|
||||||
|
null,
|
||||||
|
function onTraderChange (newTrader) {
|
||||||
|
traderPlugins[cryptoCode] = newTrader
|
||||||
|
if (newTrader === null) stopTrader(cryptoCode)
|
||||||
|
else startTrader(cryptoCode)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TRADER [optional] configure (or load)
|
|
||||||
traderPlugin = loadOrConfigPlugin(
|
|
||||||
traderPlugin,
|
|
||||||
'trade',
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
function onTraderChange (newTrader) {
|
|
||||||
traderPlugin = newTrader
|
|
||||||
if (newTrader === null) stopTrader()
|
|
||||||
else startTrader()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// ID VERIFIER [optional] configure (or load)
|
// ID VERIFIER [optional] configure (or load)
|
||||||
idVerifierPlugin = loadOrConfigPlugin(
|
idVerifierPlugin = loadOrConfigPlugin(
|
||||||
idVerifierPlugin,
|
idVerifierPlugin,
|
||||||
|
|
@ -248,14 +240,13 @@ exports.pollQueries = function pollQueries (session, cb) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function _sendCoins (toAddress, cryptoAtoms, cryptoCoin, cb) {
|
function _sendCoins (toAddress, cryptoAtoms, cryptoCode, cb) {
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
|
||||||
var walletPlugin = walletPlugins[cryptoCode]
|
var walletPlugin = walletPlugins[cryptoCode]
|
||||||
var transactionFee = cachedConfig.exchanges.settings.transactionFee
|
var transactionFee = cachedConfig.exchanges.settings.transactionFee
|
||||||
if (cryptoCode === 'BTC') {
|
if (cryptoCode === 'BTC') {
|
||||||
walletPlugin.sendBitcoins(toAddress, cryptoAtoms, transactionFee, cb)
|
walletPlugin.sendBitcoins(toAddress, cryptoAtoms, transactionFee, cb)
|
||||||
} else {
|
} else {
|
||||||
walletPlugin.sendCoins(toAddress, cryptoAtoms, cryptoCoin, transactionFee, cb)
|
walletPlugin.sendCoins(toAddress, cryptoAtoms, cryptoCode, transactionFee, cb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,10 +314,15 @@ function reapTxs () {
|
||||||
exports.trade = function trade (session, rawTrade, cb) {
|
exports.trade = function trade (session, rawTrade, cb) {
|
||||||
// TODO: move this to DB, too
|
// TODO: move this to DB, too
|
||||||
// add bill to trader queue (if trader is enabled)
|
// add bill to trader queue (if trader is enabled)
|
||||||
|
var cryptoCode = rawTrade.cryptoCode || 'BTC'
|
||||||
|
var traderPlugin = traderPlugins[cryptoCode]
|
||||||
|
var tradesQueue = tradesQueues[cryptoCode]
|
||||||
|
|
||||||
if (traderPlugin) {
|
if (traderPlugin) {
|
||||||
tradesQueue.push({
|
tradesQueue.push({
|
||||||
currency: rawTrade.currency,
|
currency: rawTrade.currency,
|
||||||
satoshis: rawTrade.satoshis
|
cryptoAtoms: rawTrade.cryptoAtoms,
|
||||||
|
cryptoCode: rawTrade.cryptoCode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -360,9 +356,7 @@ exports.cashOut = function cashOut (session, tx, cb) {
|
||||||
account: 'deposit'
|
account: 'deposit'
|
||||||
}
|
}
|
||||||
|
|
||||||
var cryptoCoin = tx.coin || BTC_COIN
|
var cryptoCode = tx.cryptoCode || 'BTC'
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
|
||||||
|
|
||||||
var walletPlugin = walletPlugins[cryptoCode]
|
var walletPlugin = walletPlugins[cryptoCode]
|
||||||
|
|
||||||
walletPlugin.newAddress(tmpInfo, function (err, address) {
|
walletPlugin.newAddress(tmpInfo, function (err, address) {
|
||||||
|
|
@ -380,9 +374,8 @@ exports.dispenseAck = function dispenseAck (session, rec) {
|
||||||
db.addDispense(session, rec.tx, rec.cartridges)
|
db.addDispense(session, rec.tx, rec.cartridges)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fiatBalance = function fiatBalance (cryptoCoin) {
|
exports.fiatBalance = function fiatBalance (cryptoCode) {
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
var rawRate = exports.getDeviceRate(cryptoCode).rates.ask
|
||||||
var rawRate = exports.getDeviceRate(cryptoCoin).rates.ask
|
|
||||||
var commission = cachedConfig.exchanges.settings.commission
|
var commission = cachedConfig.exchanges.settings.commission
|
||||||
var lastBalance = lastBalances[cryptoCode]
|
var lastBalance = lastBalances[cryptoCode]
|
||||||
|
|
||||||
|
|
@ -410,9 +403,9 @@ exports.fiatBalance = function fiatBalance (cryptoCoin) {
|
||||||
exports.startPolling = function startPolling () {
|
exports.startPolling = function startPolling () {
|
||||||
executeTrades()
|
executeTrades()
|
||||||
|
|
||||||
cryptoCoins.forEach(function (coin) {
|
cryptoCodes.forEach(function (cryptoCode) {
|
||||||
setInterval(async.apply(pollBalance, coin), POLLING_RATE)
|
setInterval(async.apply(pollBalance, cryptoCode), POLLING_RATE)
|
||||||
setInterval(async.apply(pollRate, coin), POLLING_RATE)
|
setInterval(async.apply(pollRate, cryptoCode), POLLING_RATE)
|
||||||
})
|
})
|
||||||
|
|
||||||
setInterval(reapTxs, REAP_RATE)
|
setInterval(reapTxs, REAP_RATE)
|
||||||
|
|
@ -420,29 +413,30 @@ exports.startPolling = function startPolling () {
|
||||||
startTrader()
|
startTrader()
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTrader () {
|
function startTrader (cryptoCode) {
|
||||||
// Always start trading, even if we don't have a trade exchange configured,
|
// Always start trading, even if we don't have a trade exchange configured,
|
||||||
// since configuration can always change in `Trader#configure`.
|
// since configuration can always change in `Trader#configure`.
|
||||||
// `Trader#executeTrades` returns early if we don't have a trade exchange
|
// `Trader#executeTrades` returns early if we don't have a trade exchange
|
||||||
// configured at the moment.
|
// configured at the moment.
|
||||||
|
var traderPlugin = traderPlugins[cryptoCode]
|
||||||
|
|
||||||
if (traderPlugin && !tradeInterval) {
|
if (traderPlugin && !tradeInterval) {
|
||||||
tradeInterval = setInterval(
|
tradeInterval = setInterval(
|
||||||
executeTrades,
|
function () { executeTrades(cryptoCode) },
|
||||||
cachedConfig.exchanges.settings.tradeInterval
|
cachedConfig.exchanges.settings.tradeInterval
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopTrader () {
|
function stopTrader (cryptoCode) {
|
||||||
if (tradeInterval) {
|
if (tradeInterval) {
|
||||||
clearInterval(tradeInterval)
|
clearInterval(tradeInterval)
|
||||||
tradeInterval = null
|
tradeInterval = null
|
||||||
tradesQueue = []
|
tradesQueues[cryptoCode] = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollBalance (cryptoCoin, cb) {
|
function pollBalance (cryptoCode, cb) {
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
|
||||||
logger.debug('[%s] collecting balance', cryptoCode)
|
logger.debug('[%s] collecting balance', cryptoCode)
|
||||||
|
|
||||||
var walletPlugin = walletPlugins[cryptoCode]
|
var walletPlugin = walletPlugins[cryptoCode]
|
||||||
|
|
@ -461,8 +455,7 @@ function pollBalance (cryptoCoin, cb) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollRate (cryptoCoin, cb) {
|
function pollRate (cryptoCode, cb) {
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
|
||||||
logger.debug('[%s] polling for rates (%s)', cryptoCode, tickerPlugin.NAME)
|
logger.debug('[%s] polling for rates (%s)', cryptoCode, tickerPlugin.NAME)
|
||||||
var tickerPlugin = tickerPlugins[cryptoCode]
|
var tickerPlugin = tickerPlugins[cryptoCode]
|
||||||
|
|
||||||
|
|
@ -471,7 +464,7 @@ function pollRate (cryptoCoin, cb) {
|
||||||
|
|
||||||
var tickerF = cryptoCode === 'BTC'
|
var tickerF = cryptoCode === 'BTC'
|
||||||
? async.apply(tickerPlugin.ticker, currencies)
|
? async.apply(tickerPlugin.ticker, currencies)
|
||||||
: async.apply(tickerPlugin.ticker, currencies, cryptoCoin)
|
: async.apply(tickerPlugin.ticker, currencies, cryptoCode)
|
||||||
|
|
||||||
tickerF(function (err, resRates) {
|
tickerF(function (err, resRates) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
@ -491,8 +484,7 @@ function pollRate (cryptoCoin, cb) {
|
||||||
* Getters | Helpers
|
* Getters | Helpers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.getDeviceRate = function getDeviceRate (cryptoCoin) {
|
exports.getDeviceRate = function getDeviceRate (cryptoCode) {
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
|
||||||
if (!lastRates[cryptoCode]) return null
|
if (!lastRates[cryptoCode]) return null
|
||||||
|
|
||||||
var lastRate = lastRates[cryptoCode]
|
var lastRate = lastRates[cryptoCode]
|
||||||
|
|
@ -501,8 +493,7 @@ exports.getDeviceRate = function getDeviceRate (cryptoCoin) {
|
||||||
return lastRate[deviceCurrency]
|
return lastRate[deviceCurrency]
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getBalance = function getBalance (cryptoCoin) {
|
exports.getBalance = function getBalance (cryptoCode) {
|
||||||
var cryptoCode = cryptoCoin.unitCode
|
|
||||||
var lastBalance = lastBalances[cryptoCode]
|
var lastBalance = lastBalances[cryptoCode]
|
||||||
if (!lastBalance) return null
|
if (!lastBalance) return null
|
||||||
|
|
||||||
|
|
@ -513,44 +504,50 @@ exports.getBalance = function getBalance (cryptoCoin) {
|
||||||
* Trader functions
|
* Trader functions
|
||||||
*/
|
*/
|
||||||
function purchase (trade, cb) {
|
function purchase (trade, cb) {
|
||||||
|
var cryptoCode = trade.cryptoCode
|
||||||
|
var traderPlugin = traderPlugins[cryptoCode]
|
||||||
traderPlugin.purchase(trade.satoshis, null, function (err) {
|
traderPlugin.purchase(trade.satoshis, null, function (err) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
pollBalance('BTC')
|
pollBalance(cryptoCode)
|
||||||
if (typeof cb === 'function') cb()
|
if (typeof cb === 'function') cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function consolidateTrades () {
|
function consolidateTrades (cryptoCode) {
|
||||||
// NOTE: value in satoshis stays the same no matter the currency
|
// NOTE: value in satoshis stays the same no matter the currency
|
||||||
|
var cryptoAtoms = tradesQueues[cryptoCode].reduce(function (prev, current) {
|
||||||
|
return current.cryptoAtoms.plus(prev)
|
||||||
|
}, new BigNumber(0))
|
||||||
|
|
||||||
var consolidatedTrade = {
|
var consolidatedTrade = {
|
||||||
currency: deviceCurrency,
|
currency: deviceCurrency,
|
||||||
satoshis: tradesQueue.reduce(function (prev, current) {
|
cryptoAtoms: cryptoAtoms,
|
||||||
return prev + current.satoshis
|
cryptoCode: cryptoCode
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tradesQueue = []
|
tradesQueues[cryptoCode] = []
|
||||||
|
|
||||||
logger.debug('consolidated: ', JSON.stringify(consolidatedTrade))
|
logger.debug('consolidated: ', JSON.stringify(consolidatedTrade))
|
||||||
return consolidatedTrade
|
return consolidatedTrade
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeTrades () {
|
function executeTrades (cryptoCode) {
|
||||||
|
var traderPlugin = traderPlugins[cryptoCode]
|
||||||
if (!traderPlugin) return
|
if (!traderPlugin) return
|
||||||
|
|
||||||
logger.debug('checking for trades')
|
logger.debug('checking for trades')
|
||||||
|
|
||||||
var trade = consolidateTrades()
|
var trade = consolidateTrades(cryptoCode)
|
||||||
|
|
||||||
if (trade.satoshis === 0) {
|
if (trade.cryptoAtoms.eq(0)) {
|
||||||
logger.debug('rejecting 0 trade')
|
logger.debug('rejecting 0 trade')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('making a trade: %d', trade.satoshis / SATOSHI_FACTOR)
|
logger.debug('making a trade: %d', trade.cryptoAtoms.toString())
|
||||||
purchase(trade, function (err) {
|
purchase(trade, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
tradesQueue.push(trade)
|
tradesQueues[cryptoCode].push(trade)
|
||||||
if (err.name !== 'orderTooSmall') logger.error(err)
|
if (err.name !== 'orderTooSmall') logger.error(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -567,6 +564,6 @@ exports.verifyTx = function verifyTx (data, cb) {
|
||||||
idVerifierPlugin.verifyTransaction(data, cb)
|
idVerifierPlugin.verifyTransaction(data, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCryptoCoins = function getCryptoCoins () {
|
exports.getcryptoCodes = function getcryptoCodes () {
|
||||||
return cryptoCoins
|
return cryptoCodes
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ var pids = {}
|
||||||
var reboots = {}
|
var reboots = {}
|
||||||
|
|
||||||
function buildRates () {
|
function buildRates () {
|
||||||
var cryptoCoins = plugins.getCryptoCoins()
|
var cryptoCodes = plugins.getcryptoCodes()
|
||||||
var config = plugins.getConfig()
|
var config = plugins.getConfig()
|
||||||
var settings = config.exchanges.settings
|
var settings = config.exchanges.settings
|
||||||
|
|
||||||
|
|
@ -31,9 +31,8 @@ function buildRates () {
|
||||||
var cashOutCommission = settings.fiatCommission || cashInCommission
|
var cashOutCommission = settings.fiatCommission || cashInCommission
|
||||||
|
|
||||||
var rates = {}
|
var rates = {}
|
||||||
cryptoCoins.forEach(function (coin) {
|
cryptoCodes.forEach(function (cryptoCode) {
|
||||||
var cryptoCode = coin.unitCode
|
var rate = plugins.getDeviceRate(cryptoCode).rates
|
||||||
var rate = plugins.getDeviceRate(coin).rates
|
|
||||||
rates[cryptoCode] = {
|
rates[cryptoCode] = {
|
||||||
cashIn: rate.ask.times(cashInCommission),
|
cashIn: rate.ask.times(cashInCommission),
|
||||||
cashOut: rate.bid.div(cashOutCommission)
|
cashOut: rate.bid.div(cashOutCommission)
|
||||||
|
|
@ -44,12 +43,12 @@ function buildRates () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildBalances () {
|
function buildBalances () {
|
||||||
var cryptoCoins = plugins.getCryptoCoins()
|
var cryptoCodes = plugins.getcryptoCodes()
|
||||||
|
|
||||||
var balances = {}
|
var balances = {}
|
||||||
cryptoCoins.forEach(function (coin) {
|
cryptoCodes.forEach(function (cryptoCode) {
|
||||||
var balance = plugins.fiatBalance(coin)
|
var balance = plugins.fiatBalance(cryptoCode)
|
||||||
balances[coin] = balance
|
balances[cryptoCode] = balance
|
||||||
})
|
})
|
||||||
|
|
||||||
return balances
|
return balances
|
||||||
|
|
|
||||||
10
todo.txt
10
todo.txt
|
|
@ -1,5 +1,11 @@
|
||||||
- rethink scale names: is unit satoshi or btc? cryptoAtom, cryptoAtom
|
|
||||||
- cryptoCoin is record, not code
|
|
||||||
- specify crypto and fiat for trades
|
- specify crypto and fiat for trades
|
||||||
- make sure ticker is in full coins
|
- make sure ticker is in full coins
|
||||||
- getDeviceRate should return bignumber
|
- getDeviceRate should return bignumber
|
||||||
|
- test with l-m
|
||||||
|
|
||||||
|
backwards compatibility:
|
||||||
|
|
||||||
|
- new l-m must be backwards compatible with old l-s
|
||||||
|
|
||||||
|
- parse out bignumber when loading in routes
|
||||||
|
- making a trade -- convert to units
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue