#include "BlockchainBackend.h" #include "logos_api.h" #include "logos_api_client.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include const QString BlockchainBackend::BLOCKCHAIN_MODULE_NAME = QStringLiteral("liblogos_blockchain_module"); void BlockchainBackend::setError(const QString& message) { setLastErrorMessage(message); setStatus(Error); } static QString toLocalPath(const QString& pathInput) { if (pathInput.trimmed().isEmpty()) return pathInput; return QUrl::fromUserInput(pathInput).toLocalFile(); } // invokeRemoteMethod() returns an invalid QVariant (not a LogosResult) when // the call itself fails to get a reply (timeout, disconnected module, etc.), // as opposed to the remote method running and reporting failure normally. static LogosResult toLogosResult(const QVariant& reply) { if (!reply.isValid()) return LogosResult{false, QVariant(), QStringLiteral("Call failed.")}; return reply.value(); } static QString toErrorMessage(const LogosResult& result) { return QStringLiteral("Error: %1").arg(result.error.toString()); } // QML callers distinguish success from failure by sniffing for an "Error" // prefix (see BlockchainView.qml), so both branches must share this type. // TODO: Currently functions are returning a string (for ok and ko cases), which is later re-parsed to know if the call // succeeded or failed. We can simplify this by returning the result itself upward. static QString toDisplayMessage(const LogosResult& result) { return result.success ? result.value.toString() : toErrorMessage(result); } // Decode a base58 (Bitcoin alphabet) string to raw bytes. On an invalid // character *ok is set to false and an empty array is returned. static QByteArray decodeBase58(const QString& input, bool* ok) { static const QByteArray kAlphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const QByteArray s = input.trimmed().toLatin1(); QByteArray bytes; // little-endian while building, reversed at the end bytes.append('\0'); for (const char c : s) { const int value = kAlphabet.indexOf(c); if (value < 0) { if (ok) *ok = false; return {}; } int carry = value; for (int j = 0; j < bytes.size(); ++j) { carry += static_cast(bytes[j]) * 58; bytes[j] = static_cast(carry & 0xff); carry >>= 8; } while (carry > 0) { bytes.append(static_cast(carry & 0xff)); carry >>= 8; } } // Each leading '1' maps to a leading zero byte. for (int i = 0; i < s.size() && s[i] == '1'; ++i) bytes.append('\0'); std::reverse(bytes.begin(), bytes.end()); if (ok) *ok = true; return bytes; } BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent) : BlockchainBackendSimpleSource(parent) , m_logosAPI(logosAPI) , m_accountsModel(new AccountsModel(this)) , m_logModel(new LogModel(this)) { setStatus(NotStarted); setUseGeneratedConfig(false); setGeneratedUserConfigPath( QDir::currentPath() + QStringLiteral("/user_config.yaml")); // Restore saved config paths QSettings s("Logos", "BlockchainUI"); const QString envConfigPath = QString::fromUtf8(qgetenv("LB_CONFIG_PATH")); const QString savedUserConfig = s.value("userConfigPath").toString(); const QString savedDeploymentConfig = s.value("deploymentConfigPath").toString(); if (!envConfigPath.isEmpty()) setUserConfig(toLocalPath(envConfigPath)); else if (!savedUserConfig.isEmpty()) setUserConfig(toLocalPath(savedUserConfig)); if (!savedDeploymentConfig.isEmpty()) setDeploymentConfig(toLocalPath(savedDeploymentConfig)); // Re-apply pre-.rep behavior: normalize file URLs, then persist (as master did in setters). connect(this, &BlockchainBackendSimpleSource::userConfigChanged, this, [this]() { const QString p = userConfig(); const QString n = toLocalPath(p); if (n != p) { QSignalBlocker b(this); setUserConfig(n); } QSettings("Logos", "BlockchainUI") .setValue("userConfigPath", userConfig()); }); connect(this, &BlockchainBackendSimpleSource::deploymentConfigChanged, this, [this]() { const QString p = deploymentConfig(); const QString n = toLocalPath(p); if (n != p) { QSignalBlocker b(this); setDeploymentConfig(n); } QSettings("Logos", "BlockchainUI") .setValue("deploymentConfigPath", deploymentConfig()); }); if (!m_logosAPI) { qWarning() << "BlockchainBackend: constructed without LogosAPI"; return; } m_blockchainClient = m_logosAPI->getClient(BLOCKCHAIN_MODULE_NAME); if (!m_blockchainClient) { setError(QStringLiteral("Module not initialized")); qWarning() << "BlockchainBackend: failed to get blockchain module client"; return; } LogosObject* replica = m_blockchainClient->requestObject(BLOCKCHAIN_MODULE_NAME); if (replica) { m_blockchainClient->onEvent( replica, "newBlock", [this](const QString&, const QVariantList& data) { const QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss"); QString line; if (!data.isEmpty()) line = QString("[%1] New block: %2") .arg(timestamp, data.first().toString()); else line = QString("[%1] New block (no data)").arg(timestamp); m_logModel->append(line); }); } else { setError(QStringLiteral("Failed to subscribe to events")); } qDebug() << "BlockchainBackend: initialized"; } BlockchainBackend::~BlockchainBackend() { if (status() == Running || status() == Starting) stopBlockchain(); } QString BlockchainBackend::claimLeaderRewards() { if (!m_blockchainClient) return QStringLiteral("Error: Module not initialized."); return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "leader_claim"))); } void BlockchainBackend::startBlockchain() { if (!m_blockchainClient) { setError(QStringLiteral("Module not initialized")); return; } setStatus(Starting); const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "start", userConfig(), deploymentConfig())); if (result.success) { setStatus(Running); QTimer::singleShot(500, this, [this]() { refreshAccounts(); }); } else { setError(result.error.toString()); } } void BlockchainBackend::stopBlockchain() { if (status() != Running && status() != Starting) return; if (!m_blockchainClient) { setError(QStringLiteral("Module not initialized")); return; } setStatus(Stopping); const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "stop")); if (result.success) { setStatus(Stopped); } else { setError(result.error.toString()); } } void BlockchainBackend::refreshAccounts() { if (!m_blockchainClient) return; const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "wallet_get_known_addresses")); if (!result.success) { qWarning() << "refreshAccounts: failed:" << result.error.toString(); return; } // The SDK marshals the JSON array into a QVariantList; rely on toList() // rather than canConvert() (which is unreliable for a // QVariantList under Qt6), and fall back to toStringList() for the rare // case where the value already arrives as a QStringList. QStringList list; const QVariantList items = result.value.toList(); if (!items.isEmpty()) { for (const QVariant& item : items) { const QString addr = item.toString(); if (!addr.isEmpty()) list << addr; } } else { list = result.value.toStringList(); } qDebug() << "refreshAccounts: loaded" << list.size() << "addresses"; m_accountsModel->setAddresses(list); QTimer::singleShot(0, this, [this, list]() { fetchBalancesForAccounts(list); }); } void BlockchainBackend::fetchBalancesForAccounts(const QStringList& list) { if (!m_blockchainClient) return; for (const QString& address : list) { if (address.isEmpty()) continue; getBalance(address); } } QString BlockchainBackend::getBalance(QString addressHex) { QString result; if (!m_blockchainClient) { result = QStringLiteral("Error: Module not initialized."); } else { result = toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "wallet_get_balance", addressHex))); } m_accountsModel->setBalanceForAddress(addressHex, result); return result; } QString BlockchainBackend::transferFunds( QString fromKeyHex, QString toKeyHex, QString amountStr) { if (!m_blockchainClient) return QStringLiteral("Error: Module not initialized."); QStringList senders{fromKeyHex}; return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "wallet_transfer_funds", fromKeyHex, senders, toKeyHex, amountStr, QString()))); } int BlockchainBackend::generateConfig( QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, QString deploymentConfigPath, QString statePath) { if (!m_blockchainClient) return -1; QVariantMap normalized; QString out = outputPath.trimmed(); if (out.isEmpty()) out = generatedUserConfigPath(); else out = toLocalPath(out); normalized.insert("output", out); if (!initialPeers.isEmpty()) { QVariantList peersList; for (const QString& p : initialPeers) { if (!p.trimmed().isEmpty()) peersList.append(p.trimmed()); } if (!peersList.isEmpty()) normalized.insert("initial_peers", peersList); } if (netPort > 0) normalized.insert("net_port", netPort); if (blendPort > 0) normalized.insert("blend_port", blendPort); if (!httpAddr.trimmed().isEmpty()) normalized.insert("http_addr", httpAddr.trimmed()); if (!externalAddress.trimmed().isEmpty()) normalized.insert("external_address", externalAddress.trimmed()); if (noPublicIpCheck) normalized.insert("no_public_ip_check", true); if (deploymentMode == 0) { QVariantMap deployment; deployment.insert("well_known_deployment", "devnet"); normalized.insert("deployment", deployment); } else if (deploymentMode == 1 && !deploymentConfigPath.trimmed().isEmpty()) { QVariantMap deployment; deployment.insert("config_path", toLocalPath(deploymentConfigPath.trimmed())); normalized.insert("deployment", deployment); } if (!statePath.trimmed().isEmpty()) normalized.insert("state_path", toLocalPath(statePath.trimmed())); const QJsonDocument doc = QJsonDocument::fromVariant(normalized); const QString jsonToSend = QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "generate_user_config", jsonToSend)); return result.success ? 0 : -1; } QString BlockchainBackend::getNotes(QString walletAddressHex, QString optionalTipHex) { if (!m_blockchainClient) return QStringLiteral("Error: Module not initialized."); return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "wallet_get_notes", walletAddressHex, optionalTipHex))); } QString BlockchainBackend::channelDepositWithNotes( QString channelIdHex, QStringList inputNoteIdHexes, QString metadataBase58, QString changePublicKeyHex, QStringList fundingPublicKeyHexes, QString maxTxFee, QString optionalTipHex) { if (!m_blockchainClient) return QStringLiteral("Error: Module not initialized."); // The metadata arrives base58-encoded; the module expects metadata_hex, so // decode to bytes and hex-encode. Empty stays empty (metadata is optional). QString metadataHex; if (!metadataBase58.trimmed().isEmpty()) { bool ok = false; const QByteArray bytes = decodeBase58(metadataBase58, &ok); if (!ok) return QStringLiteral("Error: Invalid base58 metadata."); metadataHex = QString::fromLatin1(bytes.toHex()); } // 7 positional args exceed the variadic invokeRemoteMethod overloads // (max 5), so pass them through the QVariantList form. QVariantList args; args << channelIdHex << inputNoteIdHexes << metadataHex << changePublicKeyHex << fundingPublicKeyHexes << maxTxFee << optionalTipHex; return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, QStringLiteral("channel_deposit_with_notes"), args))); } void BlockchainBackend::clearLogs() { m_logModel->clear(); } void BlockchainBackend::copyToClipboard(QString text) { // The backend runs in a non-GUI ViewModuleHost subprocess, where there is // no QGuiApplication and accessing the clipboard segfaults. Clipboard is // handled QML-side (see BlockchainView.copyText); guard here so any stray // call is a no-op rather than a crash. if (!qobject_cast(QCoreApplication::instance())) { qWarning() << "copyToClipboard: no GUI application; ignoring"; return; } if (QClipboard* clipboard = QGuiApplication::clipboard()) clipboard->setText(text); }