diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..10bd3e0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,28 @@ +BasedOnStyle: LLVM + +IndentWidth: 4 +ColumnLimit: 120 + +IndentAccessModifiers: false +AccessModifierOffset: -4 + +AlignAfterOpenBracket: BlockIndent +BinPackParameters: false +BinPackArguments: false + +AllowAllParametersOfDeclarationOnNextLine: false +PenaltyBreakBeforeFirstCallParameter: 1 + +PointerAlignment: Left +ReferenceAlignment: Left + +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false + +BreakAfterReturnType: None +PenaltyReturnTypeOnItsOwnLine: 1000 + +BreakTemplateDeclarations: Yes + +NamespaceIndentation: All diff --git a/flake.lock b/flake.lock index fbf894d..a937211 100644 --- a/flake.lock +++ b/flake.lock @@ -31,6 +31,7 @@ "original": { "owner": "logos-blockchain", "repo": "logos-blockchain-module", + "rev": "ab733aa7074cf1992f605c203c3d6a7923602705", "type": "github" } }, @@ -1771,6 +1772,7 @@ "original": { "owner": "logos-co", "repo": "logos-module-builder", + "rev": "38ddf92c1f240f4e420d300a1fbabb1609d5db01", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 382bc49..6f99cbc 100644 --- a/flake.nix +++ b/flake.nix @@ -2,9 +2,9 @@ description = "Blockchain UI plugin for the Logos application"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/38ddf92c1f240f4e420d300a1fbabb1609d5db01"; nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx"; - liblogos_blockchain_module.url = "github:logos-blockchain/logos-blockchain-module"; + liblogos_blockchain_module.url = "github:logos-blockchain/logos-blockchain-module/ab733aa7074cf1992f605c203c3d6a7923602705"; }; outputs = inputs@{ logos-module-builder, ... }: diff --git a/justfile b/justfile index 8b1867c..dabae76 100644 --- a/justfile +++ b/justfile @@ -28,8 +28,12 @@ clean *args: fi } - if pgrep -x logos > /dev/null; then - confirm "Kill 'logos' process?" && pkill logos + logos_procs="$(pgrep -ax logos_host_qt || true)" + if [ -n "$logos_procs" ]; then + echo "Found matching processes:" + echo "$logos_procs" + echo "This will kill the process(es) above." + confirm "Proceed?" && { pkill -x logos_host_qt; echo "Killed."; } else echo "Already killed" fi @@ -50,3 +54,19 @@ clean *args: run: (clean "-y") nix run + +# Launch CLion inside the Nix dev shell so it inherits required variables and fully integrates with Nix. +clion: + nix develop -c "$HOME/.local/share/JetBrains/Toolbox/apps/clion/bin/clion.sh" . >/dev/null 2>&1 & + +prettify: + nix shell nixpkgs#clang-tools -c clang-format -i src/**.cpp src/**.h + +# Reuses cmake-build-debug's compile_commands.json (generated by CLion) instead +# of a `configure` step — this repo has no standalone `build` recipe. +lint: + nix shell nixpkgs#clang-tools --command clang-tidy \ + -p cmake-build-debug \ + --header-filter='src/.*' \ + --extra-arg-before=--driver-mode=g++ \ + src/BlockchainPlugin.cpp src/BlockchainBackend.cpp src/AccountsModel.cpp src/LogModel.cpp diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index c33a80d..05d7744 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -19,6 +19,12 @@ 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()) @@ -26,6 +32,30 @@ static QString toLocalPath(const QString& 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); +} + BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent) : BlockchainBackendSimpleSource(parent) , m_logosAPI(logosAPI) @@ -83,7 +113,7 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent) m_blockchainClient = m_logosAPI->getClient(BLOCKCHAIN_MODULE_NAME); if (!m_blockchainClient) { - setStatus(ErrorNotInitialized); + setError(QStringLiteral("Module not initialized")); qWarning() << "BlockchainBackend: failed to get blockchain module client"; return; } @@ -105,7 +135,7 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent) m_logModel->append(line); }); } else { - setStatus(ErrorSubscribeFailed); + setError(QStringLiteral("Failed to subscribe to events")); } qDebug() << "BlockchainBackend: initialized"; @@ -117,26 +147,32 @@ BlockchainBackend::~BlockchainBackend() 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) { - setStatus(ErrorNotInitialized); + setError(QStringLiteral("Module not initialized")); return; } setStatus(Starting); - QVariant result = m_blockchainClient->invokeRemoteMethod( - BLOCKCHAIN_MODULE_NAME, "start", userConfig(), deploymentConfig()); - int resultCode = result.isValid() ? result.toInt() : -1; + const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod( + BLOCKCHAIN_MODULE_NAME, "start", userConfig(), deploymentConfig())); - if (resultCode == 0 || resultCode == 1) { + if (result.success) { setStatus(Running); QTimer::singleShot(500, this, [this]() { refreshAccounts(); }); - } else if (resultCode == 2) { - setStatus(ErrorConfigMissing); } else { - setStatus(ErrorStartFailed); + setError(result.error.toString()); } } @@ -146,32 +182,32 @@ void BlockchainBackend::stopBlockchain() return; if (!m_blockchainClient) { - setStatus(ErrorNotInitialized); + setError(QStringLiteral("Module not initialized")); return; } setStatus(Stopping); - QVariant result = m_blockchainClient->invokeRemoteMethod( - BLOCKCHAIN_MODULE_NAME, "stop"); - int resultCode = result.isValid() ? result.toInt() : -1; + const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod( + BLOCKCHAIN_MODULE_NAME, "stop")); - if (resultCode == 0 || resultCode == 1) + if (result.success) { setStatus(Stopped); - else - setStatus(ErrorStopFailed); + } else { + setError(result.error.toString()); + } } void BlockchainBackend::refreshAccounts() { if (!m_blockchainClient) return; - QVariant result = m_blockchainClient->invokeRemoteMethod( - BLOCKCHAIN_MODULE_NAME, "wallet_get_known_addresses"); - QStringList list = - result.isValid() && result.canConvert() - ? result.toStringList() - : QStringList(); + const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod( + BLOCKCHAIN_MODULE_NAME, "wallet_get_known_addresses")); + + QStringList list; + if (result.success && result.value.canConvert()) + list = result.value.toStringList(); m_accountsModel->setAddresses(list); @@ -194,10 +230,8 @@ QString BlockchainBackend::getBalance(QString addressHex) if (!m_blockchainClient) { result = QStringLiteral("Error: Module not initialized."); } else { - QVariant v = m_blockchainClient->invokeRemoteMethod( - BLOCKCHAIN_MODULE_NAME, "wallet_get_balance", addressHex); - result = v.isValid() ? v.toString() - : QStringLiteral("Error: Call failed."); + result = toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( + BLOCKCHAIN_MODULE_NAME, "wallet_get_balance", addressHex))); } m_accountsModel->setBalanceForAddress(addressHex, result); @@ -211,11 +245,9 @@ QString BlockchainBackend::transferFunds( return QStringLiteral("Error: Module not initialized."); QStringList senders{fromKeyHex}; - QVariant result = m_blockchainClient->invokeRemoteMethod( + return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "wallet_transfer_funds", - fromKeyHex, senders, toKeyHex, amountStr, QString()); - return result.isValid() ? result.toString() - : QStringLiteral("Error: Call failed."); + fromKeyHex, senders, toKeyHex, amountStr, QString()))); } int BlockchainBackend::generateConfig( @@ -272,9 +304,10 @@ int BlockchainBackend::generateConfig( const QString jsonToSend = QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); - QVariant result = m_blockchainClient->invokeRemoteMethod( - BLOCKCHAIN_MODULE_NAME, "generate_user_config", jsonToSend); - return result.isValid() ? result.toInt() : -1; + 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) @@ -282,11 +315,9 @@ QString BlockchainBackend::getNotes(QString walletAddressHex, QString optionalTi if (!m_blockchainClient) return QStringLiteral("Error: Module not initialized."); - QVariant result = m_blockchainClient->invokeRemoteMethod( + return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, "wallet_get_notes", - walletAddressHex, optionalTipHex); - return result.isValid() ? result.toString() - : QStringLiteral("Error: Call failed."); + walletAddressHex, optionalTipHex))); } QString BlockchainBackend::channelDepositWithNotes( @@ -303,11 +334,9 @@ QString BlockchainBackend::channelDepositWithNotes( args << channelIdHex << inputNoteIdHexes << metadataHex << changePublicKeyHex << fundingPublicKeyHexes << maxTxFee << optionalTipHex; - QVariant result = m_blockchainClient->invokeRemoteMethod( + return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod( BLOCKCHAIN_MODULE_NAME, QStringLiteral("channel_deposit_with_notes"), - args); - return result.isValid() ? result.toString() - : QStringLiteral("Error: Call failed."); + args))); } void BlockchainBackend::clearLogs() diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index b0082f6..7ad66e8 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -43,6 +43,7 @@ public slots: void refreshAccounts() override; QString getBalance(QString addressHex) override; QString transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr) override; + QString claimLeaderRewards() override; int generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, @@ -60,6 +61,7 @@ public slots: private: void fetchBalancesForAccounts(const QStringList& list); + void setError(const QString& message); LogosAPI* m_logosAPI = nullptr; LogosAPIClient* m_blockchainClient = nullptr; diff --git a/src/BlockchainBackend.rep b/src/BlockchainBackend.rep index 0e56717..eb8fd34 100644 --- a/src/BlockchainBackend.rep +++ b/src/BlockchainBackend.rep @@ -1,18 +1,20 @@ class BlockchainBackend { - ENUM BlockchainStatus { NotStarted=0, Starting=1, Running=2, Stopping=3, Stopped=4, Error=5, ErrorNotInitialized=6, ErrorConfigMissing=7, ErrorStartFailed=8, ErrorStopFailed=9, ErrorSubscribeFailed=10 } + ENUM BlockchainStatus { NotStarted=0, Starting=1, Running=2, Stopping=3, Stopped=4, Error=5 } PROP(BlockchainStatus status READONLY) PROP(QString userConfig READWRITE) PROP(QString deploymentConfig READWRITE) PROP(bool useGeneratedConfig READWRITE) PROP(QString generatedUserConfigPath READONLY) + PROP(QString lastErrorMessage READONLY) SLOT(void startBlockchain()) SLOT(void stopBlockchain()) SLOT(void refreshAccounts()) SLOT(QString getBalance(QString addressHex)) SLOT(QString transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr)) + SLOT(QString claimLeaderRewards()) SLOT(int generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, QString deploymentConfigPath, QString statePath)) SLOT(QString getNotes(QString walletAddressHex, QString optionalTipHex)) SLOT(QString channelDepositWithNotes(QString channelIdHex, QStringList inputNoteIdHexes, QString metadataHex, QString changePublicKeyHex, QStringList fundingPublicKeyHexes, QString maxTxFee, QString optionalTipHex)) diff --git a/src/qml/BlockchainView.qml b/src/qml/BlockchainView.qml index 5a5715b..cca4ace 100644 --- a/src/qml/BlockchainView.qml +++ b/src/qml/BlockchainView.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts import Logos.Theme import Logos.Controls -// BlockchainStatus enum (NotStarted/Starting/Running/.../ErrorSubscribeFailed) +// BlockchainStatus enum (NotStarted/Starting/Running/Stopping/Stopped/Error) // declared in BlockchainBackend.rep — registered with QML by the replica // factory plugin. import Logos.BlockchainBackend 1.0 @@ -49,12 +49,7 @@ Rectangle { case BlockchainBackend.Running: return qsTr("Running") case BlockchainBackend.Stopping: return qsTr("Stopping...") case BlockchainBackend.Stopped: return qsTr("Stopped") - case BlockchainBackend.Error: return qsTr("Error") - case BlockchainBackend.ErrorNotInitialized: return qsTr("Error: Module not initialized") - case BlockchainBackend.ErrorConfigMissing: return qsTr("Error: Config path missing") - case BlockchainBackend.ErrorStartFailed: return qsTr("Error: Failed to start node") - case BlockchainBackend.ErrorStopFailed: return qsTr("Error: Failed to stop node") - case BlockchainBackend.ErrorSubscribeFailed: return qsTr("Error: Failed to subscribe to events") + case BlockchainBackend.Error: return qsTr("Error: %1").arg(root.backend.lastErrorMessage) default: return qsTr("Unknown") } } @@ -243,6 +238,14 @@ Rectangle { function(error) { walletView.setTransferResult("Error: " + error) } ) } + onClaimLeaderRewardsRequested: function() { + if (!root.backend) return + logos.watch( + root.backend.claimLeaderRewards(), + function(result) { walletView.setLeaderClaimResult(result) }, + function(error) { walletView.setLeaderClaimResult("Error: " + error) } + ) + } } Item { diff --git a/src/qml/views/WalletView.qml b/src/qml/views/WalletView.qml index a722a46..8fe2164 100644 --- a/src/qml/views/WalletView.qml +++ b/src/qml/views/WalletView.qml @@ -17,18 +17,23 @@ RowLayout { signal getBalanceRequested(string addressHex) signal transferRequested(string fromKeyHex, string toKeyHex, string amount) + signal claimLeaderRewardsRequested() signal copyToClipboard(string text) function setTransferResult(text) { transferResultText.text = text } + function setLeaderClaimResult(text) { + leaderClaimResultText.text = text + } + spacing: Theme.spacing.medium // Get balance card Rectangle { Layout.fillWidth: true - implicitHeight: transferRect.height + implicitHeight: actionsCol.height Layout.preferredHeight: Math.min(implicitHeight, 400) color: Theme.palette.backgroundTertiary radius: Theme.spacing.radiusLarge @@ -75,84 +80,156 @@ RowLayout { } } - // Transfer funds card - Rectangle { - id: transferRect + ColumnLayout { + id: actionsCol + Layout.fillWidth: true - Layout.preferredHeight: transferCol.height + 2 * Theme.spacing.large - color: Theme.palette.backgroundTertiary - radius: Theme.spacing.radiusLarge - border.color: Theme.palette.border - border.width: 1 + spacing: Theme.spacing.large - ColumnLayout { - id: transferCol - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Theme.spacing.large - spacing: Theme.spacing.small + // Transfer funds card + Rectangle { + id: transferRect - LogosText { - text: qsTr("Transfer funds") - font.pixelSize: Theme.typography.secondaryText - font.bold: true - } + Layout.fillWidth: true + Layout.preferredHeight: transferCol.height + 2 * Theme.spacing.large + color: Theme.palette.backgroundTertiary + radius: Theme.spacing.radiusLarge + border.color: Theme.palette.border + border.width: 1 - StyledAddressComboBox { - id: transferFromCombo - model: root.accountsModel - textRole: "address" - } + ColumnLayout { + id: transferCol + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.small - LogosTextField { - id: transferToField - Layout.fillWidth: true - Layout.preferredHeight: 30 - placeholderText: qsTr("To key (64 hex chars)") - } - - LogosTextField { - id: transferAmountField - Layout.fillWidth: true - Layout.preferredHeight: 30 - placeholderText: qsTr("Amount") - } - - RowLayout { - Layout.fillWidth: true - Layout.preferredHeight: transferButton.implicitHeight - - LogosButton { - id: transferButton - Layout.preferredWidth: 60 - Layout.alignment: Qt.AlignRight - text: qsTr("Send") - onClicked: root.transferRequested(transferFromCombo.currentText.trim(), transferToField.text.trim(), transferAmountField.text) + LogosText { + text: qsTr("Transfer funds") + font.pixelSize: Theme.typography.secondaryText + font.bold: true } - LogosButton { + StyledAddressComboBox { + id: transferFromCombo + model: root.accountsModel + textRole: "address" + } + + LogosTextField { + id: transferToField Layout.fillWidth: true - enabled: true - padding: Theme.spacing.small - contentItem: RowLayout { - width: parent.width - anchors.centerIn: parent - LogosText { - id: transferResultText - Layout.fillWidth: true - color: Theme.palette.textSecondary - font.pixelSize: Theme.typography.secondaryText - font.weight: Theme.typography.weightMedium - wrapMode: Text.WordWrap - elide: Text.ElideRight + Layout.preferredHeight: 30 + placeholderText: qsTr("To key (64 hex chars)") + } + + LogosTextField { + id: transferAmountField + Layout.fillWidth: true + Layout.preferredHeight: 30 + placeholderText: qsTr("Amount") + } + + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: transferButton.implicitHeight + + LogosButton { + id: transferButton + Layout.preferredWidth: 60 + Layout.alignment: Qt.AlignRight + text: qsTr("Send") + onClicked: root.transferRequested(transferFromCombo.currentText.trim(), transferToField.text.trim(), transferAmountField.text) + } + + LogosButton { + Layout.fillWidth: true + enabled: true + padding: Theme.spacing.small + contentItem: RowLayout { + width: parent.width + anchors.centerIn: parent + LogosText { + id: transferResultText + Layout.fillWidth: true + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + font.weight: Theme.typography.weightMedium + wrapMode: Text.WordWrap + elide: Text.ElideRight + } + LogosCopyButton { + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 + onCopyText: root.copyToClipboard(transferResultText.text) + visible: transferResultText.text + } } - LogosCopyButton { - Layout.alignment: Qt.AlignRight - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - onCopyText: root.copyRequested(transferResultText.text) - visible: transferResultText.text + } + } + } + } + + // Leader rewards card + Rectangle { + id: leaderRewardsRect + + Layout.fillWidth: true + Layout.preferredHeight: leaderRewardsCol.height + 2 * Theme.spacing.large + color: Theme.palette.backgroundTertiary + radius: Theme.spacing.radiusLarge + border.color: Theme.palette.border + border.width: 1 + + ColumnLayout { + id: leaderRewardsCol + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.small + + LogosText { + text: qsTr("Leader rewards") + font.pixelSize: Theme.typography.secondaryText + font.bold: true + } + + RowLayout { + Layout.fillWidth: true + + LogosButton { + id: leaderClaimButton + Layout.preferredWidth: 140 + text: qsTr("Claim") + onClicked: root.claimLeaderRewardsRequested() + } + + LogosButton { + Layout.fillWidth: true + enabled: true + padding: Theme.spacing.small + contentItem: RowLayout { + width: parent.width + anchors.centerIn: parent + LogosText { + id: leaderClaimResultText + Layout.fillWidth: true + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + font.weight: Theme.typography.weightMedium + wrapMode: Text.WordWrap + elide: Text.ElideRight + } + LogosCopyButton { + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 + onCopyText: root.copyToClipboard(leaderClaimResultText.text) + visible: leaderClaimResultText.text + } } } }