OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / core / core / relay-node / api-credits.js

188 lines · javascript ref:
import { PricingEngine } from '../../incentive/credits/pricing.js'

export const MAX_CREDITS_WALLETS = 100

const DEFAULT_PRICING_ENGINE = new PricingEngine()

const CREDITS_ROUTES = Object.freeze({
  'GET /api/v1/credits/pricing': Object.freeze({
    kind: 'credits-pricing',
    requiresAuth: false
  }),
  'GET /api/v1/credits/pricing/compare': Object.freeze({
    kind: 'credits-pricing-compare',
    requiresAuth: false
  }),
  'GET /api/v1/credits/stats': Object.freeze({
    kind: 'credits-stats',
    requiresAuth: true,
    authMessage: 'Unauthorized — credits stats require API key or localhost'
  }),
  'GET /api/v1/credits/wallets': Object.freeze({
    kind: 'credits-wallets',
    requiresAuth: true,
    authMessage: 'Unauthorized — credits wallets require API key or localhost'
  })
})

export function resolveCreditsRoute (method, path) {
  const route = CREDITS_ROUTES[`${method} ${path}`]
  if (!route) return null
  return { ...route }
}

export function buildCreditsPricingRoutePayload ({ route, node } = {}) {
  if (!route || route.kind !== 'credits-pricing') return notFound(route)
  const engine = pricingEngine(node)
  return {
    ok: true,
    status: 200,
    payload: engine.getRateCard()
  }
}

export function buildCreditsCompareRoutePayload ({ route, node } = {}) {
  if (!route || route.kind !== 'credits-pricing-compare') return notFound(route)
  const engine = pricingEngine(node)
  return {
    ok: true,
    status: 200,
    payload: engine.getComparison()
  }
}

export function buildCreditsStatsRoutePayload ({ route, node } = {}) {
  if (!route || route.kind !== 'credits-stats') return notFound(route)
  const manager = creditManager(node)
  if (!manager || typeof manager.stats !== 'function') return creditsUnavailable()

  let stats
  try {
    stats = manager.stats()
  } catch {
    return creditsUnavailable()
  }

  return {
    ok: true,
    status: 200,
    payload: {
      credits: {
        totalWallets: finiteCount(stats && stats.totalWallets),
        totalBalance: finiteAmount(stats && stats.totalBalance),
        totalDeposited: finiteAmount(stats && stats.totalDeposited),
        totalSpent: finiteAmount(stats && stats.totalSpent),
        totalWelcomeCredits: finiteAmount(stats && stats.totalWelcomeCredits),
        frozenWallets: finiteCount(stats && stats.frozenWallets),
        avgBalance: finiteAmount(stats && stats.avgBalance)
      }
    }
  }
}

export function buildCreditsWalletsRoutePayload ({ route, node } = {}) {
  if (!route || route.kind !== 'credits-wallets') return notFound(route)
  const manager = creditManager(node)
  if (!manager || !manager.wallets || typeof manager.wallets.values !== 'function') {
    return creditsUnavailable()
  }

  let wallets
  try {
    wallets = Array.from(manager.wallets.values(), sanitizeWallet)
  } catch {
    return creditsUnavailable()
  }

  wallets.sort(compareWallets)
  const total = wallets.length
  if (wallets.length > MAX_CREDITS_WALLETS) wallets.length = MAX_CREDITS_WALLETS

  return {
    ok: true,
    status: 200,
    payload: {
      wallets,
      total,
      truncated: total > wallets.length
    }
  }
}

function pricingEngine (node) {
  const engine = node && node.pricingEngine
  return engine && typeof engine.getRateCard === 'function' && typeof engine.getComparison === 'function'
    ? engine
    : DEFAULT_PRICING_ENGINE
}

function creditManager (node) {
  if (!node) return null
  const manager = node.creditManager
  return manager || null
}

function sanitizeWallet (wallet) {
  const value = wallet && typeof wallet === 'object' ? wallet : {}
  return {
    appPubkey: boundedString(value.appPubkey, 128),
    balance: finiteAmount(value.balance),
    totalDeposited: finiteAmount(value.totalDeposited),
    totalSpent: finiteAmount(value.totalSpent),
    totalBonusReceived: finiteAmount(value.totalBonusReceived),
    welcomeCreditsReceived: finiteAmount(value.welcomeCreditsReceived),
    tier: creditTier(value.tier),
    lastActivity: finiteTimestamp(value.lastActivity),
    createdAt: finiteTimestamp(value.createdAt)
  }
}

function compareWallets (left, right) {
  const activity = (right.lastActivity || 0) - (left.lastActivity || 0)
  if (activity !== 0) return activity
  return String(left.appPubkey || '').localeCompare(String(right.appPubkey || ''))
}

function creditTier (value) {
  if (value === 'standard' || value === 'unlimited') return value
  return 'free'
}

function boundedString (value, maxBytes) {
  if (typeof value !== 'string') return null
  return value.length <= maxBytes ? value : value.slice(0, maxBytes)
}

function finiteAmount (value) {
  const number = Number(value)
  return Number.isFinite(number) && number >= 0 ? number : 0
}

function finiteCount (value) {
  return Math.floor(finiteAmount(value))
}

function finiteTimestamp (value) {
  const number = Number(value)
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : null
}

function creditsUnavailable () {
  return {
    ok: false,
    status: 503,
    payload: {
      error: 'credits runtime unavailable',
      errorCode: 'credits-unavailable'
    }
  }
}

function notFound (route) {
  return {
    ok: false,
    status: 404,
    payload: { error: 'unknown credits route', route: route && route.kind }
  }
}