#include "wallet/lite_result_parsers.h" #include #include #include 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 Result makeResult(LiteResultCommand command) { Result result; result.command = command; return result; } template 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 void succeed(Result& result) { result.ok = true; result.error = LiteResultParserError::None; result.errorPath.clear(); result.errorMessage.clear(); } template 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 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 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(); return true; } if (value.is_number_integer()) { const auto signedValue = value.get(); if (signedValue < 0) return false; output = static_cast(signedValue); return true; } if (value.is_string()) { const auto text = value.get(); 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(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(); return true; } if (value.is_number_unsigned()) { const auto unsignedValue = value.get(); if (unsignedValue > static_cast(std::numeric_limits::max())) { return false; } output = static_cast(unsignedValue); return true; } if (value.is_string()) { const auto text = value.get(); 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(parsed); return true; } } catch (const std::exception&) { return false; } return false; } template 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(); return true; } template bool readOptionalStringField(const json& object, const std::string& key, std::optional& 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(); return true; } template 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(); return true; } template 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 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 bool readOptionalSignedField(const json& object, const std::string& key, std::optional& 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 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(); return true; } template bool readStringArrayField(const json& object, const std::string& key, std::vector& 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()); } return true; } template 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 bool parseSpendableOutputArray(const json& object, const std::string& key, LiteSpendableOutputKind kind, std::vector& 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 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 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(LiteResultCommand::Info); json parsed; if (!parseJsonText(jsonText, result, parsed)) return result; return parseLiteInfoResponse(parsed); } LiteInfoParseResult parseLiteInfoResponse(const json& value) { auto result = makeResult(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(LiteResultCommand::Height); json parsed; if (!parseJsonText(jsonText, result, parsed)) return result; return parseLiteHeightResponse(parsed); } LiteHeightParseResult parseLiteHeightResponse(const json& value) { auto result = makeResult(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(LiteResultCommand::Balance); json parsed; if (!parseJsonText(jsonText, result, parsed)) return result; return parseLiteBalanceResponse(parsed); } LiteBalanceParseResult parseLiteBalanceResponse(const json& value) { auto result = makeResult(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(LiteResultCommand::Addresses); json parsed; if (!parseJsonText(jsonText, result, parsed)) return result; return parseLiteAddressesResponse(parsed); } LiteAddressesParseResult parseLiteAddressesResponse(const json& value) { auto result = makeResult(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(LiteResultCommand::Notes); json parsed; if (!parseJsonText(jsonText, result, parsed)) return result; return parseLiteNotesResponse(parsed); } LiteNotesParseResult parseLiteNotesResponse(const json& value) { auto result = makeResult(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(LiteResultCommand::List); json parsed; if (!parseJsonText(jsonText, result, parsed)) return result; return parseLiteTransactionsResponse(parsed); } LiteTransactionsParseResult parseLiteTransactionsResponse(const json& value) { auto result = makeResult(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(LiteResultCommand::SyncStatus); json parsed; if (!parseJsonText(jsonText, result, parsed)) return result; return parseLiteSyncStatusResponse(parsed); } LiteSyncStatusParseResult parseLiteSyncStatusResponse(const json& value) { auto result = makeResult(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(result.syncStatus.syncedBlocks) / static_cast(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