<|start_filename|>src/utils/errors.js<|end_filename|>
const BaseError = require('standard-error')

function createError (name) {
  class Err extends BaseError {}
  Err.prototype.name = name
  return Err
}

class RescheduleError extends BaseError {
  constructor (msg, asset, props) {
    super(msg, props)
    this.asset = asset
  }
}

class PossibleTimelockError extends RescheduleError {}

module.exports.RescheduleError = RescheduleError
module.exports.PossibleTimelockError = PossibleTimelockError
module.exports.MarketNotFoundError = createError('MarketNotFoundError')
module.exports.MarketNotActiveError = createError('MarketNotActiveError')
module.exports.InvalidAmountError = createError('InvalidAmountError')
module.exports.CounterPartyInsufficientBalanceError = createError('CounterPartyInsufficientBalanceError')
module.exports.OrderNotFoundError = createError('OrderNotFoundError')
module.exports.UnauthorisedError = createError('UnauthorisedError')
module.exports.InvalidOrderStateError = createError('InvalidOrderStateError')
module.exports.InvalidHashError = createError('InvalidHashError')
module.exports.InvalidHTTPBodyError = createError('InvalidHTTPBodyError')
module.exports.DuplicateOrderError = createError('DuplicateOrderError')


<|start_filename|>src/utils/fx.js<|end_filename|>
const { assets, unitToCurrency, currencyToUnit } = require('@liquality/cryptoassets')

function calculateToAmount (from, to, fromAmount, rate) {
  const fromAmountBase = unitToCurrency(assets[from], fromAmount)
  const toBaseAmount = fromAmountBase.times(rate).toNumber()
  const toAmount = Math.floor(currencyToUnit(assets[to], toBaseAmount).toNumber())
  return toAmount
}

function calculateUsdAmount (asset, amount, usdRate) {
  return unitToCurrency(assets[asset], amount).times(usdRate).dp(2).toNumber()
}

function calculateFeeUsdAmount (asset, fee, usdRate) {
  return unitToCurrency(assets[asset], fee).times(usdRate).dp(2).toNumber()
}

module.exports = {
  calculateToAmount,
  calculateUsdAmount,
  calculateFeeUsdAmount
}


<|start_filename|>src/worker/handleJobError.js<|end_filename|>
const Sentry = require('@sentry/node')
const _ = require('lodash')

const debug = require('debug')('liquality:agent:worker:error-handler')

const config = require('../config')

const getStatusCode = e => _.get(e, 'statusCode') || _.get(e, 'response.status') || _.get(e, 'response.statusCode')
const getResponseBody = e => _.get(e, 'response.data') || _.get(e, 'response.body')
const getRequestUrl = e => _.get(e, 'config.url') || e.url
const getRequestData = e => e.data
const getRequestParams = e => e.params

module.exports = async (err, job) => {
  if (
    err.name === 'RescheduleError' ||
    ( // do not retry PossibleTimelockError in production
      process.env.NODE_ENV !== 'production' &&
      err.name === 'PossibleTimelockError'
    )
  ) {
    debug(`[x${job.attrs.failCount}]`, err.name, err.message, job.attrs.name, job.attrs.data)

    const scheduleIn = err.waitFor
      ? 'in ' + err.waitFor + ' seconds'
      : 'in ' + config.assets[err.asset].blockTime // TODO: blocktime should probably be per chain and in cryptoassets

    job.schedule(scheduleIn)
    return job.save()
  }

  const httpData = {
    req: {
      url: getRequestUrl(err),
      data: getRequestData(err),
      params: getRequestParams(err)
    },
    res: {
      statusCode: getStatusCode(err),
      body: getResponseBody(err)
    }
  }

  debug(
    '[failed]',
    err.name,
    err.message,
    job.attrs,
    httpData
  )

  Sentry.withScope(scope => {
    scope.setTag('httpUrl', httpData.req.url)
    scope.setTag('httpResponseStatusCode', httpData.res.statusCode)
    scope.setTag('jobName', _.get(job, 'attrs.name'))
    scope.setTag('orderId', _.get(job, 'attrs.data.orderId'))

    scope.setExtra('attrs', job.attrs)
    scope.setExtra('httpRequestData', httpData.req.data)
    scope.setExtra('httpRequestParams', httpData.req.params)
    scope.setExtra('httpResponseBody', httpData.res.responseBody)

    Sentry.captureException(err)
  })

  job.fail(err)
  return job.save()
}


<|start_filename|>src/api/routes/dash.js<|end_filename|>
const asyncHandler = require('express-async-handler')
const router = require('express').Router()
const { fromUnixTime, differenceInDays, parseISO, compareAsc, eachDayOfInterval, format } = require('date-fns')

const Market = require('../../models/Market')
const Order = require('../../models/Order')
const MarketHistory = require('../../models/MarketHistory')

const addressHashVariants = address => {
  const addressLowerCase = address.toLowerCase()

  const arr = [address, addressLowerCase]

  if (/0x/i.test(address)) {
    arr.push(...arr.map(item => item.replace(/^0x/, '')))
  } else {
    arr.push(...arr.map(item => `0x${item}`))
  }

  return [...new Set(arr)]
}

const getLimitPageSort = ({ limit, page, sort }, defaultSortBy = '-createdAt') => {
  try {
    page = parseInt(page)
    if (!page || page < 1) throw new Error('Invalid page')
  } catch (e) {
    page = 1
  }

  try {
    limit = parseInt(limit)
    if (!limit || limit < 1 || limit > 25) throw new Error('Invalid limit')
  } catch (e) {
    limit = 25
  }

  if (!sort) sort = defaultSortBy

  return {
    limit,
    page,
    sort
  }
}

const createQuery = _query => {
  const { q, from, to, start, end, status, excludeStatus, userAgent, pending } = _query
  const query = {}

  if (userAgent && userAgent.length !== 2) {
    if (userAgent[0] === 'WALLET') {
      query.userAgent = {
        $regex: /^Wallet/i
      }
    } else {
      query.userAgent = {
        $regex: /^SwapUI/i
      }
    }
  }

  if (q) {
    const inAddresses = { $in: addressHashVariants(q) }

    query.$or = [
      { orderId: inAddresses },

      { fromCounterPartyAddress: inAddresses },
      { toCounterPartyAddress: inAddresses },
      { fromAddress: inAddresses },
      { toAddress: inAddresses },

      { fromFundHash: inAddresses },
      { fromSecondaryFundHash: inAddresses },
      { fromClaimHash: inAddresses },
      { toFundHash: inAddresses },
      { toSecondaryFundHash: inAddresses },
      { toRefundHash: inAddresses },

      { secretHash: inAddresses }
    ]
  }

  if (from && from.length > 0) {
    query.from = { $in: from }
  }

  if (to && to.length > 0) {
    query.to = { $in: to }
  }

  if (status && status.length > 0) {
    query.status = { $in: status }
  } else if (excludeStatus && excludeStatus.length > 0) {
    query.status = { $nin: excludeStatus }
  }

  if (start) {
    query.createdAt = { $gte: new Date(Number(start)) }
  }

  if (end) {
    if (!query.createdAt) query.createdAt = {}
    query.createdAt.$lte = new Date(Number(end))
  }

  if (pending) {
    if (pending.length === 2) {
      query.hasUnconfirmedTx = true
    } else if (pending[0] === 'USER') {
      query.hasUserUnconfirmedTx = true
    } else {
      query.hasAgentUnconfirmedTx = true
    }
  }

  return query
}

router.get('/orders', asyncHandler(async (req, res) => {
  const query = createQuery(req.query)
  const { limit, page, sort } = getLimitPageSort(req.query)

  const result = await Order.find(query, null, {
    sort,
    skip: limit * (page - 1),
    limit
  }).lean().exec()

  res.json({
    page,
    count: result.length,
    result
  })
}))

router.get('/rate', asyncHandler(async (req, res) => {
  const { market, timestamp } = req.query

  const rate = await MarketHistory.getRateNear(market, timestamp)

  res.json({
    result: rate
  })
}))

router.get('/statsByAddress', asyncHandler(async (req, res) => {
  const { address } = req.query

  const inAddresses = { $in: addressHashVariants(address) }

  const _result = await Order.aggregate([
    {
      $match: {
        status: 'AGENT_CLAIMED',
        $or: [
          { fromCounterPartyAddress: inAddresses },
          { toCounterPartyAddress: inAddresses },
          { fromAddress: inAddresses },
          { toAddress: inAddresses }
        ]
      }
    },
    {
      $group: {
        _id: null,
        'sum:fromAmountUsd': { $sum: '$fromAmountUsd' },
        'sum:toAmountUsd': { $sum: '$toAmountUsd' },
        count: { $sum: 1 }
      }
    }
  ]).exec()

  const result = _result[0] || {}

  res.json({
    address,
    result
  })
}))

router.get('/topAddresses', asyncHandler(async (req, res) => {
  const { limit, page, sort } = getLimitPageSort(req.query, 'volume')

  const sortKey = sort.endsWith('volume')
    ? 'sum:fromAmountUsd'
    : 'count'

  const _result = await Order.aggregate([
    {
      $match: {
        status: 'AGENT_CLAIMED'
      }
    },
    {
      $addFields: {
        market: { $concat: ['$from', '-', '$to'] }
      }
    },
    {
      $group: {
        _id: '$fromAddress',
        'sum:fromAmountUsd': { $sum: '$fromAmountUsd' },
        'sum:toAmountUsd': { $sum: '$toAmountUsd' },
        markets: { $addToSet: '$market' },
        count: { $sum: 1 }
      }
    },
    {
      $sort: { [sortKey]: sort.startsWith('-') ? -1 : 1 }
    },
    {
      $skip: limit * (page - 1)
    },
    {
      $limit: limit
    }
  ]).exec()

  const result = _result.map(r => {
    r.address = r._id
    delete r._id

    return r
  })

  res.json({
    count: result.length,
    result
  })
}))

router.get('/stats', asyncHandler(async (req, res) => {
  const query = createQuery({
    ...req.query,
    status: ['AGENT_CLAIMED']
  })
  const markets = (await Market.find({}, 'from to').lean().exec()).map(market => `${market.from}-${market.to}`)

  const $group = markets.reduce((acc, market) => {
    acc[`market:${market}:sum:fromAmountUsd`] = { $sum: { $cond: [{ $eq: ['$market', market] }, '$fromAmountUsd', 0] } }
    acc[`market:${market}:sum:toAmountUsd`] = { $sum: { $cond: [{ $eq: ['$market', market] }, '$toAmountUsd', 0] } }
    acc[`market:${market}:count`] = { $sum: { $cond: [{ $eq: ['$market', market] }, 1, 0] } }

    return acc
  }, {})

  const result = await Order.aggregate([
    {
      $match: query
    },
    {
      $addFields: {
        market: { $concat: ['$from', '-', '$to'] },
        date: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }
      }
    },
    {
      $group: {
        _id: '$date',
        ...$group,
        'wallet:sum:fromAmountUsd': { $sum: { $cond: [{ $regexMatch: { input: '$userAgent', regex: /^Wallet/i } }, '$fromAmountUsd', 0] } },
        'wallet:sum:toAmountUsd': { $sum: { $cond: [{ $regexMatch: { input: '$userAgent', regex: /^Wallet/i } }, '$toAmountUsd', 0] } },
        'wallet:count': { $sum: { $cond: [{ $regexMatch: { input: '$userAgent', regex: /^Wallet/i } }, 1, 0] } },
        'sum:totalAgentFeeUsd': { $sum: '$totalAgentFeeUsd' },
        'sum:totalUserFeeUsd': { $sum: '$totalUserFeeUsd' },
        'sum:fromAmountUsd': { $sum: '$fromAmountUsd' },
        'sum:toAmountUsd': { $sum: '$toAmountUsd' },
        count: { $sum: 1 }
      }
    }
  ]).exec()

  const stats = result.map(json => {
    json.date = json._id
    delete json._id

    json.markets = Object.entries(json)
      .filter(([key]) => key.startsWith('market:'))
      .reduce((acc, [key, value]) => {
        const arr = key.split(':')
        arr.shift() // discard 'market'

        const market = arr.shift()
        const type = arr.join(':')

        if (!acc[market]) acc[market] = {}
        acc[market][type] = value

        return acc
      }, {})

    return json
  })

  const emptyDataPoint = {
    'wallet:sum:fromAmountUsd': 0,
    'wallet:sum:toAmountUsd': 0,
    'wallet:count': 0,
    'sum:totalAgentFeeUsd': 0,
    'sum:totalUserFeeUsd': 0,
    'sum:fromAmountUsd': 0,
    'sum:toAmountUsd': 0,
    count: 0
  }

  eachDayOfInterval({
    start: new Date(Number(req.query.start)),
    end: new Date(Number(req.query.end))
  }).forEach(date => {
    date = format(date, 'yyyy-MM-dd')
    if (stats.find(stat => stat.date === date)) return

    stats.push({
      date,
      ...emptyDataPoint
    })
  })

  stats.sort((a, b) => compareAsc(parseISO(a.date), parseISO(b.date)))

  res.json({
    count: result.length,
    result: {
      markets,
      stats
    }
  })
}))

router.get('/rates', asyncHandler(async (req, res) => {
  const { market, start, end } = req.query

  if (!market) return res.notOk(400, 'Value not specified: market')
  if (!start) return res.notOk(400, 'Value not specified: start')
  if (!end) return res.notOk(400, 'Value not specified: end')
  if (start >= end) return res.notOk(400, 'Invalid values: start should be <= end')

  const diff = differenceInDays(fromUnixTime(end), fromUnixTime(start))
  if (diff > 30) return res.notOk(400, 'Range cannot exceed 30 days')

  const result = await MarketHistory.getRates(market, start, end)

  res.json({
    count: result.length,
    result
  })
}))

router.get('/accumulate', asyncHandler(async (req, res) => {
  const [result] = await Order.aggregate([
    {
      $match: {
        status: 'AGENT_CLAIMED'
      }
    },
    {
      $group: {
        _id: null,
        'sum:fromAmountUsd': { $sum: '$fromAmountUsd' },
        'sum:toAmountUsd': { $sum: '$toAmountUsd' },
        count: { $sum: 1 }
      }
    }
  ]).exec()

  res.json({
    result
  })
}))

module.exports = router


<|start_filename|>src/api/routes/user.js<|end_filename|>
const asyncHandler = require('express-async-handler')
const router = require('express').Router()
const { spawn } = require('child_process')
const { parseArgsStringToArgv } = require('string-argv')

const config = require('../../config')
const Check = require('../../models/Check')
const Order = require('../../models/Order')

const ensureAuth = require('../../middlewares/ensureAuth')

const ALLOWED_TYPES = [
  'reciprocate-init-swap'
]

const ALLOWED_ACTIONS = [
  'approve',
  'reject'
]

const ALLOWED_RETRY_JOBS = [
  {
    name: 'reciprocate-init-swap',
    setStatus: 'USER_FUNDED'
  },
  {
    name: 'fund-swap',
    setStatus: 'AGENT_CONTRACT_CREATED'
  },
  {
    name: 'find-claim-tx-or-refund',
    setStatus: 'AGENT_FUNDED'
  },
  {
    name: 'agent-claim',
    setStatus: 'USER_CLAIMED'
  }
]

router.post('/login', asyncHandler(async (req, res) => {
  const { body } = req

  if (body.password !== config.auth.simplePassword) {
    return res.notOk(401, 'Invalid password')
  }

  req.session.authAt = Date.now()

  res.ok()
}))

router.post('/logout', asyncHandler(async (req, res) => {
  await new Promise(resolve => {
    if (req.session) {
      req.session.destroy(_ => {
        resolve()
      })
    } else {
      resolve()
    }
  })

  res.ok()
}))

router.post('/killswitch', ensureAuth(401), asyncHandler(async (req, res) => {
  const arr = parseArgsStringToArgv(config.worker.killswitch)
  spawn(arr.shift(), arr, { stdio: 'inherit' }) // TODO: find a better way
  res.ok()
}))

router.get('/', ensureAuth(200), asyncHandler(async (req, res) => {
  res.ok()
}))

router.get('/order', ensureAuth(401), asyncHandler(async (req, res) => {
  const { query } = req
  const { orderId } = query

  if (!orderId) {
    return res.notOk(400, 'Order ID missing')
  }

  const order = await Order.findOne({ orderId: orderId }).exec()
  if (!order) {
    return res.notOk(400, `Order not found: ${orderId}`)
  }

  const check = await Check.findOne({ orderId: orderId }).exec()
  res.ok(check)
}))

router.post('/order/retry', ensureAuth(401), asyncHandler(async (req, res) => {
  const agenda = req.app.get('agenda')
  const { body } = req
  const { orderId, jobName } = body

  if (!orderId) {
    return res.notOk(400, 'Order ID missing')
  }

  if (!ALLOWED_RETRY_JOBS.find(job => job.name === jobName)) {
    return res.notOk(400, `Invalid job name: ${jobName}`)
  }

  const order = await Order.findOne({ orderId: orderId }).exec()
  if (!order) {
    return res.notOk(400, `Order not found: ${orderId}`)
  }

  const index = ALLOWED_RETRY_JOBS.findIndex(job => job.name === jobName)
  const jobsToBeRemoved = ALLOWED_RETRY_JOBS.slice(index).map(job => job.name)

  await agenda.cancel({
    name: {
      $in: jobsToBeRemoved
    },
    'data.orderId': orderId
  })

  order.status = ALLOWED_RETRY_JOBS[index].setStatus
  await order.save()

  await agenda.now(jobName, { orderId: order.orderId })

  await order.log('RETRY', jobName)

  res.ok()
}))

router.post('/order/ignore', ensureAuth(401), asyncHandler(async (req, res) => {
  const { body } = req
  const { orderId, setQuoteExpired } = body

  if (!orderId) {
    return res.notOk(400, 'Order ID missing')
  }

  const order = await Order.findOne({ orderId: orderId }).exec()
  if (!order) {
    return res.notOk(400, `Order not found: ${orderId}`)
  }

  Object
    .entries(order.txMap)
    .forEach(([key, value]) => {
      if (!value.blockHash) {
        order.set(`txMap.${key}.replacedBy`, true)
      }
    })

  if (setQuoteExpired) {
    order.status = 'QUOTE_EXPIRED'
  }

  await order.save()

  await order.log('IGNORE')

  res.ok()
}))

router.post('/order', ensureAuth(401), asyncHandler(async (req, res) => {
  const { body } = req
  const { orderId, type, message, action } = body

  if (!orderId) {
    return res.notOk(400, 'Order ID missing')
  }

  if (!ALLOWED_TYPES.includes(type)) {
    return res.notOk(400, `Invalid type: ${type}`)
  }

  if (!ALLOWED_ACTIONS.includes(action)) {
    return res.notOk(400, `Invalid action: ${action}`)
  }

  const order = await Order.findOne({ orderId: orderId }).exec()
  if (!order) {
    return res.notOk(400, `Order not found: ${orderId}`)
  }

  const check = await Check.getCheckForOrder(orderId)

  const entry = check.get(`flags.${type}`) || {}
  if (entry.reject) {
    return res.notOk(400, `Check ${orderId}:${type} has already been rejected: ${entry.message}`)
  }

  if (entry.approve) {
    return res.notOk(400, `Check ${orderId}:${type} has already been approved: ${entry.message}`)
  }

  check.set(`flags.${type}`, {
    [action]: new Date(),
    message
  })

  await check.save()

  await order.log('AUTH', action === 'approve' ? 'APPROVED' : 'REJECTED', { type, message, action })

  res.ok()
}))

module.exports = router
