Merge remote-tracking branch 'origin/master' into deposit

# Conflicts:
#	src/qml/BlockchainView.qml
This commit is contained in:
Daniel 2026-06-19 16:00:55 +02:00
commit 7d849653fa
9 changed files with 287 additions and 124 deletions

28
.clang-format Normal file
View File

@ -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

2
flake.lock generated
View File

@ -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"
}
},

View File

@ -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, ... }:

View File

@ -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

View File

@ -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<LogosResult>();
}
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<QStringList>()
? 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<QStringList>())
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()

View File

@ -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;

View File

@ -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))

View File

@ -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 {

View File

@ -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
}
}
}
}