Skip to content

coinprices/PriceResponseParsers.cpp

Namespaces

Name
sgns

Functions

Name
PriceResult< std::vector< PriceQuote > > ParseCoinGeckoSimplePrice(const std::string & body, const std::vector< std::string > & tokenIds, const std::string & currency, std::chrono::system_clock::time_point fetchTime)
Parse a CoinGecko /simple/price 200-body into quotes.
PriceResult< std::vector< PriceQuote > > ParseGnusPriceEnvelope(const std::string & body, const std::vector< std::string > & tokenIds, const std::string & currency)
Parse a token.gnus.ai price envelope into quotes.

Functions Documentation

function ParseCoinGeckoSimplePrice

PriceResult< std::vector< PriceQuote > > ParseCoinGeckoSimplePrice(
    const std::string & body,
    const std::vector< std::string > & tokenIds,
    const std::string & currency,
    std::chrono::system_clock::time_point fetchTime
)

Parse a CoinGecko /simple/price 200-body into quotes.

Parameters:

  • body The 200 response body
  • tokenIds Requested ids — quotes are emitted for requested ids present in the body; unknown ids stay absent, not errors (D-09)
  • currency Target currency
  • fetchTime D-14 fetch time stamped on every quote

Return: One PriceQuote per requested id found in the body, or NoDataFound when none matched, or JsonParseError on malformed JSON (status 0 — the HTTP status belongs to the facade layer)

function ParseGnusPriceEnvelope

PriceResult< std::vector< PriceQuote > > ParseGnusPriceEnvelope(
    const std::string & body,
    const std::vector< std::string > & tokenIds,
    const std::string & currency
)

Parse a token.gnus.ai price envelope into quotes.

Parameters:

  • body The 200 response body (Phase-1 envelope.ts contract)
  • tokenIds Requested ids — quotes are emitted for requested ids present in prices; absent ids stay absent, not errors (D-09)
  • currency Expected target currency (envelope currency)

Return: One PriceQuote per requested id found in prices, or NoDataFound when none matched, or JsonParseError on malformed JSON / an unknown source string (status 0 — the HTTP status belongs to the facade layer)

Note:

  • LANDMINE: envelope fetchedAt is epoch SECONDS (never ms) and is the max across the returned ids, SHARED by all quotes — do not invent per-id timestamps (Phase-1 D-11).
  • LANDMINE: the envelope source names the upstream that PRODUCED the price, not the serving tier — "coingecko" maps to PriceSource::CoinGecko and "coingecko-cache" to PriceSource::GnusPriceService (Phase-1 D-06). Counterintuitive but contractual; do NOT "fix" this.

Source code

#include "PriceResponseParsers.hpp"

#include <rapidjson/document.h>

#include <utility>

namespace sgns
{
    PriceResult<std::vector<PriceQuote>> ParseCoinGeckoSimplePrice( const std::string              &body,
                                                                    const std::vector<std::string> &tokenIds,
                                                                    const std::string              &currency,
                                                                    std::chrono::system_clock::time_point fetchTime )
    {
        rapidjson::Document document;
        document.Parse( body.c_str() );
        if ( document.HasParseError() )
        {
            return outcome::failure( PriceFetchFailure{ PriceFetchError::JsonParseError, 0 } );
        }

        std::vector<PriceQuote> quotes;
        for ( const auto &id : tokenIds )
        {
            // IsNumber covers int and double literals — CoinGecko may
            // serialize whole-number prices without a decimal point
            if ( document.IsObject() && document.HasMember( id.c_str() ) && document[id.c_str()].IsObject()
                 && document[id.c_str()].HasMember( currency.c_str() )
                 && document[id.c_str()][currency.c_str()].IsNumber() )
            {
                PriceQuote quote;
                quote.asset     = id;
                quote.currency  = currency;
                quote.price     = document[id.c_str()][currency.c_str()].GetDouble();
                quote.timestamp = fetchTime; // D-14: fetch time
                quote.source    = PriceSource::CoinGecko;
                quote.stale     = false;
                quotes.push_back( std::move( quote ) );
            }
            // Unknown ids stay absent — partial coverage, not an error
        }

        if ( quotes.empty() )
        {
            return outcome::failure( PriceFetchFailure{ PriceFetchError::NoDataFound, 0 } );
        }
        return outcome::success( std::move( quotes ) );
    }

    PriceResult<std::vector<PriceQuote>> ParseGnusPriceEnvelope( const std::string              &body,
                                                                 const std::vector<std::string> &tokenIds,
                                                                 const std::string              &currency )
    {
        rapidjson::Document document;
        document.Parse( body.c_str() );
        if ( document.HasParseError() || !document.IsObject() )
        {
            return outcome::failure( PriceFetchFailure{ PriceFetchError::JsonParseError, 0 } );
        }
        if ( !document.HasMember( "currency" ) || !document["currency"].IsString()
             || !document.HasMember( "prices" ) || !document["prices"].IsObject()
             || !document.HasMember( "fetchedAt" ) || !document["fetchedAt"].IsInt64()
             || !document.HasMember( "source" ) || !document["source"].IsString()
             || !document.HasMember( "stale" ) || !document["stale"].IsBool() )
        {
            return outcome::failure( PriceFetchFailure{ PriceFetchError::JsonParseError, 0 } );
        }

        // LANDMINE (Phase-1 D-11): fetchedAt is epoch SECONDS and the max
        // across the returned ids — one shared timestamp for all quotes.
        const auto fetchedAt = std::chrono::system_clock::time_point{
            std::chrono::seconds{ document["fetchedAt"].GetInt64() }
        };

        // LANDMINE (Phase-1 D-06): exactly two source kinds exist; the
        // envelope source names the producing upstream, NOT the serving
        // tier. Unknown source strings are malformed, not silently mapped.
        const std::string source( document["source"].GetString() );
        PriceSource       quoteSource;
        if ( source == "coingecko" )
        {
            quoteSource = PriceSource::CoinGecko;
        }
        else if ( source == "coingecko-cache" )
        {
            quoteSource = PriceSource::GnusPriceService;
        }
        else
        {
            return outcome::failure( PriceFetchFailure{ PriceFetchError::JsonParseError, 0 } );
        }

        const bool envelopeStale = document["stale"].GetBool();
        const auto &prices       = document["prices"];

        std::vector<PriceQuote> quotes;
        for ( const auto &id : tokenIds )
        {
            if ( prices.HasMember( id.c_str() ) && prices[id.c_str()].IsNumber() )
            {
                PriceQuote quote;
                quote.asset     = id;
                quote.currency  = currency;
                quote.price     = prices[id.c_str()].GetDouble();
                quote.timestamp = fetchedAt; // shared max-of-ids timestamp (D-11)
                quote.source    = quoteSource;
                quote.stale     = envelopeStale;
                quotes.push_back( std::move( quote ) );
            }
            // Absent ids stay absent — partial coverage, not an error (D-09)
        }

        if ( quotes.empty() )
        {
            return outcome::failure( PriceFetchFailure{ PriceFetchError::NoDataFound, 0 } );
        }
        return outcome::success( std::move( quotes ) );
    }
} // namespace sgns

Updated on 2026-10-09 at 04:02:50 +0000