The real backend returns syncstatus as idle {"syncing":"false"} (string) or in-progress
{"syncing":"true","synced_blocks":N,"total_blocks":M} (commands.rs:83-87), but
parseLiteSyncStatusResponse hard-required the block fields and failed whenever the wallet
wasn't actively syncing — so sync/progress never updated in the real app.
- Read "syncing" as a string; require synced_blocks/total_blocks only when syncing=true;
idle => complete, synced/total 0.
- fake_lite_backend syncstatus now uses the real "syncing":"true" shape.
- testLiteSyncStatusParserRealShapes covers idle, in-progress, and missing-counts-while-syncing.
- Verified against the live backend via lite_smoke --refresh (syncstatus parse_ok=1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
586 lines
23 KiB
C++
586 lines
23 KiB
C++
#include "wallet/lite_result_parsers.h"
|
|
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
|
|
namespace dragonx::wallet {
|
|
namespace {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
std::string fieldPath(const std::string& key)
|
|
{
|
|
return "$.'" + key + "'";
|
|
}
|
|
|
|
std::string indexPath(const std::string& parentPath, std::size_t index)
|
|
{
|
|
return parentPath + "[" + std::to_string(index) + "]";
|
|
}
|
|
|
|
template <typename Result>
|
|
Result makeResult(LiteResultCommand command)
|
|
{
|
|
Result result;
|
|
result.command = command;
|
|
return result;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool fail(Result& result,
|
|
LiteResultParserError error,
|
|
const std::string& path,
|
|
const std::string& message)
|
|
{
|
|
result.ok = false;
|
|
result.error = error;
|
|
result.errorPath = path;
|
|
result.errorMessage = message;
|
|
result.issues.push_back({path, message});
|
|
return false;
|
|
}
|
|
|
|
template <typename Result>
|
|
void succeed(Result& result)
|
|
{
|
|
result.ok = true;
|
|
result.error = LiteResultParserError::None;
|
|
result.errorPath.clear();
|
|
result.errorMessage.clear();
|
|
}
|
|
|
|
template <typename Result>
|
|
bool parseJsonText(const std::string& jsonText, Result& result, json& parsed)
|
|
{
|
|
parsed = json::parse(jsonText, nullptr, false);
|
|
if (parsed.is_discarded()) {
|
|
return fail(result, LiteResultParserError::InvalidJson, "$", "response is not valid JSON");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool requireObject(const json& value, const std::string& path, Result& result)
|
|
{
|
|
if (!value.is_object()) {
|
|
return fail(result, LiteResultParserError::ExpectedObject, path, "expected a JSON object");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool requireArray(const json& value, const std::string& path, Result& result)
|
|
{
|
|
if (!value.is_array()) {
|
|
return fail(result, LiteResultParserError::ExpectedArray, path, "expected a JSON array");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool jsonToUnsigned(const json& value, std::uint64_t& output)
|
|
{
|
|
try {
|
|
if (value.is_number_unsigned()) {
|
|
output = value.get<std::uint64_t>();
|
|
return true;
|
|
}
|
|
if (value.is_number_integer()) {
|
|
const auto signedValue = value.get<std::int64_t>();
|
|
if (signedValue < 0) return false;
|
|
output = static_cast<std::uint64_t>(signedValue);
|
|
return true;
|
|
}
|
|
if (value.is_string()) {
|
|
const auto text = value.get<std::string>();
|
|
if (text.empty()) return false;
|
|
std::size_t parsedSize = 0;
|
|
const auto parsed = std::stoull(text, &parsedSize, 10);
|
|
if (parsedSize != text.size()) return false;
|
|
output = static_cast<std::uint64_t>(parsed);
|
|
return true;
|
|
}
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool jsonToSigned(const json& value, std::int64_t& output)
|
|
{
|
|
try {
|
|
if (value.is_number_integer()) {
|
|
output = value.get<std::int64_t>();
|
|
return true;
|
|
}
|
|
if (value.is_number_unsigned()) {
|
|
const auto unsignedValue = value.get<std::uint64_t>();
|
|
if (unsignedValue > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
|
|
return false;
|
|
}
|
|
output = static_cast<std::int64_t>(unsignedValue);
|
|
return true;
|
|
}
|
|
if (value.is_string()) {
|
|
const auto text = value.get<std::string>();
|
|
if (text.empty()) return false;
|
|
std::size_t parsedSize = 0;
|
|
const auto parsed = std::stoll(text, &parsedSize, 10);
|
|
if (parsedSize != text.size()) return false;
|
|
output = static_cast<std::int64_t>(parsed);
|
|
return true;
|
|
}
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readRequiredStringField(const json& object,
|
|
const std::string& key,
|
|
std::string& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) {
|
|
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required string field");
|
|
}
|
|
if (!object.at(key).is_string()) {
|
|
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a string");
|
|
}
|
|
output = object.at(key).get<std::string>();
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readOptionalStringField(const json& object,
|
|
const std::string& key,
|
|
std::optional<std::string>& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) return true;
|
|
if (!object.at(key).is_string()) {
|
|
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a string");
|
|
}
|
|
output = object.at(key).get<std::string>();
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readOptionalStringField(const json& object,
|
|
const std::string& key,
|
|
std::string& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) return true;
|
|
if (!object.at(key).is_string()) {
|
|
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a string");
|
|
}
|
|
output = object.at(key).get<std::string>();
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readRequiredSignedField(const json& object,
|
|
const std::string& key,
|
|
std::int64_t& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) {
|
|
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required integer field");
|
|
}
|
|
if (!jsonToSigned(object.at(key), output)) {
|
|
return fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "expected a signed integer-compatible value");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readRequiredUnsignedField(const json& object,
|
|
const std::string& key,
|
|
std::uint64_t& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) {
|
|
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required unsigned integer field");
|
|
}
|
|
if (!jsonToUnsigned(object.at(key), output)) {
|
|
return fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "expected an unsigned integer-compatible value");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readOptionalSignedField(const json& object,
|
|
const std::string& key,
|
|
std::optional<std::int64_t>& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) return true;
|
|
std::int64_t parsed = 0;
|
|
if (!jsonToSigned(object.at(key), parsed)) {
|
|
return fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "expected a signed integer-compatible value");
|
|
}
|
|
output = parsed;
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readOptionalBoolField(const json& object,
|
|
const std::string& key,
|
|
bool& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) return true;
|
|
if (!object.at(key).is_boolean()) {
|
|
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a boolean");
|
|
}
|
|
output = object.at(key).get<bool>();
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool readStringArrayField(const json& object,
|
|
const std::string& key,
|
|
std::vector<std::string>& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) {
|
|
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required array field");
|
|
}
|
|
const auto& values = object.at(key);
|
|
if (!requireArray(values, fieldPath(key), result)) return false;
|
|
|
|
output.clear();
|
|
for (std::size_t index = 0; index < values.size(); ++index) {
|
|
if (!values[index].is_string()) {
|
|
return fail(result, LiteResultParserError::InvalidFieldType, indexPath(fieldPath(key), index), "expected a string array item");
|
|
}
|
|
output.push_back(values[index].get<std::string>());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool parseSpendableOutput(const json& value,
|
|
LiteSpendableOutputKind kind,
|
|
const std::string& path,
|
|
LiteSpendableOutput& output,
|
|
Result& result)
|
|
{
|
|
if (!requireObject(value, path, result)) return false;
|
|
|
|
output.kind = kind;
|
|
output.pending = kind == LiteSpendableOutputKind::PendingNote || kind == LiteSpendableOutputKind::PendingUtxo;
|
|
if (!readRequiredStringField(value, "address", output.address, result)) return false;
|
|
if (!readRequiredStringField(value, "created_in_txid", output.createdInTxid, result)) return false;
|
|
if (!readOptionalSignedField(value, "created_in_block", output.createdInBlock, result)) return false;
|
|
if (!readRequiredUnsignedField(value, "value", output.value, result)) return false;
|
|
if (!readOptionalBoolField(value, "spent", output.spent, result)) return false;
|
|
if (!readOptionalBoolField(value, "unconfirmed_spent", output.unconfirmedSpent, result)) return false;
|
|
output.spendable = !output.pending && !output.spent && !output.unconfirmedSpent;
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool parseSpendableOutputArray(const json& object,
|
|
const std::string& key,
|
|
LiteSpendableOutputKind kind,
|
|
std::vector<LiteSpendableOutput>& output,
|
|
Result& result)
|
|
{
|
|
if (!object.contains(key) || object.at(key).is_null()) {
|
|
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required spendable output array");
|
|
}
|
|
const auto& values = object.at(key);
|
|
if (!requireArray(values, fieldPath(key), result)) return false;
|
|
|
|
output.clear();
|
|
for (std::size_t index = 0; index < values.size(); ++index) {
|
|
LiteSpendableOutput parsed;
|
|
if (!parseSpendableOutput(values[index], kind, indexPath(fieldPath(key), index), parsed, result)) return false;
|
|
output.push_back(std::move(parsed));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool parseTransactionOutput(const json& value,
|
|
const std::string& path,
|
|
LiteTransactionOutput& output,
|
|
Result& result)
|
|
{
|
|
if (!requireObject(value, path, result)) return false;
|
|
if (!readRequiredStringField(value, "address", output.address, result)) return false;
|
|
if (!readRequiredSignedField(value, "value", output.value, result)) return false;
|
|
if (!readOptionalStringField(value, "memo", output.memo, result)) return false;
|
|
return true;
|
|
}
|
|
|
|
template <typename Result>
|
|
bool parseTransactionRecord(const json& value,
|
|
const std::string& path,
|
|
LiteTransactionRecord& output,
|
|
Result& result)
|
|
{
|
|
if (!requireObject(value, path, result)) return false;
|
|
if (!readRequiredStringField(value, "txid", output.txid, result)) return false;
|
|
if (!readRequiredSignedField(value, "datetime", output.datetime, result)) return false;
|
|
if (!readOptionalSignedField(value, "block_height", output.blockHeight, result)) return false;
|
|
if (!readOptionalBoolField(value, "unconfirmed", output.unconfirmed, result)) return false;
|
|
|
|
if (value.contains("outgoing_metadata") && !value.at("outgoing_metadata").is_null()) {
|
|
const auto& metadata = value.at("outgoing_metadata");
|
|
if (!requireArray(metadata, path + ".outgoing_metadata", result)) return false;
|
|
output.direction = LiteTransactionDirection::Send;
|
|
for (std::size_t index = 0; index < metadata.size(); ++index) {
|
|
LiteTransactionOutput parsedOutput;
|
|
if (!parseTransactionOutput(metadata[index], path + ".outgoing_metadata[" + std::to_string(index) + "]", parsedOutput, result)) return false;
|
|
output.amount += parsedOutput.value;
|
|
output.outgoingMetadata.push_back(std::move(parsedOutput));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
output.direction = LiteTransactionDirection::Receive;
|
|
if (!readRequiredStringField(value, "address", output.address, result)) return false;
|
|
if (!readRequiredSignedField(value, "amount", output.amount, result)) return false;
|
|
if (!readOptionalStringField(value, "memo", output.memo, result)) return false;
|
|
if (!readOptionalSignedField(value, "position", output.position, result)) return false;
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char* liteResultCommandName(LiteResultCommand command)
|
|
{
|
|
switch (command) {
|
|
case LiteResultCommand::Info: return "info";
|
|
case LiteResultCommand::Height: return "height";
|
|
case LiteResultCommand::Balance: return "balance";
|
|
case LiteResultCommand::Addresses: return "addresses";
|
|
case LiteResultCommand::List: return "list";
|
|
case LiteResultCommand::Notes: return "notes";
|
|
case LiteResultCommand::SyncStatus: return "syncstatus";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
const char* liteResultParserErrorName(LiteResultParserError error)
|
|
{
|
|
switch (error) {
|
|
case LiteResultParserError::None: return "none";
|
|
case LiteResultParserError::InvalidJson: return "invalid_json";
|
|
case LiteResultParserError::ExpectedObject: return "expected_object";
|
|
case LiteResultParserError::ExpectedArray: return "expected_array";
|
|
case LiteResultParserError::MissingField: return "missing_field";
|
|
case LiteResultParserError::InvalidFieldType: return "invalid_field_type";
|
|
case LiteResultParserError::InvalidFieldValue: return "invalid_field_value";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
const char* liteSpendableOutputKindName(LiteSpendableOutputKind kind)
|
|
{
|
|
switch (kind) {
|
|
case LiteSpendableOutputKind::UnspentNote: return "unspent_note";
|
|
case LiteSpendableOutputKind::Utxo: return "utxo";
|
|
case LiteSpendableOutputKind::PendingNote: return "pending_note";
|
|
case LiteSpendableOutputKind::PendingUtxo: return "pending_utxo";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
const char* liteTransactionDirectionName(LiteTransactionDirection direction)
|
|
{
|
|
switch (direction) {
|
|
case LiteTransactionDirection::Unknown: return "unknown";
|
|
case LiteTransactionDirection::Send: return "send";
|
|
case LiteTransactionDirection::Receive: return "receive";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
LiteInfoParseResult parseLiteInfoResponse(const std::string& jsonText)
|
|
{
|
|
auto result = makeResult<LiteInfoParseResult>(LiteResultCommand::Info);
|
|
json parsed;
|
|
if (!parseJsonText(jsonText, result, parsed)) return result;
|
|
return parseLiteInfoResponse(parsed);
|
|
}
|
|
|
|
LiteInfoParseResult parseLiteInfoResponse(const json& value)
|
|
{
|
|
auto result = makeResult<LiteInfoParseResult>(LiteResultCommand::Info);
|
|
if (!requireObject(value, "$", result)) return result;
|
|
|
|
if (!readOptionalStringField(value, "chain_name", result.info.chainName, result)) return result;
|
|
if (!readOptionalStringField(value, "version", result.info.version, result)) return result;
|
|
if (!readOptionalStringField(value, "vendor", result.info.vendor, result)) return result;
|
|
if (!readOptionalSignedField(value, "latest_block_height", result.info.latestBlockHeight, result)) return result;
|
|
if (!readOptionalSignedField(value, "difficulty", result.info.difficulty, result)) return result;
|
|
if (!readOptionalSignedField(value, "longestchain", result.info.longestChain, result)) return result;
|
|
if (!readOptionalSignedField(value, "notarized", result.info.notarized, result)) return result;
|
|
if (!result.info.latestBlockHeight.has_value()) {
|
|
fail(result, LiteResultParserError::MissingField, fieldPath("latest_block_height"), "info response is missing latest_block_height");
|
|
return result;
|
|
}
|
|
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
LiteHeightParseResult parseLiteHeightResponse(const std::string& jsonText)
|
|
{
|
|
auto result = makeResult<LiteHeightParseResult>(LiteResultCommand::Height);
|
|
json parsed;
|
|
if (!parseJsonText(jsonText, result, parsed)) return result;
|
|
return parseLiteHeightResponse(parsed);
|
|
}
|
|
|
|
LiteHeightParseResult parseLiteHeightResponse(const json& value)
|
|
{
|
|
auto result = makeResult<LiteHeightParseResult>(LiteResultCommand::Height);
|
|
std::int64_t parsedHeight = 0;
|
|
if (jsonToSigned(value, parsedHeight)) {
|
|
result.height.height = parsedHeight;
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
if (!requireObject(value, "$", result)) return result;
|
|
for (const std::string key : {"height", "latest_block_height", "block_height"}) {
|
|
if (!value.contains(key) || value.at(key).is_null()) continue;
|
|
if (!jsonToSigned(value.at(key), parsedHeight)) {
|
|
fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "height field is not an integer-compatible value");
|
|
return result;
|
|
}
|
|
result.height.height = parsedHeight;
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
fail(result, LiteResultParserError::MissingField, "$", "height response has no recognized height field");
|
|
return result;
|
|
}
|
|
|
|
LiteBalanceParseResult parseLiteBalanceResponse(const std::string& jsonText)
|
|
{
|
|
auto result = makeResult<LiteBalanceParseResult>(LiteResultCommand::Balance);
|
|
json parsed;
|
|
if (!parseJsonText(jsonText, result, parsed)) return result;
|
|
return parseLiteBalanceResponse(parsed);
|
|
}
|
|
|
|
LiteBalanceParseResult parseLiteBalanceResponse(const json& value)
|
|
{
|
|
auto result = makeResult<LiteBalanceParseResult>(LiteResultCommand::Balance);
|
|
if (!requireObject(value, "$", result)) return result;
|
|
if (!readRequiredUnsignedField(value, "tbalance", result.balance.transparentBalance, result)) return result;
|
|
if (!readRequiredUnsignedField(value, "zbalance", result.balance.shieldedBalance, result)) return result;
|
|
if (!readRequiredUnsignedField(value, "unconfirmed", result.balance.unconfirmedBalance, result)) return result;
|
|
if (!readRequiredUnsignedField(value, "verified_zbalance", result.balance.verifiedShieldedBalance, result)) return result;
|
|
if (!readRequiredUnsignedField(value, "spendable_zbalance", result.balance.spendableShieldedBalance, result)) return result;
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
LiteAddressesParseResult parseLiteAddressesResponse(const std::string& jsonText)
|
|
{
|
|
auto result = makeResult<LiteAddressesParseResult>(LiteResultCommand::Addresses);
|
|
json parsed;
|
|
if (!parseJsonText(jsonText, result, parsed)) return result;
|
|
return parseLiteAddressesResponse(parsed);
|
|
}
|
|
|
|
LiteAddressesParseResult parseLiteAddressesResponse(const json& value)
|
|
{
|
|
auto result = makeResult<LiteAddressesParseResult>(LiteResultCommand::Addresses);
|
|
if (!requireObject(value, "$", result)) return result;
|
|
if (!readStringArrayField(value, "z_addresses", result.addresses.zAddresses, result)) return result;
|
|
if (!readStringArrayField(value, "t_addresses", result.addresses.tAddresses, result)) return result;
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
LiteNotesParseResult parseLiteNotesResponse(const std::string& jsonText)
|
|
{
|
|
auto result = makeResult<LiteNotesParseResult>(LiteResultCommand::Notes);
|
|
json parsed;
|
|
if (!parseJsonText(jsonText, result, parsed)) return result;
|
|
return parseLiteNotesResponse(parsed);
|
|
}
|
|
|
|
LiteNotesParseResult parseLiteNotesResponse(const json& value)
|
|
{
|
|
auto result = makeResult<LiteNotesParseResult>(LiteResultCommand::Notes);
|
|
if (!requireObject(value, "$", result)) return result;
|
|
if (!parseSpendableOutputArray(value, "unspent_notes", LiteSpendableOutputKind::UnspentNote, result.notes.unspentNotes, result)) return result;
|
|
if (!parseSpendableOutputArray(value, "utxos", LiteSpendableOutputKind::Utxo, result.notes.utxos, result)) return result;
|
|
if (!parseSpendableOutputArray(value, "pending_notes", LiteSpendableOutputKind::PendingNote, result.notes.pendingNotes, result)) return result;
|
|
if (!parseSpendableOutputArray(value, "pending_utxos", LiteSpendableOutputKind::PendingUtxo, result.notes.pendingUtxos, result)) return result;
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
LiteTransactionsParseResult parseLiteTransactionsResponse(const std::string& jsonText)
|
|
{
|
|
auto result = makeResult<LiteTransactionsParseResult>(LiteResultCommand::List);
|
|
json parsed;
|
|
if (!parseJsonText(jsonText, result, parsed)) return result;
|
|
return parseLiteTransactionsResponse(parsed);
|
|
}
|
|
|
|
LiteTransactionsParseResult parseLiteTransactionsResponse(const json& value)
|
|
{
|
|
auto result = makeResult<LiteTransactionsParseResult>(LiteResultCommand::List);
|
|
if (!requireArray(value, "$", result)) return result;
|
|
result.transactions.transactions.clear();
|
|
for (std::size_t index = 0; index < value.size(); ++index) {
|
|
LiteTransactionRecord record;
|
|
if (!parseTransactionRecord(value[index], "$[" + std::to_string(index) + "]", record, result)) return result;
|
|
result.transactions.transactions.push_back(std::move(record));
|
|
}
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
LiteSyncStatusParseResult parseLiteSyncStatusResponse(const std::string& jsonText)
|
|
{
|
|
auto result = makeResult<LiteSyncStatusParseResult>(LiteResultCommand::SyncStatus);
|
|
json parsed;
|
|
if (!parseJsonText(jsonText, result, parsed)) return result;
|
|
return parseLiteSyncStatusResponse(parsed);
|
|
}
|
|
|
|
LiteSyncStatusParseResult parseLiteSyncStatusResponse(const json& value)
|
|
{
|
|
auto result = makeResult<LiteSyncStatusParseResult>(LiteResultCommand::SyncStatus);
|
|
if (!requireObject(value, "$", result)) return result;
|
|
|
|
// The backend reports "syncing" as a STRING ("true"/"false"). synced_blocks/total_blocks
|
|
// are present only while actively syncing; an idle wallet returns just {"syncing":"false"}.
|
|
std::string syncing = "false";
|
|
if (!readOptionalStringField(value, "syncing", syncing, result)) return result;
|
|
const bool isSyncing = (syncing == "true" || syncing == "1");
|
|
|
|
if (isSyncing) {
|
|
if (!readRequiredUnsignedField(value, "synced_blocks", result.syncStatus.syncedBlocks, result)) return result;
|
|
if (!readRequiredUnsignedField(value, "total_blocks", result.syncStatus.totalBlocks, result)) return result;
|
|
result.syncStatus.complete = false; // actively syncing
|
|
result.syncStatus.progress = result.syncStatus.totalBlocks > 0
|
|
? std::min(1.0, static_cast<double>(result.syncStatus.syncedBlocks) /
|
|
static_cast<double>(result.syncStatus.totalBlocks))
|
|
: 0.0;
|
|
} else {
|
|
// Not actively syncing: the backend reports no block counts. Treat as idle/caught-up.
|
|
result.syncStatus.syncedBlocks = 0;
|
|
result.syncStatus.totalBlocks = 0;
|
|
result.syncStatus.complete = true;
|
|
result.syncStatus.progress = 1.0;
|
|
}
|
|
succeed(result);
|
|
return result;
|
|
}
|
|
|
|
} // namespace dragonx::wallet
|