feat: add GraphQL server and resolvers
This commit is contained in:
parent
4d8c8c4b62
commit
a0ed5a3a0e
5 changed files with 387 additions and 1 deletions
140
lib/graphql/resolvers.js
Normal file
140
lib/graphql/resolvers.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const { accounts: accountsConfig, countries, languages } = require('../new-admin/config')
|
||||||
|
const plugins = require('../plugins')
|
||||||
|
const configManager = require('../new-config-manager')
|
||||||
|
const customRequestQueries = require('../new-admin/services/customInfoRequests')
|
||||||
|
const state = require('../middlewares/state')
|
||||||
|
|
||||||
|
const urlsToPing = [
|
||||||
|
`us.archive.ubuntu.com`,
|
||||||
|
`uk.archive.ubuntu.com`,
|
||||||
|
`za.archive.ubuntu.com`,
|
||||||
|
`cn.archive.ubuntu.com`
|
||||||
|
]
|
||||||
|
|
||||||
|
const speedtestFiles = [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/lamassu/speed-test-assets/raw/main/python-defaults_2.7.18-3.tar.gz',
|
||||||
|
size: 44668
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const addSmthInfo = (dstField, srcFields) => smth =>
|
||||||
|
smth && smth.active ? _.set(dstField, _.pick(srcFields, smth)) : _.identity
|
||||||
|
|
||||||
|
const addOperatorInfo = addSmthInfo(
|
||||||
|
'operatorInfo',
|
||||||
|
['name', 'phone', 'email', 'website', 'companyNumber']
|
||||||
|
)
|
||||||
|
|
||||||
|
const addReceiptInfo = addSmthInfo(
|
||||||
|
'receiptInfo',
|
||||||
|
[
|
||||||
|
'sms',
|
||||||
|
'operatorWebsite',
|
||||||
|
'operatorEmail',
|
||||||
|
'operatorPhone',
|
||||||
|
'companyNumber',
|
||||||
|
'machineLocation',
|
||||||
|
'customerNameOrPhoneNumber',
|
||||||
|
'exchangeRate',
|
||||||
|
'addressQRCode',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
/* TODO: Simplify this. */
|
||||||
|
const buildTriggers = (allTriggers) => {
|
||||||
|
const normalTriggers = []
|
||||||
|
const customTriggers = _.filter(o => {
|
||||||
|
if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o)
|
||||||
|
return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId)
|
||||||
|
}, allTriggers)
|
||||||
|
|
||||||
|
return _.flow(
|
||||||
|
_.map(_.get('customInfoRequestId')),
|
||||||
|
customRequestQueries.batchGetCustomInfoRequest
|
||||||
|
)(customTriggers)
|
||||||
|
.then(res => {
|
||||||
|
res.forEach((details, index) => {
|
||||||
|
// make sure we aren't attaching the details to the wrong trigger
|
||||||
|
if (customTriggers[index].customInfoRequestId !== details.id) return
|
||||||
|
customTriggers[index] = { ...customTriggers[index], customInfoRequest: details }
|
||||||
|
})
|
||||||
|
return [...normalTriggers, ...customTriggers]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, settings }, info) =>
|
||||||
|
Promise.all([
|
||||||
|
plugins(settings, deviceId).staticConfigQueries(),
|
||||||
|
!!configManager.getCompliance(settings.config).enablePaperWalletOnly,
|
||||||
|
configManager.getTriggersAutomation(settings.config),
|
||||||
|
buildTriggers(configManager.getTriggers(settings.config)),
|
||||||
|
configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2',
|
||||||
|
configManager.getLocale(deviceId, settings.config),
|
||||||
|
configManager.getOperatorInfo(settings.config),
|
||||||
|
configManager.getReceipt(settings.config),
|
||||||
|
!!configManager.getCashOut(deviceId, settings.config).active,
|
||||||
|
])
|
||||||
|
.then(([
|
||||||
|
staticConf,
|
||||||
|
enablePaperWalletOnly,
|
||||||
|
triggersAutomation,
|
||||||
|
triggers,
|
||||||
|
hasLightning,
|
||||||
|
localeInfo,
|
||||||
|
operatorInfo,
|
||||||
|
receiptInfo,
|
||||||
|
twoWayMode,
|
||||||
|
]) =>
|
||||||
|
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
||||||
|
null :
|
||||||
|
_.flow(
|
||||||
|
_.assign({
|
||||||
|
enablePaperWalletOnly,
|
||||||
|
triggersAutomation,
|
||||||
|
triggers,
|
||||||
|
hasLightning,
|
||||||
|
localeInfo: {
|
||||||
|
country: localeInfo.country,
|
||||||
|
languages: localeInfo.languages,
|
||||||
|
fiatCode: localeInfo.fiatCurrency
|
||||||
|
},
|
||||||
|
machineInfo: { deviceId, deviceName },
|
||||||
|
twoWayMode,
|
||||||
|
speedtestFiles,
|
||||||
|
urlsToPing,
|
||||||
|
}),
|
||||||
|
_.update('triggersAutomation', _.mapValues(_.eq('Automatic'))),
|
||||||
|
addOperatorInfo(operatorInfo),
|
||||||
|
addReceiptInfo(receiptInfo)
|
||||||
|
)(staticConf))
|
||||||
|
|
||||||
|
|
||||||
|
const setZeroConfLimit = config => coin =>
|
||||||
|
_.set(
|
||||||
|
'zeroConfLimit',
|
||||||
|
configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit,
|
||||||
|
coin
|
||||||
|
)
|
||||||
|
|
||||||
|
const dynamicConfig = (parent, variables, { deviceId, operatorId, pid, settings }, info) => {
|
||||||
|
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
||||||
|
return plugins(settings, deviceId)
|
||||||
|
.dynamicConfigQueries()
|
||||||
|
.then(_.flow(
|
||||||
|
_.update('coins', _.map(setZeroConfLimit(settings.config))),
|
||||||
|
_.set('reboot', !!pid && state.reboots?.[operatorId]?.[deviceId] === pid),
|
||||||
|
_.set('shutdown', !!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid),
|
||||||
|
_.set('restartServices', !!pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Query: {
|
||||||
|
staticConfig,
|
||||||
|
dynamicConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/graphql/server.js
Normal file
27
lib/graphql/server.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
const logger = require('../logger')
|
||||||
|
|
||||||
|
const https = require('https')
|
||||||
|
const { ApolloServer } = require('apollo-server-express')
|
||||||
|
|
||||||
|
const devMode = !!require('minimist')(process.argv.slice(2)).dev
|
||||||
|
|
||||||
|
module.exports = new ApolloServer({
|
||||||
|
typeDefs: require('./types'),
|
||||||
|
resolvers: require('./resolvers'),
|
||||||
|
context: ({ req, res }) => ({
|
||||||
|
deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */
|
||||||
|
deviceName: req.deviceName, /* lib/middlewares/authorize.js */
|
||||||
|
operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */
|
||||||
|
pid: req.query.pid,
|
||||||
|
settings: req.settings, /* lib/middlewares/populateSettings.js */
|
||||||
|
}),
|
||||||
|
uploads: false,
|
||||||
|
playground: false,
|
||||||
|
introspection: false,
|
||||||
|
formatError: error => {
|
||||||
|
logger.error(error)
|
||||||
|
return error
|
||||||
|
},
|
||||||
|
debug: devMode,
|
||||||
|
logger
|
||||||
|
})
|
||||||
137
lib/graphql/types.js
Normal file
137
lib/graphql/types.js
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
const { gql } = require('apollo-server-express')
|
||||||
|
module.exports = gql`
|
||||||
|
type Coin {
|
||||||
|
cryptoCode: String!
|
||||||
|
display: String!
|
||||||
|
minimumTx: String!
|
||||||
|
cashInFee: String!
|
||||||
|
cashInCommission: String!
|
||||||
|
cashOutCommission: String!
|
||||||
|
cryptoNetwork: Boolean!
|
||||||
|
cryptoUnits: String!
|
||||||
|
batchable: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocaleInfo {
|
||||||
|
country: String!
|
||||||
|
fiatCode: String!
|
||||||
|
languages: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperatorInfo {
|
||||||
|
name: String!
|
||||||
|
phone: String!
|
||||||
|
email: String!
|
||||||
|
website: String!
|
||||||
|
companyNumber: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineInfo {
|
||||||
|
deviceId: String!
|
||||||
|
deviceName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceiptInfo {
|
||||||
|
sms: Boolean!
|
||||||
|
operatorWebsite: Boolean!
|
||||||
|
operatorEmail: Boolean!
|
||||||
|
operatorPhone: Boolean!
|
||||||
|
companyNumber: Boolean!
|
||||||
|
machineLocation: Boolean!
|
||||||
|
customerNameOrPhoneNumber: Boolean!
|
||||||
|
exchangeRate: Boolean!
|
||||||
|
addressQRCode: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpeedtestFile {
|
||||||
|
url: String!
|
||||||
|
size: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
# True if automatic, False otherwise
|
||||||
|
type TriggersAutomation {
|
||||||
|
sanctions: Boolean!
|
||||||
|
idCardPhoto: Boolean!
|
||||||
|
idCardData: Boolean!
|
||||||
|
facephoto: Boolean!
|
||||||
|
usSsn: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trigger {
|
||||||
|
id: String!
|
||||||
|
customInfoRequestId: String!
|
||||||
|
direction: String!
|
||||||
|
requirement: String!
|
||||||
|
triggerType: String!
|
||||||
|
|
||||||
|
suspensionDays: Int
|
||||||
|
threshold: Int
|
||||||
|
thresholdDays: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaticConfig {
|
||||||
|
configVersion: Int!
|
||||||
|
|
||||||
|
areThereAvailablePromoCodes: Boolean!
|
||||||
|
coins: [Coin!]!
|
||||||
|
enablePaperWalletOnly: Boolean!
|
||||||
|
hasLightning: Boolean!
|
||||||
|
serverVersion: String!
|
||||||
|
timezone: Int!
|
||||||
|
twoWayMode: Boolean!
|
||||||
|
|
||||||
|
localeInfo: LocaleInfo!
|
||||||
|
operatorInfo: OperatorInfo
|
||||||
|
machineInfo: MachineInfo!
|
||||||
|
receiptInfo: ReceiptInfo
|
||||||
|
|
||||||
|
speedtestFiles: [SpeedtestFile!]!
|
||||||
|
urlsToPing: [String!]!
|
||||||
|
|
||||||
|
triggersAutomation: TriggersAutomation!
|
||||||
|
triggers: [Trigger!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynamicCoinValues {
|
||||||
|
# NOTE: Doesn't seem to be used anywhere outside of lib/plugins.js.
|
||||||
|
# However, it can be used to generate the cache key, if we ever move to an
|
||||||
|
# actual caching mechanism.
|
||||||
|
#timestamp: String!
|
||||||
|
|
||||||
|
cryptoCode: String!
|
||||||
|
balance: String!
|
||||||
|
|
||||||
|
# Raw rates
|
||||||
|
ask: String!
|
||||||
|
bid: String!
|
||||||
|
|
||||||
|
# Rates with commissions applied
|
||||||
|
cashIn: String!
|
||||||
|
cashOut: String!
|
||||||
|
|
||||||
|
zeroConfLimit: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhysicalCassette {
|
||||||
|
denomination: Int!
|
||||||
|
count: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cassettes {
|
||||||
|
physical: [PhysicalCassette!]!
|
||||||
|
virtual: [Int!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynamicConfig {
|
||||||
|
cassettes: Cassettes
|
||||||
|
coins: [DynamicCoinValues!]!
|
||||||
|
reboot: Boolean!
|
||||||
|
shutdown: Boolean!
|
||||||
|
restartServices: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
staticConfig(currentConfigVersion: Int): StaticConfig
|
||||||
|
dynamicConfig: DynamicConfig!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -25,6 +25,9 @@ const customers = require('./customers')
|
||||||
const commissionMath = require('./commission-math')
|
const commissionMath = require('./commission-math')
|
||||||
const loyalty = require('./loyalty')
|
const loyalty = require('./loyalty')
|
||||||
const transactionBatching = require('./tx-batching')
|
const transactionBatching = require('./tx-batching')
|
||||||
|
const state = require('./middlewares/state')
|
||||||
|
|
||||||
|
const VERSION = require('../package.json').version
|
||||||
|
|
||||||
const { CASSETTE_MAX_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants')
|
const { CASSETTE_MAX_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants')
|
||||||
|
|
||||||
|
|
@ -281,6 +284,78 @@ function plugins (settings, deviceId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function staticConfigQueries () {
|
||||||
|
const massageCoins = _.map(_.pick([
|
||||||
|
'batchable',
|
||||||
|
'cashInCommission',
|
||||||
|
'cashInFee',
|
||||||
|
'cashOutCommission',
|
||||||
|
'cryptoCode',
|
||||||
|
'cryptoNetwork',
|
||||||
|
'cryptoUnits',
|
||||||
|
'display',
|
||||||
|
'minimumTx'
|
||||||
|
]))
|
||||||
|
|
||||||
|
return pollQueries()
|
||||||
|
.then(_.flow(
|
||||||
|
_.pick([
|
||||||
|
'areThereAvailablePromoCodes',
|
||||||
|
'coins',
|
||||||
|
'configVersion',
|
||||||
|
'timezone'
|
||||||
|
]),
|
||||||
|
_.update('coins', massageCoins),
|
||||||
|
_.set('serverVersion', VERSION)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
function dynamicConfigQueries () {
|
||||||
|
const massageCassettes = cassettes =>
|
||||||
|
cassettes ?
|
||||||
|
_.flow(
|
||||||
|
cassettes => _.set('physical', _.get('cassettes', cassettes), cassettes),
|
||||||
|
cassettes => _.set('virtual', _.get('virtualCassettes', cassettes), cassettes),
|
||||||
|
_.unset('cassettes'),
|
||||||
|
_.unset('virtualCassettes')
|
||||||
|
)(cassettes) :
|
||||||
|
null
|
||||||
|
|
||||||
|
return pollQueries()
|
||||||
|
.then(_.flow(
|
||||||
|
_.pick(['balances', 'cassettes', 'coins', 'rates']),
|
||||||
|
|
||||||
|
_.update('cassettes', massageCassettes),
|
||||||
|
|
||||||
|
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
||||||
|
_.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])),
|
||||||
|
|
||||||
|
/* [{ cryptoCode: balance }, ...] => [[cryptoCode, { balance }], ...] */
|
||||||
|
_.update('balances', _.flow(
|
||||||
|
_.toPairs,
|
||||||
|
_.map(([cryptoCode, balance]) => [cryptoCode, { balance }])
|
||||||
|
)),
|
||||||
|
|
||||||
|
/* Group the separate objects by cryptoCode */
|
||||||
|
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
||||||
|
({ balances, cassettes, coins, rates }) => ({
|
||||||
|
cassettes,
|
||||||
|
coins: _.flow(
|
||||||
|
_.reduce(
|
||||||
|
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
|
||||||
|
rates
|
||||||
|
),
|
||||||
|
|
||||||
|
/* { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } => [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] */
|
||||||
|
_.toPairs,
|
||||||
|
|
||||||
|
/* [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] => [{ cryptoCode, balance, ask, bid, cashIn, cashOut }, ...] */
|
||||||
|
_.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj))
|
||||||
|
)(_.concat(balances, coins))
|
||||||
|
})
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
function sendCoins (tx) {
|
function sendCoins (tx) {
|
||||||
return wallet.supportsBatching(settings, tx.cryptoCode)
|
return wallet.supportsBatching(settings, tx.cryptoCode)
|
||||||
.then(supportsBatching => {
|
.then(supportsBatching => {
|
||||||
|
|
@ -854,6 +929,8 @@ function plugins (settings, deviceId) {
|
||||||
getRawRates,
|
getRawRates,
|
||||||
buildRatesNoCommission,
|
buildRatesNoCommission,
|
||||||
pollQueries,
|
pollQueries,
|
||||||
|
staticConfigQueries,
|
||||||
|
dynamicConfigQueries,
|
||||||
sendCoins,
|
sendCoins,
|
||||||
newAddress,
|
newAddress,
|
||||||
isHd,
|
isHd,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ const verifyUserRoutes = require('./routes/verifyUserRoutes')
|
||||||
const verifyTxRoutes = require('./routes/verifyTxRoutes')
|
const verifyTxRoutes = require('./routes/verifyTxRoutes')
|
||||||
const verifyPromoCodeRoutes = require('./routes/verifyPromoCodeRoutes')
|
const verifyPromoCodeRoutes = require('./routes/verifyPromoCodeRoutes')
|
||||||
|
|
||||||
|
const graphQLServer = require('./graphql/server')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const configRequiredRoutes = [
|
const configRequiredRoutes = [
|
||||||
|
|
@ -39,7 +41,8 @@ const configRequiredRoutes = [
|
||||||
'/phone_code',
|
'/phone_code',
|
||||||
'/customer',
|
'/customer',
|
||||||
'/tx',
|
'/tx',
|
||||||
'/verify_promo_code'
|
'/verify_promo_code',
|
||||||
|
'/graphql'
|
||||||
]
|
]
|
||||||
const devMode = argv.dev || options.http
|
const devMode = argv.dev || options.http
|
||||||
|
|
||||||
|
|
@ -79,6 +82,8 @@ app.use('/tx', txRoutes)
|
||||||
|
|
||||||
app.use('/logs', logsRoutes)
|
app.use('/logs', logsRoutes)
|
||||||
|
|
||||||
|
graphQLServer.applyMiddleware({ app })
|
||||||
|
|
||||||
app.use(errorHandler)
|
app.use(errorHandler)
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.status(404).json({ error: 'No such route' })
|
res.status(404).json({ error: 'No such route' })
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue