diff --git a/package.json b/package.json index 7a9a64ed..13c62870 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "koa": "^1.0.0", "koa-compress": "^1.0.6", "koa-mag": "^1.0.4", - "koa-passport": "^1.2.0", + "koa-passport": "^1.3.0", "koa-route": "^2.4.2", "koa-static": "^1.4.5", "lodash": "^3.5.0", diff --git a/src/app.js b/src/app.js index abbd17f5..5a02202f 100644 --- a/src/app.js +++ b/src/app.js @@ -1,39 +1,102 @@ 'use strict' +const _ = require('lodash') +const co = require('co') const metadata = require('./controllers/metadata') const health = require('./controllers/health') const pairs = require('./controllers/pairs') const quote = require('./controllers/quote') const notifications = require('./controllers/notifications') +const subscriptions = require('./models/subscriptions') const compress = require('koa-compress') const serve = require('koa-static') const route = require('koa-route') const errorHandler = require('five-bells-shared/middlewares/error-handler') const koa = require('koa') const path = require('path') -const log = require('./services/log') const logger = require('koa-mag') -const passport = require('koa-passport') -const app = module.exports = koa() +const Passport = require('koa-passport').KoaPassport +const log = require('./services/log') + +function listen (koaApp, config, backend, ledgers) { + if (config.getIn(['server', 'secure'])) { + const spdy = require('spdy') + const tls = config.get('tls') + + const options = { + port: config.getIn(['server', 'port']), + host: config.getIn(['server', 'bind_ip']), + key: tls.key, + cert: tls.cert, + ca: tls.ca, + crl: tls.crl, + requestCert: config.getIn(['auth', 'client_certificates_enabled']), + + // Certificates are checked in the passport-client-cert middleware + // Authorization check is disabled here to allow clients to connect + // to some endpoints without presenting client certificates, or using a + // different authentication method (e.g., Basic Auth) + rejectUnauthorized: false + } + + spdy.createServer( + options, koaApp.callback()).listen(config.getIn(['server', 'port'])) + } else { + koaApp.listen(config.getIn(['server', 'port'])) + } + + log('app').info('connector listening on ' + config.getIn(['server', 'bind_ip']) + ':' + + config.getIn(['server', 'port'])) + log('app').info('public at ' + config.getIn(['server', 'base_uri'])) + for (let pair of config.get('tradingPairs')) { + log('app').info('pair', pair) + } + + // Start a coroutine that connects to the backend and + // subscribes to all the ledgers in the background + co(function * () { + yield backend.connect() + + yield subscriptions.subscribePairs(config.get('tradingPairs'), ledgers, config) + }).catch(function (err) { + log('app').error(typeof err === 'object' && err.stack || err) + }) +} + +function createApp (config, backend, ledgers) { + const koaApp = koa() + + koaApp.context.config = config + koaApp.context.backend = backend + koaApp.context.ledgers = ledgers + + // Configure passport + const passport = new Passport() + require('./services/auth')(passport, config) + + // Logger + koaApp.use(logger()) + koaApp.use(errorHandler({log: log('error-handler')})) + koaApp.use(passport.initialize()) -// Configure passport -require('./services/auth') + koaApp.use(route.get('/', metadata.getResource)) + koaApp.use(route.get('/health', health.getResource)) + koaApp.use(route.get('/pairs', pairs.getCollection)) -// Logger -app.use(logger()) -app.use(errorHandler({log: log('error-handler')})) -app.use(passport.initialize()) + koaApp.use(route.get('/quote', quote.get)) -app.use(route.get('/', metadata.getResource)) -app.use(route.get('/health', health.getResource)) -app.use(route.get('/pairs', pairs.getCollection)) + koaApp.use(route.post('/notifications', notifications.post)) -app.use(route.get('/quote', quote.get)) + // Serve static files + koaApp.use(serve(path.join(__dirname, 'public'))) -app.use(route.post('/notifications', notifications.post)) + // Compress + koaApp.use(compress()) -// Serve static files -app.use(serve(path.join(__dirname, 'public'))) + return { + koaApp: koaApp, + listen: _.partial(listen, koaApp, config, backend, ledgers) + } +} -// Compress -app.use(compress()) +module.exports = createApp diff --git a/src/controllers/notifications.js b/src/controllers/notifications.js index cc5ca4f5..8cf955a6 100644 --- a/src/controllers/notifications.js +++ b/src/controllers/notifications.js @@ -72,7 +72,7 @@ exports.post = function * postNotification () { const notification = yield requestUtil.validateBody(this, 'Notification') log.debug('Got notification: ' + JSON.stringify(notification)) try { - yield model.processNotification(notification) + yield model.processNotification(notification, this.config, this.backend, this.ledgers) } catch (e) { if (!(e instanceof UnacceptableExpiryError)) { log.error('Notification handling received critical error: ' + e) diff --git a/src/controllers/pairs.js b/src/controllers/pairs.js index e8404729..2343191b 100644 --- a/src/controllers/pairs.js +++ b/src/controllers/pairs.js @@ -36,5 +36,5 @@ const model = require('../models/pairs') */ /* eslint-enable */ exports.getCollection = function * getCollection () { - this.body = model.getPairs() + this.body = model.getPairs(this.config) } diff --git a/src/controllers/quote.js b/src/controllers/quote.js index 79449e18..886b41c9 100644 --- a/src/controllers/quote.js +++ b/src/controllers/quote.js @@ -109,6 +109,7 @@ const model = require('../models/quote') /* eslint-enable */ exports.get = function * () { - this.body = yield model.getQuote(this.query) + this.body = yield model.getQuote( + this.query, this.ledgers, this.backend, this.config) } diff --git a/src/index.js b/src/index.js index e63836ef..e8d491b6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,65 +1,19 @@ 'use strict' -const co = require('co') const ledgers = require('./services/ledgers') const config = require('./services/config') -const log = require('./services/log') const backend = require('./services/backend') -const subscriptions = require('./models/subscriptions') -const app = require('./app') -const ledgersService = require('./services/ledgers') +const balanceCache = require('./services/balance-cache') +const createApp = require('./app') -function listen () { - if (config.getIn(['server', 'secure'])) { - const spdy = require('spdy') - const tls = config.get('tls') - - const options = { - port: config.getIn(['server', 'port']), - host: config.getIn(['server', 'bind_ip']), - key: tls.key, - cert: tls.cert, - ca: tls.ca, - crl: tls.crl, - requestCert: config.getIn(['auth', 'client_certificates_enabled']), - - // Certificates are checked in the passport-client-cert middleware - // Authorization check is disabled here to allow clients to connect - // to some endpoints without presenting client certificates, or using a - // different authentication method (e.g., Basic Auth) - rejectUnauthorized: false - } - - spdy.createServer( - options, app.callback()).listen(config.getIn(['server', 'port'])) - } else { - app.listen(config.getIn(['server', 'port'])) - } - - log('app').info('connector listening on ' + config.getIn(['server', 'bind_ip']) + ':' + - config.getIn(['server', 'port'])) - log('app').info('public at ' + config.getIn(['server', 'base_uri'])) - for (let pair of config.get('tradingPairs')) { - log('app').info('pair', pair) - } - - // Start a coroutine that connects to the backend and - // subscribes to all the ledgers in the background - co(function * () { - yield backend.connect() - - yield subscriptions.subscribePairs(config.get('tradingPairs'), ledgersService, config) - }).catch(function (err) { - log('app').error(typeof err === 'object' && err.stack || err) - }) -} +const connector = createApp(config, backend, ledgers) module.exports = { - app: app, - listen: listen, + app: connector.koaApp, + listen: connector.listen, addLedger: ledgers.addLedger.bind(ledgers), _test: { BalanceCache: require('./lib/balance-cache'), - balanceCache: require('./services/balance-cache'), + balanceCache: balanceCache, loadConnectorConfig: require('./lib/config'), config: require('./services/config'), logger: require('./services/log'), @@ -68,5 +22,5 @@ module.exports = { } if (!module.parent) { - listen() + connector.listen() } diff --git a/src/models/notifications.js b/src/models/notifications.js index e4cda06d..13c3c503 100644 --- a/src/models/notifications.js +++ b/src/models/notifications.js @@ -2,9 +2,10 @@ const payments = require('./payments') -function * processNotification (notification) { +function * processNotification (notification, config, backend, ledgers) { if (notification.event === 'transfer.update') { - yield payments.updateTransfer(notification.resource, notification.related_resources) + yield payments.updateTransfer( + notification.resource, notification.related_resources, config, backend, ledgers) } } diff --git a/src/models/pairs.js b/src/models/pairs.js index 279d9039..6178836e 100644 --- a/src/models/pairs.js +++ b/src/models/pairs.js @@ -1,8 +1,6 @@ 'use strict' -const config = require('../services/config') - -function getPairs () { +function getPairs (config) { const tradingPairs = config.get('tradingPairs') return tradingPairs.map((pair) => { const currencies = pair.map(function (currencyLedgerString) { diff --git a/src/models/payments.js b/src/models/payments.js index 92fc6f75..ef3cf646 100644 --- a/src/models/payments.js +++ b/src/models/payments.js @@ -16,9 +16,6 @@ const UnrelatedNotificationError = require('../errors/unrelated-notification-error') const AssetsNotTradedError = require('../errors/assets-not-traded-error') const hashJSON = require('five-bells-shared/utils/hashJson') -const config = require('../services/config') -const backend = require('../services/backend') -const ledgers = require('../services/ledgers') // TODO this should handle the different types of execution_condition's. function sourceConditionIsDestinationTransfer (source, destination) { @@ -89,7 +86,7 @@ function validateExecutionConditions (payment) { } } -function * validateExecutionConditionPublicKey (payment) { +function * validateExecutionConditionPublicKey (payment, ledgers) { // TODO: use a cache of ledgers' public keys and move this functionality // into the synchronous validateExecutionConditions function for (const sourceTransfer of payment.source_transfers) { @@ -118,7 +115,7 @@ function * validateExecutionConditionPublicKey (payment) { } } -function * validateExpiry (payment) { +function * validateExpiry (payment, config) { // TODO tie the maxHoldTime to the fx rate // TODO bring all these loops into one to speed this up const tester = yield testPaymentExpiry(config, payment) @@ -133,7 +130,7 @@ function * validateExpiry (payment) { } } -function amountFinder (ledger, creditOrDebit) { +function amountFinder (ledger, creditOrDebit, config) { // TODO: we need a more elegant way of handling assets that we don't trade if (!config.getIn(['ledgerCredentials', ledger])) { throw new AssetsNotTradedError('This connector does not support ' + @@ -159,7 +156,7 @@ function amountFinder (ledger, creditOrDebit) { * @param {String} opts.convertToLedger The ledger representing the asset we will convert all of the amounts into (for easier comparisons) * @yield {Float} The total amount converted into the asset represented by opts.convertToLedger */ -function * calculateAmountEquivalent (opts) { +function * calculateAmountEquivalent (opts, config, backend) { // convertedAmountTotal is going to be the total of either the credits // if the transfers are source_transfers or debits if the transfers // are destination_transfers (the amount that is either entering @@ -174,9 +171,9 @@ function * calculateAmountEquivalent (opts) { for (const transfer of opts.transfers) { // Total the number of credits or debits to the connectors account - const relevantAmountTotal = _.reduce(transfer[opts.creditsOrDebits], function (result, creditOrDebit) { - return result.plus(amountFinder(transfer.ledger, creditOrDebit)) - }, new BigNumber(0), this) + const relevantAmountTotal = _.reduce(transfer[opts.creditsOrDebits], (result, creditOrDebit) => { + return result.plus(amountFinder(transfer.ledger, creditOrDebit, config)) + }, new BigNumber(0)) // Throw an error if we're not included in the transfer if (relevantAmountTotal.lte(0) && !opts.noErrors) { @@ -215,7 +212,7 @@ function * calculateAmountEquivalent (opts) { return convertedAmountTotal } -function * validateRate (payment) { +function * validateRate (payment, config, backend) { log.debug('validating rate') // Determine which ledger's asset we will convert all @@ -235,14 +232,14 @@ function * validateRate (payment) { transferSide: 'source', creditsOrDebits: 'credits', convertToLedger: convertToLedger - }) + }, config, backend) const destinationDebitEquivalent = yield calculateAmountEquivalent({ transfers: payment.destination_transfers, transferSide: 'destination', creditsOrDebits: 'debits', convertToLedger: convertToLedger - }) + }, config, backend) if (sourceCreditEquivalent.lt(destinationDebitEquivalent)) { throw new UnacceptableRateError('Payment rate does not match ' + @@ -251,7 +248,7 @@ function * validateRate (payment) { } // Note this modifies the original object -function addAuthorizationToTransfers (transfers) { +function addAuthorizationToTransfers (transfers, config) { // TODO: make sure we're not authorizing anything extra // that shouldn't be taking money out of our account let credentials @@ -273,7 +270,7 @@ function addAuthorizationToTransfers (transfers) { // TODO authorize credits } -function * submitTransfer (destinationTransfer, sourceTransfer) { +function * submitTransfer (destinationTransfer, sourceTransfer, ledgers) { for (const debit of destinationTransfer.debits) { debit.memo = Object.assign({}, debit.memo, { source_transfer_ledger: sourceTransfer.ledger, @@ -283,24 +280,24 @@ function * submitTransfer (destinationTransfer, sourceTransfer) { yield ledgers.putTransfer(destinationTransfer) } -function * validate (payment) { +function * validate (payment, ledgers, config, backend) { // TODO: Check expiry settings // TODO: Check ledger signature on source payment // TODO: Check ledger signature on destination payment - yield validateExpiry(payment) - yield validateRate(payment) + yield validateExpiry(payment, config) + yield validateRate(payment, config, backend) validateExecutionConditions(payment) - yield validateExecutionConditionPublicKey(payment) + yield validateExecutionConditionPublicKey(payment, ledgers) } -function * settle (payment) { +function * settle (payment, config, ledgers) { log.debug('Settle payment: ' + JSON.stringify(payment)) - addAuthorizationToTransfers(payment.destination_transfers) + addAuthorizationToTransfers(payment.destination_transfers, config) const sourceTransfer = payment.source_transfers[0] for (const destinationTransfer of payment.destination_transfers) { - yield submitTransfer(destinationTransfer, sourceTransfer) + yield submitTransfer(destinationTransfer, sourceTransfer, ledgers) } const anyTransfersAreExecuted = _.some(payment.destination_transfers, (transfer) => { @@ -316,13 +313,13 @@ function * settle (payment) { } } -function isTraderFunds (funds) { +function isTraderFunds (config, funds) { return _.some(config.ledgerCredentials, (credentials) => { return credentials.account_uri === funds.account }) } -function * updateSourceTransfer (updatedTransfer, traderCredit) { +function * updateSourceTransfer (updatedTransfer, traderCredit, ledgers, config, backend) { const destinationTransfer = traderCredit.memo && traderCredit.memo.destination_transfer if (!destinationTransfer) return ledgers.validateTransfer(destinationTransfer) @@ -334,8 +331,9 @@ function * updateSourceTransfer (updatedTransfer, traderCredit) { source_transfers: [updatedTransfer], destination_transfers: [destinationTransfer] } - yield validate(payment) - yield settle(payment) + + yield validate(payment, ledgers, config, backend) + yield settle(payment, config, ledgers) } function * updateDestinationTransfer (updatedTransfer, traderDebit, relatedResources) { @@ -348,18 +346,17 @@ function * updateDestinationTransfer (updatedTransfer, traderDebit, relatedResou yield executeSourceTransfers([updatedTransfer], relatedResources) } -function * updateTransfer (updatedTransfer, relatedResources) { - // TODO: make sure the transfer is signed by the ledger +function * updateTransfer (updatedTransfer, relatedResources, config, backend, ledgers) { // Maybe it's a source transfer: // When the payment's source transfer is "prepared", authorized/submit the payment. - const traderCredit = updatedTransfer.credits.find(isTraderFunds) + const traderCredit = updatedTransfer.credits.find(_.partial(isTraderFunds, config)) if (traderCredit) { - yield updateSourceTransfer(updatedTransfer, traderCredit) + yield updateSourceTransfer(updatedTransfer, traderCredit, ledgers, config, backend) return } // Or a destination transfer: - const traderDebit = updatedTransfer.debits.find(isTraderFunds) + const traderDebit = updatedTransfer.debits.find(_.partial(isTraderFunds, config)) if (traderDebit) { yield updateDestinationTransfer(updatedTransfer, traderDebit, relatedResources) return diff --git a/src/models/quote.js b/src/models/quote.js index 9cdfee9f..7721cdaa 100644 --- a/src/models/quote.js +++ b/src/models/quote.js @@ -2,17 +2,14 @@ const request = require('co-request') const BigNumber = require('bignumber.js') -const config = require('../services/config') const log = require('../services/log')('quote') -const backend = require('../services/backend') -const ledgers = require('../services/ledgers') -const balanceCache = require('../services/balance-cache') const UnacceptableExpiryError = require('../errors/unacceptable-expiry-error') const UnacceptableAmountError = require('../errors/unacceptable-amount-error') const InvalidURIParameterError = require('five-bells-shared').InvalidUriParameterError const ExternalError = require('../errors/external-error') +const balanceCache = require('../services/balance-cache.js') -function * makeQuoteQuery (params) { +function * makeQuoteQuery (params, config) { // TODO: include the expiry duration in the quote logic let destinationExpiryDuration = parseFloat(params.destination_expiry_duration) let sourceExpiryDuration = parseFloat(params.source_expiry_duration) @@ -77,7 +74,7 @@ function makeQuoteArgs (query) { } } -function makePaymentTemplate (query, quote) { +function makePaymentTemplate (query, quote, ledgers) { const source_amount = quote.source_amount const destination_amount = quote.destination_amount const payment = { @@ -122,8 +119,8 @@ function * getAccountLedger (account) { return ledger } -function * getQuote (params) { - const query = yield makeQuoteQuery(params) +function * getQuote (params, ledgers, backend, config) { + const query = yield makeQuoteQuery(params, config) const quote = yield backend.getQuote(makeQuoteArgs(query)) const sourceBalance = yield balanceCache.get(query.source_ledger) @@ -138,7 +135,7 @@ function * getQuote (params) { quote.destination_amount + ' ' + query.destination_ledger) - return makePaymentTemplate(query, quote) + return makePaymentTemplate(query, quote, ledgers) } module.exports = { diff --git a/src/services/auth.js b/src/services/auth.js index 38d870da..82877f85 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -1,13 +1,13 @@ 'use strict' -const passport = require('koa-passport') const ClientCertStrategy = require('passport-client-certificate').Strategy const UnauthorizedError = require('five-bells-shared/errors/unauthorized-error') -const config = require('./config') -passport.use(new ClientCertStrategy((certificate, done) => { - if (!config.getIn(['auth', 'client_certificates_enabled'])) { - return done(new UnauthorizedError('Unsupported authentication method')) - } -})) +module.exports = function (passport, config) { + passport.use(new ClientCertStrategy((certificate, done) => { + if (!config.getIn(['auth', 'client_certificates_enabled'])) { + return done(new UnauthorizedError('Unsupported authentication method')) + } + })) +}