mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-17 04:19:35 +00:00
refactor(amm-ui): use shared wallet modules
Replace AMM-local wallet access, account state, controls, and confirmation-dialog mechanics with the reusable wallet targets. Keep AMM-specific transaction summaries and actions in the AMM UI.\n\nRefs #227
This commit is contained in:
parent
ea4fcc2c6c
commit
ae52613279
@ -1,12 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(AmmUiPlugin LANGUAGES CXX)
|
||||
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Core Gui Network Qml Quick QuickControls2)
|
||||
qt_standard_project_setup(REQUIRES 6.8)
|
||||
|
||||
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
||||
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
||||
else()
|
||||
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
|
||||
endif()
|
||||
|
||||
set(LOGOS_WALLET_SOURCE_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../shared/wallet"
|
||||
CACHE PATH "Path to the shared Logos wallet module"
|
||||
)
|
||||
set(LOGOS_WALLET_GENERATED_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated_code"
|
||||
CACHE PATH "Path to generated Logos SDK sources"
|
||||
)
|
||||
add_subdirectory("${LOGOS_WALLET_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/shared-wallet")
|
||||
|
||||
# ui_qml module with a hand-written C++ backend (QtRO .rep view contract +
|
||||
# generated *SimpleSource/*ViewPluginBase). Mirrors the LEZ wallet UI module.
|
||||
logos_module(
|
||||
@ -18,12 +31,12 @@ logos_module(
|
||||
src/AmmUiPlugin.cpp
|
||||
src/AmmUiBackend.h
|
||||
src/AmmUiBackend.cpp
|
||||
src/AccountModel.h
|
||||
src/AccountModel.cpp
|
||||
FIND_PACKAGES
|
||||
Qt6Gui
|
||||
Qt6Network
|
||||
LINK_LIBRARIES
|
||||
Qt6::Gui
|
||||
Qt6::Network
|
||||
LINK_TARGETS
|
||||
logos_wallet_access
|
||||
)
|
||||
|
||||
937
apps/amm/flake.lock
generated
937
apps/amm/flake.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,17 +4,41 @@
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
|
||||
# Shared C++ wallet access and Logos.Wallet QML sources.
|
||||
shared_wallet = {
|
||||
url = "path:../shared/wallet";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
# Core wallet module (the LEZ wallet FFI Qt plugin). The input name must
|
||||
# match the metadata.json `dependencies` entry so the builder can resolve
|
||||
# it as a module dependency. This rev pins LEZ (lssa) at fb8cbac4, which
|
||||
# includes the macOS Metal-build fix, so no `--override-input` is needed.
|
||||
logos_execution_zone.url = "github:logos-blockchain/logos-execution-zone-module?rev=d2e9400ac06c3cdbfc2405b4f153fff9841a453c";
|
||||
# it as a module dependency. This revision exposes generic transaction
|
||||
# submission by deployed program ID.
|
||||
logos_execution_zone.url = "github:logos-blockchain/logos-execution-zone-module?rev=d70225ced646934d2294fd9e8f8b03615c104b80";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, ... }:
|
||||
outputs = inputs@{ logos-module-builder, shared_wallet, ... }:
|
||||
logos-module-builder.lib.mkLogosQmlModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
preConfigure = ''
|
||||
cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${shared_wallet}")
|
||||
'';
|
||||
postInstall = ''
|
||||
# The builder installs the view under lib/qml after this hook. Its
|
||||
# import descriptor points back to this compiled shared QML module.
|
||||
test -f ${./qml}/Logos/Wallet/qmldir
|
||||
|
||||
walletQmlDir="shared-wallet/qml/Logos/Wallet"
|
||||
if [ ! -d "$walletQmlDir" ]; then
|
||||
echo "Built Logos.Wallet QML module not found"
|
||||
exit 1
|
||||
fi
|
||||
walletQmlInstallDir="$out/lib/Logos/Wallet"
|
||||
mkdir -p "$walletQmlInstallDir"
|
||||
cp -r "$walletQmlDir/." "$walletQmlInstallDir/"
|
||||
test -f "$walletQmlInstallDir/qmldir"
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
5
apps/amm/qml/Logos/Wallet/qmldir
Normal file
5
apps/amm/qml/Logos/Wallet/qmldir
Normal file
@ -0,0 +1,5 @@
|
||||
module Logos.Wallet
|
||||
optional plugin logos_wallet_qmlplugin ../../../Logos/Wallet
|
||||
classname Logos_WalletPlugin
|
||||
prefer :/qt/qml/Logos/Wallet/
|
||||
depends QtQuick
|
||||
@ -2,8 +2,7 @@ import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import Logos.Theme
|
||||
|
||||
import "components/wallet"
|
||||
import Logos.Wallet
|
||||
|
||||
// Self-contained navigation bar — styling is independent of any view's theme.
|
||||
// Use currentIndex to read the active tab; tabChanged(index) fires on selection.
|
||||
@ -94,11 +93,15 @@ Item {
|
||||
}
|
||||
|
||||
// Wallet / account control on the far right.
|
||||
AccountControl {
|
||||
WalletControl {
|
||||
id: accountControl
|
||||
Layout.leftMargin: 12
|
||||
backend: root.backend
|
||||
wallet: root.backend
|
||||
accountModel: root.accountModel
|
||||
viewportWidth: root.width
|
||||
watchCall: function(result, success, failure) {
|
||||
logos.watch(result, success, failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,238 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var snapshot: ({})
|
||||
property bool open: false
|
||||
readonly property bool isAdd: root.snapshot.action === "add"
|
||||
|
||||
signal canceled
|
||||
signal confirmed(var snapshot)
|
||||
|
||||
visible: root.open
|
||||
z: 20
|
||||
|
||||
Keys.onEscapePressed: root.cancel()
|
||||
|
||||
function openWithSnapshot(nextSnapshot) {
|
||||
root.snapshot = nextSnapshot;
|
||||
root.open = true;
|
||||
root.forceActiveFocus();
|
||||
cancelButton.forceActiveFocus();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
root.open = false;
|
||||
root.canceled();
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
const confirmedSnapshot = root.snapshot;
|
||||
root.open = false;
|
||||
root.confirmed(confirmedSnapshot);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#99000000"
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: panel
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: "#1D1D1D"
|
||||
implicitHeight: dialogContent.implicitHeight + 24
|
||||
radius: 8
|
||||
width: Math.max(0, Math.min(360, root.width - 32))
|
||||
border.color: "#343434"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: dialogContent
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
color: "#E7E1D8"
|
||||
font.bold: true
|
||||
font.pixelSize: 16
|
||||
text: root.isAdd ? qsTr("Confirm add liquidity") : qsTr("Confirm remove liquidity")
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 8
|
||||
visible: root.isAdd
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Deposit %1").arg(root.snapshot.tokenA || "")
|
||||
value: root.snapshot.depositA || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Deposit %1").arg(root.snapshot.tokenB || "")
|
||||
value: root.snapshot.depositB || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Receive at least")
|
||||
value: root.snapshot.minLpReceived || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Current ratio")
|
||||
value: root.snapshot.currentRatio || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Fee tier")
|
||||
value: root.snapshot.feeTier || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Slippage tolerance")
|
||||
value: root.snapshot.slippageTolerance || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 8
|
||||
visible: !root.isAdd
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Burn LP")
|
||||
value: qsTr("%1 (%2)").arg(root.snapshot.burnText || "").arg(root.snapshot.burnPercent || "")
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Receive at least %1").arg(root.snapshot.tokenA || "")
|
||||
value: root.snapshot.minTokenAReceived || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Receive at least %1").arg(root.snapshot.tokenB || "")
|
||||
value: root.snapshot.minTokenBReceived || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Slippage tolerance")
|
||||
value: root.snapshot.slippageTolerance || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
label: qsTr("Post-removal share")
|
||||
value: root.snapshot.postRemovalShare || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 8
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
id: cancelButton
|
||||
|
||||
activeFocusOnTab: true
|
||||
focusPolicy: Qt.StrongFocus
|
||||
hoverEnabled: true
|
||||
text: qsTr("Cancel")
|
||||
|
||||
Accessible.name: cancelButton.text
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 44
|
||||
|
||||
onClicked: root.cancel()
|
||||
|
||||
contentItem: Text {
|
||||
color: cancelButton.hovered || cancelButton.activeFocus ? "#151515" : "#E7E1D8"
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: cancelButton.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
border.color: cancelButton.activeFocus ? "#F26A21" : "#343434"
|
||||
border.width: 1
|
||||
color: cancelButton.pressed ? "#343434" : cancelButton.hovered || cancelButton.activeFocus ? "#E7E1D8" : "#151515"
|
||||
radius: 6
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: confirmButton
|
||||
|
||||
activeFocusOnTab: true
|
||||
focusPolicy: Qt.StrongFocus
|
||||
hoverEnabled: true
|
||||
text: qsTr("Confirm")
|
||||
|
||||
Accessible.name: confirmButton.text
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 44
|
||||
|
||||
onClicked: root.confirm()
|
||||
|
||||
contentItem: Text {
|
||||
color: "#151515"
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: confirmButton.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
border.color: "#F26A21"
|
||||
border.width: 1
|
||||
color: confirmButton.pressed ? "#D95C1E" : confirmButton.hovered || confirmButton.activeFocus ? "#FF8A3D" : "#F26A21"
|
||||
radius: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var snapshot: ({})
|
||||
readonly property bool isAdd: root.snapshot.action === "add"
|
||||
|
||||
spacing: 8
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
visible: root.isAdd
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Deposit %1").arg(root.snapshot.tokenA || "")
|
||||
value: root.snapshot.depositA || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Deposit %1").arg(root.snapshot.tokenB || "")
|
||||
value: root.snapshot.depositB || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Receive at least")
|
||||
value: root.snapshot.minLpReceived || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Current ratio")
|
||||
value: root.snapshot.currentRatio || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Fee tier")
|
||||
value: root.snapshot.feeTier || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Slippage tolerance")
|
||||
value: root.snapshot.slippageTolerance || ""
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
visible: !root.isAdd
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Burn LP")
|
||||
value: qsTr("%1 (%2)")
|
||||
.arg(root.snapshot.burnText || "")
|
||||
.arg(root.snapshot.burnPercent || "")
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Receive at least %1").arg(root.snapshot.tokenA || "")
|
||||
value: root.snapshot.minTokenAReceived || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Receive at least %1").arg(root.snapshot.tokenB || "")
|
||||
value: root.snapshot.minTokenBReceived || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Slippage tolerance")
|
||||
value: root.snapshot.slippageTolerance || ""
|
||||
}
|
||||
|
||||
SummaryRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Post-removal share")
|
||||
value: root.snapshot.postRemovalShare || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var theme
|
||||
property var snapshot: ({})
|
||||
property bool open: false
|
||||
|
||||
signal canceled
|
||||
signal confirmed(var snapshot)
|
||||
|
||||
visible: root.open
|
||||
z: 20
|
||||
|
||||
Keys.onEscapePressed: root.cancel()
|
||||
|
||||
function openWithSnapshot(nextSnapshot) {
|
||||
root.snapshot = nextSnapshot;
|
||||
root.open = true;
|
||||
root.forceActiveFocus();
|
||||
cancelButton.forceActiveFocus();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
root.open = false;
|
||||
root.canceled();
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
const confirmedSnapshot = root.snapshot;
|
||||
root.open = false;
|
||||
root.confirmed(confirmedSnapshot);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#99000000"
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: panel
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.theme.colors.cardBg
|
||||
implicitHeight: dialogContent.implicitHeight + 32
|
||||
radius: 16
|
||||
width: Math.max(0, Math.min(380, root.width - 32))
|
||||
border.color: root.theme.colors.border
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: dialogContent
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 14
|
||||
|
||||
Text {
|
||||
color: root.theme.colors.textPrimary
|
||||
font.bold: true
|
||||
font.pixelSize: 17
|
||||
text: qsTr("Confirm swap")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 10
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: root.theme.colors.inputBg
|
||||
radius: 12
|
||||
implicitHeight: payColumn.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: payColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: qsTr("You pay")
|
||||
color: root.theme.colors.textSecondary
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: qsTr("%1 %2")
|
||||
.arg(root.snapshot.sellAmount || "")
|
||||
.arg(root.snapshot.sellToken || "")
|
||||
color: root.theme.colors.textPrimary
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: root.theme.colors.inputBg
|
||||
radius: 12
|
||||
implicitHeight: receiveColumn.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: receiveColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: qsTr("You receive at least")
|
||||
color: root.theme.colors.textSecondary
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: qsTr("%1 %2")
|
||||
.arg(root.snapshot.minReceived || "")
|
||||
.arg(root.snapshot.buyToken || "")
|
||||
color: root.theme.colors.textPrimary
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwapSummary {
|
||||
Layout.fillWidth: true
|
||||
theme: root.theme
|
||||
swapModeText: root.snapshot.swapModeText || ""
|
||||
feeText: root.snapshot.feeAmount || ""
|
||||
priceImpactText: root.snapshot.priceImpactPercent || ""
|
||||
priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0
|
||||
slippageText: root.snapshot.slippageTolerance || ""
|
||||
minReceivedText: qsTr("%1 %2")
|
||||
.arg(root.snapshot.minReceived || "")
|
||||
.arg(root.snapshot.buyToken || "")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
|
||||
Button {
|
||||
id: cancelButton
|
||||
activeFocusOnTab: true
|
||||
focusPolicy: Qt.StrongFocus
|
||||
hoverEnabled: true
|
||||
text: qsTr("Cancel")
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 48
|
||||
onClicked: root.cancel()
|
||||
|
||||
contentItem: Text {
|
||||
color: root.theme.colors.textPrimary
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
font.pixelSize: 14
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: cancelButton.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
background: Rectangle {
|
||||
border.color: root.theme.colors.borderStrong
|
||||
border.width: 1
|
||||
color: cancelButton.pressed
|
||||
? root.theme.colors.panelHoverBg
|
||||
: cancelButton.hovered || cancelButton.activeFocus
|
||||
? root.theme.colors.panelBg
|
||||
: "transparent"
|
||||
radius: 14
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: confirmButton
|
||||
activeFocusOnTab: true
|
||||
focusPolicy: Qt.StrongFocus
|
||||
hoverEnabled: true
|
||||
text: qsTr("Confirm Swap")
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 48
|
||||
onClicked: root.confirm()
|
||||
|
||||
contentItem: Text {
|
||||
color: "#ffffff"
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
font.pixelSize: 14
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: confirmButton.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
background: Rectangle {
|
||||
border.width: 0
|
||||
color: confirmButton.pressed
|
||||
? "#D95C1E"
|
||||
: confirmButton.hovered || confirmButton.activeFocus
|
||||
? root.theme.colors.ctaHoverBg
|
||||
: root.theme.colors.ctaBg
|
||||
radius: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
apps/amm/qml/components/swap/SwapConfirmationSummary.qml
Normal file
88
apps/amm/qml/components/swap/SwapConfirmationSummary.qml
Normal file
@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var theme
|
||||
property var snapshot: ({})
|
||||
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: root.theme.colors.inputBg
|
||||
radius: 8
|
||||
implicitHeight: payColumn.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: payColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("You pay")
|
||||
color: root.theme.colors.textSecondary
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("%1 %2")
|
||||
.arg(root.snapshot.sellAmount || "")
|
||||
.arg(root.snapshot.sellToken || "")
|
||||
color: root.theme.colors.textPrimary
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: root.theme.colors.inputBg
|
||||
radius: 8
|
||||
implicitHeight: receiveColumn.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: receiveColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("You receive at least")
|
||||
color: root.theme.colors.textSecondary
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("%1 %2")
|
||||
.arg(root.snapshot.minReceived || "")
|
||||
.arg(root.snapshot.buyToken || "")
|
||||
color: root.theme.colors.textPrimary
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwapSummary {
|
||||
Layout.fillWidth: true
|
||||
theme: root.theme
|
||||
swapModeText: root.snapshot.swapModeText || ""
|
||||
feeText: root.snapshot.feeAmount || ""
|
||||
priceImpactText: root.snapshot.priceImpactPercent || ""
|
||||
priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0
|
||||
slippageText: root.snapshot.slippageTolerance || ""
|
||||
minReceivedText: qsTr("%1 %2")
|
||||
.arg(root.snapshot.minReceived || "")
|
||||
.arg(root.snapshot.buyToken || "")
|
||||
}
|
||||
}
|
||||
@ -1,490 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQml
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// Header wallet control (Uniswap-style), with two states:
|
||||
// - not connected → a "Connect" button that opens the create-wallet modal
|
||||
// - connected → a single button showing the active account address;
|
||||
// clicking it opens a popup (top-right, just under the
|
||||
// button) holding the account selector, create-account and
|
||||
// disconnect actions.
|
||||
// The selected account address is exposed via selectedAddress for the
|
||||
// trade/liquidity flows to use as the "from" account.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Backend replica (logos.module("amm_ui")) and its account model.
|
||||
property var backend: null
|
||||
property var accountModel: null
|
||||
|
||||
readonly property bool connected: backend !== null && backend.isWalletOpen
|
||||
|
||||
// Index of the active account. selectedAddress/selectedName are derived from
|
||||
// the model mirror below so they stay valid while the popup (and its list)
|
||||
// is closed.
|
||||
property int selectedIndex: 0
|
||||
|
||||
// Non-visual mirror of the account model: realizes every row regardless of
|
||||
// popup visibility, so the active account is addressable by index at all
|
||||
// times (a ListView only realizes rows while it is shown).
|
||||
Instantiator {
|
||||
id: accounts
|
||||
model: root.accountModel
|
||||
delegate: QtObject {
|
||||
readonly property string address: model.address ?? ""
|
||||
readonly property string name: model.name ?? ""
|
||||
readonly property string balance: model.balance ?? ""
|
||||
readonly property bool isPublic: model.isPublic ?? false
|
||||
}
|
||||
}
|
||||
|
||||
function entryAt(i) {
|
||||
return (i >= 0 && i < accounts.count) ? accounts.objectAt(i) : null
|
||||
}
|
||||
|
||||
readonly property string selectedAddress: {
|
||||
const e = root.entryAt(root.selectedIndex)
|
||||
return e ? e.address : ""
|
||||
}
|
||||
readonly property string selectedName: {
|
||||
const e = root.entryAt(root.selectedIndex)
|
||||
return e ? e.name : ""
|
||||
}
|
||||
readonly property string selectedBalance: {
|
||||
const e = root.entryAt(root.selectedIndex)
|
||||
return e ? e.balance : ""
|
||||
}
|
||||
readonly property bool selectedIsPublic: {
|
||||
const e = root.entryAt(root.selectedIndex)
|
||||
return e ? e.isPublic : false
|
||||
}
|
||||
|
||||
// Keep the selection within bounds as accounts are added/removed.
|
||||
function clampSelection() {
|
||||
if (accounts.count === 0) { root.selectedIndex = 0; return }
|
||||
if (root.selectedIndex < 0) root.selectedIndex = 0
|
||||
else if (root.selectedIndex >= accounts.count) root.selectedIndex = accounts.count - 1
|
||||
}
|
||||
Connections {
|
||||
target: root.accountModel
|
||||
ignoreUnknownSignals: true
|
||||
function onModelReset() { root.clampSelection() }
|
||||
function onRowsInserted() { root.clampSelection() }
|
||||
function onRowsRemoved() { root.clampSelection() }
|
||||
}
|
||||
|
||||
// 0x123456…cdef style truncation for the connected button label.
|
||||
function truncated(addr) {
|
||||
if (!addr) return ""
|
||||
return addr.length > 13 ? (addr.substring(0, 6) + "…" + addr.substring(addr.length - 4)) : addr
|
||||
}
|
||||
|
||||
// Copy on the QML/view side. Routing this through the backend would call
|
||||
// QGuiApplication::clipboard() in the (headless) module host process, which
|
||||
// has no clipboard — that call tears the backend down, dropping the wallet
|
||||
// connection. A hidden TextEdit copies via the GUI process that owns it.
|
||||
TextEdit { id: clipboardProxy; visible: false }
|
||||
function copyToClipboard(text) {
|
||||
if (!text) return
|
||||
clipboardProxy.text = text
|
||||
clipboardProxy.selectAll()
|
||||
clipboardProxy.copy()
|
||||
clipboardProxy.deselect()
|
||||
clipboardProxy.text = ""
|
||||
}
|
||||
|
||||
implicitWidth: root.connected ? connectedButton.width : connectButton.width
|
||||
implicitHeight: 40
|
||||
|
||||
// ── Disconnected: Connect ────────────────────────────────────────────
|
||||
LogosButton {
|
||||
id: connectButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 40
|
||||
visible: !root.connected
|
||||
enabled: root.backend !== null
|
||||
text: qsTr("Connect")
|
||||
onClicked: {
|
||||
// Re-open an existing wallet; only show the create modal on first run.
|
||||
if (root.backend && root.backend.walletExists)
|
||||
logos.watch(root.backend.openExisting(),
|
||||
function(ok) { if (!ok) console.warn("openExisting failed") },
|
||||
function(error) { console.warn("openExisting error:", error) })
|
||||
else
|
||||
createWalletDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connected: address pill that toggles the wallet menu ─────────────
|
||||
Rectangle {
|
||||
id: connectedButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.connected
|
||||
implicitHeight: 40
|
||||
implicitWidth: connectedRow.implicitWidth + Theme.spacing.medium * 2
|
||||
radius: height / 2
|
||||
// Keep an opaque dark fill in both states: the navbar is white, and the
|
||||
// active "muted" fill is translucent gray, which renders light over white
|
||||
// and makes the white label unreadable. Signal "open" with an accent
|
||||
// border instead.
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.width: 1
|
||||
border.color: walletMenu.opened ? Theme.palette.overlayOrange : "transparent"
|
||||
|
||||
RowLayout {
|
||||
id: connectedRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 8
|
||||
Layout.preferredHeight: 8
|
||||
radius: 4
|
||||
color: "#39c06a"
|
||||
}
|
||||
LogosText {
|
||||
text: root.truncated(root.selectedAddress) || qsTr("Connected")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.text
|
||||
}
|
||||
LogosText {
|
||||
text: walletMenu.opened ? "▴" : "▾"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// CloseOnPressOutside already dismisses the popup on this same press
|
||||
// (the button is outside it), so `opened` is false by the time this
|
||||
// fires. Without the recency guard the dismissing click would just
|
||||
// reopen it. If it just closed, leave it closed.
|
||||
onClicked: {
|
||||
if (walletMenu.opened || (Date.now() - walletMenu.lastClosedMs) < 200)
|
||||
walletMenu.close()
|
||||
else
|
||||
walletMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wallet menu popup (top-right, under the connected button) ─────────
|
||||
Popup {
|
||||
id: walletMenu
|
||||
parent: connectedButton
|
||||
y: connectedButton.height + Theme.spacing.small
|
||||
x: connectedButton.width - width // right-align under the button
|
||||
width: 360
|
||||
padding: Theme.spacing.medium
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
// Timestamp of the last dismissal, used by the toggle button to tell a
|
||||
// genuine "open" click from the press that just closed the popup.
|
||||
property real lastClosedMs: 0
|
||||
onClosed: {
|
||||
walletMenu.lastClosedMs = Date.now()
|
||||
// Always reopen on the main (selected-account) view.
|
||||
if (viewStack.depth > 1)
|
||||
viewStack.pop(null, StackView.Immediate)
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundTertiary
|
||||
border.width: 1
|
||||
border.color: Theme.palette.backgroundElevated
|
||||
radius: Theme.spacing.radiusLarge
|
||||
}
|
||||
|
||||
// Two stacked views: the main view (active account + actions) and the
|
||||
// accounts view (full list + create). The popup height follows the
|
||||
// active view's natural height, animated so the resize isn't abrupt.
|
||||
contentItem: StackView {
|
||||
id: viewStack
|
||||
clip: true
|
||||
implicitWidth: walletMenu.availableWidth
|
||||
implicitHeight: currentItem ? currentItem.implicitHeight : 0
|
||||
initialItem: mainView
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
pushEnter: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 120 } }
|
||||
pushExit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 120 } }
|
||||
popEnter: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 120 } }
|
||||
popExit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 120 } }
|
||||
}
|
||||
|
||||
// ── Main view: account icon + power icon, then the active account ──
|
||||
Component {
|
||||
id: mainView
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Top-right actions: open the account list / disconnect.
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
WalletIconButton {
|
||||
iconSource: Qt.resolvedUrl("icons/account.svg")
|
||||
onClicked: viewStack.push(accountsView)
|
||||
}
|
||||
WalletIconButton {
|
||||
iconSource: Qt.resolvedUrl("icons/settings.svg")
|
||||
onClicked: viewStack.push(settingsView)
|
||||
}
|
||||
WalletIconButton {
|
||||
iconSource: Qt.resolvedUrl("icons/power.svg")
|
||||
onClicked: {
|
||||
walletMenu.close()
|
||||
if (root.backend) root.backend.disconnectWallet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Active account card.
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: cardColumn.implicitHeight + Theme.spacing.medium * 2
|
||||
radius: Theme.spacing.radiusLarge
|
||||
color: Theme.palette.backgroundMuted
|
||||
|
||||
ColumnLayout {
|
||||
id: cardColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.medium
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: root.selectedName
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
Rectangle {
|
||||
Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
|
||||
Layout.preferredHeight: tagLabel.implicitHeight + 4
|
||||
radius: 4
|
||||
color: Theme.palette.backgroundSecondary
|
||||
LogosText {
|
||||
id: tagLabel
|
||||
anchors.centerIn: parent
|
||||
text: root.selectedIsPublic ? qsTr("Public") : qsTr("Private")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosText {
|
||||
text: root.selectedBalance.length > 0 ? root.selectedBalance : "—"
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: root.selectedAddress
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textMuted
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
LogosCopyButton {
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 40
|
||||
visible: root.selectedAddress.length > 0
|
||||
onCopyText: root.copyToClipboard(root.selectedAddress)
|
||||
icon.color: Theme.palette.textMuted
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Accounts view: back + full list + create ──────────────────────
|
||||
Component {
|
||||
id: accountsView
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Header: back to the main view + title.
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
WalletIconButton {
|
||||
iconSource: Qt.resolvedUrl("icons/back.svg")
|
||||
onClicked: viewStack.pop()
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Accounts")
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
}
|
||||
|
||||
// Account list: tap a row to make it the active account, then
|
||||
// return to the main view so the selection is reflected.
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(contentHeight, 260)
|
||||
clip: true
|
||||
model: root.accountModel
|
||||
spacing: Theme.spacing.small
|
||||
ScrollIndicator.vertical: ScrollIndicator { }
|
||||
|
||||
delegate: AccountDelegate {
|
||||
width: ListView.view.width
|
||||
highlighted: index === root.selectedIndex
|
||||
onClicked: {
|
||||
root.selectedIndex = index
|
||||
viewStack.pop()
|
||||
}
|
||||
onCopyRequested: (text) => root.copyToClipboard(text)
|
||||
}
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
height: 40
|
||||
text: qsTr("Add")
|
||||
// Leave the wallet menu open behind the (modal) dialog.
|
||||
onClicked: createAccountDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Settings view: back + editable network (sequencer) ────────────
|
||||
Component {
|
||||
id: settingsView
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Header: back to the main view + title.
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
WalletIconButton {
|
||||
iconSource: Qt.resolvedUrl("icons/back.svg")
|
||||
onClicked: viewStack.pop()
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Settings")
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Network (sequencer URL)")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: seqField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "http://127.0.0.1:3040"
|
||||
// Initialize from the live value without binding, so typing
|
||||
// isn't clobbered when sequencerAddr updates after a save.
|
||||
Component.onCompleted: text = root.backend ? root.backend.sequencerAddr : ""
|
||||
}
|
||||
|
||||
LogosText {
|
||||
id: seqStatus
|
||||
Layout.fillWidth: true
|
||||
visible: text.length > 0
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
property bool ok: false
|
||||
color: ok ? Theme.palette.success : Theme.palette.error
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
height: 40
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
if (!root.backend) return
|
||||
seqStatus.text = ""
|
||||
logos.watch(root.backend.changeSequencerAddr(seqField.text),
|
||||
function(ok) {
|
||||
seqStatus.ok = ok
|
||||
seqStatus.text = ok ? qsTr("Network updated.")
|
||||
: qsTr("Failed to update network.")
|
||||
},
|
||||
function(error) {
|
||||
seqStatus.ok = false
|
||||
seqStatus.text = qsTr("Error: %1").arg(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialogs ──────────────────────────────────────────────────────────
|
||||
CreateWalletDialog {
|
||||
id: createWalletDialog
|
||||
walletHome: root.backend ? root.backend.walletHome : ""
|
||||
onCreateWallet: function(password) {
|
||||
if (!root.backend) return
|
||||
// createNewDefault returns the new wallet's seed phrase (empty on
|
||||
// failure). On success we hand it to the dialog, which switches to
|
||||
// its backup page — we do NOT close here, so the user can't skip it.
|
||||
logos.watch(root.backend.createNewDefault(password),
|
||||
function(mnemonic) {
|
||||
if (mnemonic && mnemonic.length > 0)
|
||||
createWalletDialog.mnemonic = mnemonic
|
||||
else
|
||||
createWalletDialog.createError = qsTr("Failed to create wallet. Please try again.")
|
||||
},
|
||||
function(error) {
|
||||
createWalletDialog.createError = qsTr("Error creating wallet: %1").arg(error)
|
||||
})
|
||||
}
|
||||
onCopyRequested: function(text) {
|
||||
if (root.backend) root.backend.copyToClipboard(text)
|
||||
}
|
||||
}
|
||||
|
||||
CreateAccountDialog {
|
||||
id: createAccountDialog
|
||||
onCreatePublicRequested: {
|
||||
if (!root.backend) return
|
||||
logos.watch(root.backend.createAccountPublic(),
|
||||
function(_id) { /* model updates via NOTIFY after refresh */ },
|
||||
function(error) { console.warn("createAccountPublic failed:", error) })
|
||||
}
|
||||
onCreatePrivateRequested: {
|
||||
if (!root.backend) return
|
||||
logos.watch(root.backend.createAccountPrivate(),
|
||||
function(_id) { /* model updates via NOTIFY after refresh */ },
|
||||
function(error) { console.warn("createAccountPrivate failed:", error) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// One account row in the account dropdown. Ported from the LEZ wallet UI.
|
||||
ItemDelegate {
|
||||
id: root
|
||||
|
||||
// Emitted when the user clicks the copy icon; the parent view connects this
|
||||
// to its QML-side clipboard helper (AccountControl.copyToClipboard).
|
||||
signal copyRequested(string text)
|
||||
|
||||
leftPadding: Theme.spacing.medium
|
||||
rightPadding: Theme.spacing.medium
|
||||
topPadding: Theme.spacing.medium
|
||||
bottomPadding: Theme.spacing.medium
|
||||
|
||||
background: Rectangle {
|
||||
color: root.highlighted || root.hovered ?
|
||||
Theme.palette.backgroundMuted :
|
||||
Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Theme.spacing.small
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: model.name ?? ""
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
|
||||
Layout.preferredHeight: tagLabel.implicitHeight + 4
|
||||
radius: 4
|
||||
color: Theme.palette.backgroundSecondary
|
||||
|
||||
LogosText {
|
||||
id: tagLabel
|
||||
anchors.centerIn: parent
|
||||
text: model.isPublic ? qsTr("Public") : qsTr("Private")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosText {
|
||||
text: model.balance && model.balance.length > 0 ? model.balance : "—"
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
LogosText {
|
||||
id: addressLabel
|
||||
Layout.fillWidth: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: model.address ?? ""
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textMuted
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
LogosCopyButton {
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 40
|
||||
onCopyText: root.copyRequested(model.address)
|
||||
visible: addressLabel.text
|
||||
icon.color: Theme.palette.textMuted
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// Public/private account creation dialog. Ported from the LEZ wallet UI.
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
signal createPublicRequested()
|
||||
signal createPrivateRequested()
|
||||
|
||||
modal: true
|
||||
dim: true
|
||||
padding: Theme.spacing.large
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
// Center on the full-window overlay rather than the small navbar control
|
||||
// this popup is declared inside.
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
width: 360
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
border.color: Theme.palette.backgroundElevated
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentLayout
|
||||
// Pin to the popup's padded width so children stay within the modal.
|
||||
width: root.availableWidth
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Create account")
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
font.weight: Theme.typography.weightBold
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Choose account type.")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
Layout.topMargin: -Theme.spacing.small
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Private")
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
color: Theme.palette.text
|
||||
}
|
||||
LogosText {
|
||||
text: qsTr("Private balance and activity.")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
LogosSwitch {
|
||||
id: privateSwitch
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Theme.spacing.medium
|
||||
spacing: Theme.spacing.medium
|
||||
Layout.fillWidth: true
|
||||
LogosButton {
|
||||
text: qsTr("Cancel")
|
||||
Layout.fillWidth: true
|
||||
onClicked: root.close()
|
||||
}
|
||||
LogosButton {
|
||||
text: qsTr("Create")
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
if (privateSwitch.checked)
|
||||
root.createPrivateRequested()
|
||||
else
|
||||
root.createPublicRequested()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,201 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// Wallet creation modal. Two pages, driven by whether a mnemonic exists yet:
|
||||
// 1. Password entry — emits createWallet(password); the parent creates the
|
||||
// wallet and, on success, sets `mnemonic` to the returned seed phrase.
|
||||
// 2. Seed-phrase backup — shows the mnemonic once and gates dismissal behind
|
||||
// an explicit acknowledgement. This is the only time the phrase is shown,
|
||||
// so the popup is not auto-dismissable while it is visible.
|
||||
// Storage/config live at the per-app default (backend.walletHome) — no path
|
||||
// picking. Opened from the navbar "Connect" button.
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
// Where the wallet will be stored, shown for transparency.
|
||||
property string walletHome: ""
|
||||
property string createError: ""
|
||||
// Set by the parent to the BIP39 seed phrase once creation succeeds. A
|
||||
// non-empty value flips the dialog to the backup page.
|
||||
property string mnemonic: ""
|
||||
|
||||
signal createWallet(string password)
|
||||
signal copyRequested(string text)
|
||||
|
||||
modal: true
|
||||
dim: true
|
||||
padding: Theme.spacing.large
|
||||
// Once the wallet exists we must not let the user dismiss the modal (and
|
||||
// lose the only view of their seed phrase) by clicking away or pressing Esc.
|
||||
closePolicy: root.mnemonic.length > 0
|
||||
? Popup.NoAutoClose
|
||||
: (Popup.CloseOnEscape | Popup.CloseOnPressOutside)
|
||||
// Center on the full-window overlay rather than the small navbar control
|
||||
// this popup is declared inside.
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
width: 380
|
||||
|
||||
onOpened: {
|
||||
passwordField.text = ""
|
||||
confirmField.text = ""
|
||||
root.createError = ""
|
||||
root.mnemonic = ""
|
||||
passwordField.forceActiveFocus()
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
border.color: Theme.palette.backgroundElevated
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
// Pin to the popup's padded width so long text wraps and fillWidth
|
||||
// children don't push the layout wider than the modal.
|
||||
width: root.availableWidth
|
||||
spacing: 0
|
||||
|
||||
// ── Page 1: password entry ────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
id: passwordPage
|
||||
visible: root.mnemonic.length === 0
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Create your wallet")
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
font.weight: Theme.typography.weightBold
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Secure your wallet with a password. It will be stored on this device at %1.")
|
||||
.arg(root.walletHome || qsTr("the default location"))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -Theme.spacing.small
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: passwordField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Password")
|
||||
echoMode: TextInput.Password
|
||||
Keys.onReturnPressed: createButton.tryCreate()
|
||||
}
|
||||
LogosTextField {
|
||||
id: confirmField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Confirm password")
|
||||
echoMode: TextInput.Password
|
||||
Keys.onReturnPressed: createButton.tryCreate()
|
||||
}
|
||||
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.error
|
||||
wrapMode: Text.WordWrap
|
||||
visible: text.length > 0
|
||||
text: root.createError
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Theme.spacing.small
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.medium
|
||||
LogosButton {
|
||||
text: qsTr("Cancel")
|
||||
Layout.fillWidth: true
|
||||
onClicked: root.close()
|
||||
}
|
||||
LogosButton {
|
||||
id: createButton
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Create Wallet")
|
||||
function tryCreate() {
|
||||
if (passwordField.text.length === 0) {
|
||||
root.createError = qsTr("Password cannot be empty.")
|
||||
} else if (passwordField.text !== confirmField.text) {
|
||||
root.createError = qsTr("Passwords do not match.")
|
||||
} else {
|
||||
root.createError = ""
|
||||
root.createWallet(passwordField.text)
|
||||
}
|
||||
}
|
||||
onClicked: tryCreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Page 2: seed-phrase backup ────────────────────────────────────
|
||||
ColumnLayout {
|
||||
id: backupPage
|
||||
visible: root.mnemonic.length > 0
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Back up your recovery phrase")
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
font.weight: Theme.typography.weightBold
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Write these words down in order and store them somewhere safe. Anyone with this phrase can control your wallet, and it will not be shown again — it is the only way to recover access.")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -Theme.spacing.small
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
radius: Theme.spacing.radiusLarge
|
||||
color: Theme.palette.backgroundElevated
|
||||
implicitHeight: phraseText.implicitHeight + 2 * Theme.spacing.medium
|
||||
|
||||
LogosText {
|
||||
id: phraseText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.medium
|
||||
text: root.mnemonic
|
||||
wrapMode: Text.WordWrap
|
||||
lineHeight: 1.4
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
font.weight: Theme.typography.weightBold
|
||||
color: Theme.palette.text
|
||||
}
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Copy to clipboard")
|
||||
onClicked: root.copyRequested(root.mnemonic)
|
||||
}
|
||||
|
||||
LogosCheckbox {
|
||||
id: ackCheck
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("I have safely backed up my recovery phrase")
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
enabled: ackCheck.checked
|
||||
text: qsTr("Continue")
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import Logos.Theme
|
||||
|
||||
Button {
|
||||
id: root
|
||||
|
||||
signal copyText()
|
||||
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
display: AbstractButton.IconOnly
|
||||
flat: true
|
||||
|
||||
property string iconSource: Qt.resolvedUrl("icons/copy.svg")
|
||||
|
||||
icon.source: root.iconSource
|
||||
icon.width: 24
|
||||
icon.height: 24
|
||||
icon.color: Theme.palette.textSecondary
|
||||
|
||||
function reset() {
|
||||
iconSource = Qt.resolvedUrl("icons/copy.svg")
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resetTimer
|
||||
interval: 1500
|
||||
repeat: false
|
||||
onTriggered: root.reset()
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.copyText()
|
||||
root.iconSource = Qt.resolvedUrl("icons/checkmark.svg")
|
||||
resetTimer.restart()
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import Logos.Theme
|
||||
|
||||
// Small icon-only action button for the wallet menu. Uses the same Button +
|
||||
// icon.source/icon.color path as LogosCopyButton, which renders reliably here
|
||||
// (LogosIconButton's Image + MultiEffect shader path does not).
|
||||
Button {
|
||||
id: root
|
||||
|
||||
property url iconSource
|
||||
property color iconColor: Theme.palette.textSecondary
|
||||
property int iconSize: 18
|
||||
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
display: AbstractButton.IconOnly
|
||||
flat: true
|
||||
|
||||
icon.source: root.iconSource
|
||||
icon.width: root.iconSize
|
||||
icon.height: root.iconSize
|
||||
icon.color: root.iconColor
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" fill="#000"/><path d="M4 20c0-3.866 3.582-6 8-6s8 2.134 8 6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 253 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M15 5l-7 7 7 7"/></svg>
|
||||
|
Before Width: | Height: | Size: 206 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" height="17" viewBox="0 0 16 17" width="16" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m6.76413 10.1901 5.56067-5.41122c.382-.37171 1-.37297 1.3863.00299.3837.37336.3857.97674.0031 1.34906l-6.25845 6.09017c-.18993.1849-.43822.2781-.68737.2789-.25655-.0019-.50554-.0937-.69254-.2756l-2.79161-2.7166c-.38013-.36991-.37994-.96985.00641-1.34581.38367-.37336 1.00799-.37115 1.38299-.00623z" fill="#000" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 463 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path d="m4.16634 7c.27614 0 .5-.22386.5-.5s-.22386-.5-.5-.5h-.16667c-1.47275 0-2.66666 1.19391-2.66666 2.66667v3.33333c0 1.4728 1.19391 2.6667 2.66666 2.6667h3.33334c1.47276 0 2.66666-1.1939 2.66666-2.6667v-.1667c0-.2761-.22385-.5-.5-.5-.27614 0-.5.2239-.5.5v.1667c0 .9205-.74619 1.6667-1.66666 1.6667h-3.33334c-.92047 0-1.66666-.7462-1.66666-1.6667v-3.33333c0-.92048.74619-1.66667 1.66666-1.66667z"/><path clip-rule="evenodd" d="m5.99967 4c0-1.47276 1.19391-2.66666 2.66667-2.66666h3.33336c1.4727 0 2.6666 1.1939 2.6666 2.66666v3.33334c0 1.47276-1.1939 2.66666-2.6666 2.66666h-3.33336c-1.47276 0-2.66667-1.1939-2.66667-2.66666zm2.66667-1.66666h3.33336c.9204 0 1.6666.74619 1.6666 1.66666v3.33334c0 .92047-.7462 1.66666-1.6666 1.66666h-3.33336c-.92047 0-1.66667-.74619-1.66667-1.66666v-3.33334c0-.92047.7462-1.66666 1.66667-1.66666z" fill-rule="evenodd"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 977 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M12 3v9"/><path d="M7.05 7.05a7 7 0 1 0 9.9 0"/></svg>
|
||||
|
Before Width: | Height: | Size: 237 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 845 B |
@ -1,5 +1,6 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Logos.Wallet
|
||||
import "../components/shared"
|
||||
import "../components/liquidity"
|
||||
import "../state"
|
||||
@ -162,10 +163,18 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LiquidityConfirmationDialog {
|
||||
id: confirmationDialog
|
||||
Component {
|
||||
id: liquidityConfirmationSummary
|
||||
|
||||
anchors.fill: parent
|
||||
LiquidityConfirmationSummary { }
|
||||
}
|
||||
|
||||
TransactionConfirmationDialog {
|
||||
id: confirmationDialog
|
||||
title: snapshot.action === "add"
|
||||
? qsTr("Confirm add liquidity")
|
||||
: qsTr("Confirm remove liquidity")
|
||||
summary: liquidityConfirmationSummary
|
||||
|
||||
onConfirmed: function (snapshot) {
|
||||
root.confirmLiquidityAction(snapshot);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Logos.Wallet
|
||||
import "../components/shared"
|
||||
import "../components/swap"
|
||||
import "../state"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@ -18,7 +19,7 @@ Item {
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: theme
|
||||
id: pageTheme
|
||||
property bool isDark: true
|
||||
property var colors: isDark ? dark : light
|
||||
|
||||
@ -61,7 +62,7 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: theme.colors.background
|
||||
color: pageTheme.colors.background
|
||||
Behavior on color { ColorAnimation { duration: 300 } }
|
||||
|
||||
// Theme toggle
|
||||
@ -70,19 +71,19 @@ Item {
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 16
|
||||
width: 44; height: 24; radius: 12
|
||||
color: theme.colors.panelBg
|
||||
border.color: theme.colors.border
|
||||
color: pageTheme.colors.panelBg
|
||||
border.color: pageTheme.colors.border
|
||||
border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: theme.isDark ? "☀" : "☾"
|
||||
text: pageTheme.isDark ? "☀" : "☾"
|
||||
font.pixelSize: 13
|
||||
color: theme.colors.textSecondary
|
||||
color: pageTheme.colors.textSecondary
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: theme.isDark = !theme.isDark
|
||||
onClicked: pageTheme.isDark = !pageTheme.isDark
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +94,7 @@ Item {
|
||||
SwapCard {
|
||||
id: swapCard
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
theme: theme
|
||||
theme: pageTheme
|
||||
tokens: root.tokens
|
||||
width: Math.min(480, root.width - 32)
|
||||
|
||||
@ -109,9 +110,9 @@ Item {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "Buy and sell crypto on <font color='" + theme.colors.textPrimary + "'>LEZ</font>."
|
||||
text: "Buy and sell crypto on <font color='" + pageTheme.colors.textPrimary + "'>LEZ</font>."
|
||||
textFormat: Text.RichText
|
||||
color: theme.colors.textSecondary
|
||||
color: pageTheme.colors.textSecondary
|
||||
font.pixelSize: 15
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
@ -121,7 +122,7 @@ Item {
|
||||
id: tokenModal
|
||||
anchors.fill: parent
|
||||
z: 10
|
||||
theme: theme
|
||||
theme: pageTheme
|
||||
tokens: root.tokens
|
||||
|
||||
property string targetSide: "sell"
|
||||
@ -144,10 +145,19 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SwapConfirmationDialog {
|
||||
Component {
|
||||
id: swapConfirmationSummary
|
||||
|
||||
SwapConfirmationSummary {
|
||||
theme: pageTheme
|
||||
}
|
||||
}
|
||||
|
||||
TransactionConfirmationDialog {
|
||||
id: swapConfirmationDialog
|
||||
anchors.fill: parent
|
||||
theme: theme
|
||||
title: qsTr("Confirm swap")
|
||||
confirmText: qsTr("Confirm swap")
|
||||
summary: swapConfirmationSummary
|
||||
|
||||
onConfirmed: function(snapshot) {
|
||||
swapCard.resetAmounts()
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
#include "AccountModel.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
AccountModel::AccountModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int AccountModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_entries.size();
|
||||
}
|
||||
|
||||
QVariant AccountModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size())
|
||||
return QVariant();
|
||||
|
||||
const AccountEntry& e = m_entries.at(index.row());
|
||||
switch (role) {
|
||||
case NameRole: return e.name;
|
||||
case AddressRole: return e.address;
|
||||
case BalanceRole: return e.balance;
|
||||
case IsPublicRole: return e.isPublic;
|
||||
default: return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ NameRole, "name" },
|
||||
{ AddressRole, "address" },
|
||||
{ BalanceRole, "balance" },
|
||||
{ IsPublicRole, "isPublic" }
|
||||
};
|
||||
}
|
||||
|
||||
void AccountModel::replaceFromJsonArray(const QJsonArray& arr)
|
||||
{
|
||||
beginResetModel();
|
||||
const int oldCount = m_entries.size();
|
||||
m_entries.clear();
|
||||
int idx = 0;
|
||||
for (const QJsonValue& v : arr) {
|
||||
AccountEntry e;
|
||||
e.name = QStringLiteral("Account %1").arg(++idx);
|
||||
e.balance = QString();
|
||||
if (v.isObject()) {
|
||||
const QJsonObject obj = v.toObject();
|
||||
e.address = obj.value(QStringLiteral("account_id")).toString();
|
||||
e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true);
|
||||
} else {
|
||||
e.address = v.toString();
|
||||
e.isPublic = true;
|
||||
}
|
||||
m_entries.append(e);
|
||||
}
|
||||
endResetModel();
|
||||
if (oldCount != m_entries.size())
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void AccountModel::setBalanceByAddress(const QString& address, const QString& balance)
|
||||
{
|
||||
for (int i = 0; i < m_entries.size(); ++i) {
|
||||
if (m_entries.at(i).address == address) {
|
||||
if (m_entries.at(i).balance != balance) {
|
||||
m_entries[i].balance = balance;
|
||||
const QModelIndex idx = index(i, 0);
|
||||
emit dataChanged(idx, idx, { BalanceRole });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
// One wallet account row. Mirrors the shape returned by the core wallet
|
||||
// module's list_accounts() (account_id + is_public), with a display name and
|
||||
// a lazily-fetched balance.
|
||||
struct AccountEntry {
|
||||
QString name;
|
||||
QString address;
|
||||
QString balance;
|
||||
bool isPublic = true;
|
||||
};
|
||||
|
||||
// QAbstractListModel of wallet accounts, exposed to QML via
|
||||
// logos.model("amm_ui", "accountModel"). Ported from the LEZ wallet UI.
|
||||
class AccountModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
public:
|
||||
enum Role {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
AddressRole,
|
||||
BalanceRole,
|
||||
IsPublicRole
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
|
||||
explicit AccountModel(QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void replaceFromJsonArray(const QJsonArray& arr);
|
||||
void setBalanceByAddress(const QString& address, const QString& balance);
|
||||
int count() const { return m_entries.size(); }
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
private:
|
||||
QVector<AccountEntry> m_entries;
|
||||
};
|
||||
@ -1,15 +1,8 @@
|
||||
#include "AmmUiBackend.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
@ -17,27 +10,21 @@
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
namespace {
|
||||
const char SETTINGS_ORG[] = "Logos";
|
||||
const char SETTINGS_APP[] = "AmmUI";
|
||||
// Sticky "user pressed Disconnect" flag so the wallet stays locked across
|
||||
// relaunches until the user reconnects.
|
||||
const char DISCONNECTED_KEY[] = "disconnected";
|
||||
const int WALLET_FFI_SUCCESS = 0;
|
||||
const char SETTINGS_ORG[] = "Logos";
|
||||
const char SETTINGS_APP[] = "AmmUI";
|
||||
const char DISCONNECTED_KEY[] = "disconnected";
|
||||
const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
|
||||
|
||||
// Wallet home env override. Mirrors LEZ's own var so the app shares the
|
||||
// canonical wallet (~/.lee/wallet) used by the wallet UI and other apps.
|
||||
const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
|
||||
|
||||
// Normalise file:// URLs and OS paths to a plain local path.
|
||||
QString toLocalPath(const QString& path) {
|
||||
if (path.startsWith("file://") || path.contains("/"))
|
||||
return QUrl::fromUserInput(path).toLocalFile();
|
||||
return path;
|
||||
}
|
||||
QString toLocalPath(const QString& path)
|
||||
{
|
||||
if (path.startsWith(QStringLiteral("file://")) || path.contains(QLatin1Char('/')))
|
||||
return QUrl::fromUserInput(path).toLocalFile();
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
QString AmmUiBackend::defaultWalletHome()
|
||||
@ -45,8 +32,6 @@ QString AmmUiBackend::defaultWalletHome()
|
||||
const QByteArray override = qgetenv(WALLET_HOME_ENV);
|
||||
if (!override.isEmpty())
|
||||
return QString::fromLocal8Bit(override);
|
||||
// LEZ's canonical wallet home, shared with the wallet UI and other LEZ apps
|
||||
// (matches lez/wallet get_home_default_path()).
|
||||
return QDir::homePath() + QStringLiteral("/.lee/wallet");
|
||||
}
|
||||
|
||||
@ -62,286 +47,196 @@ QString AmmUiBackend::defaultStoragePath() const
|
||||
|
||||
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
: AmmUiBackendSimpleSource(parent),
|
||||
m_accountModel(new AccountModel(this)),
|
||||
m_accountModel(new WalletAccountModel(this)),
|
||||
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
|
||||
m_logos(new LogosModules(m_logosAPI)),
|
||||
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
|
||||
m_net(new QNetworkAccessManager(this)),
|
||||
m_reachabilityTimer(new QTimer(this))
|
||||
{
|
||||
// PROP defaults via the generated setters.
|
||||
setIsWalletOpen(false);
|
||||
setLastSyncedBlock(0);
|
||||
setCurrentBlockHeight(0);
|
||||
setWalletHome(defaultWalletHome());
|
||||
// Assume reachable until a probe proves otherwise (avoids a startup flash).
|
||||
setSequencerReachable(true);
|
||||
setWalletExists(QFileInfo::exists(defaultStoragePath()));
|
||||
|
||||
// Periodically re-probe the sequencer so the banner reacts to a node going
|
||||
// up/down while the app is running. Probes are no-ops until a wallet (and
|
||||
// thus a sequencer address) is open.
|
||||
m_reachabilityTimer->setInterval(10000);
|
||||
connect(m_reachabilityTimer, &QTimer::timeout, this, [this]() { checkReachability(); });
|
||||
connect(m_reachabilityTimer, &QTimer::timeout,
|
||||
this, &AmmUiBackend::checkReachability);
|
||||
m_reachabilityTimer->start();
|
||||
|
||||
// Always resolve against the canonical wallet home (LEE_WALLET_HOME_DIR or
|
||||
// ~/.lee/wallet). We intentionally don't seed config/storage paths from
|
||||
// QSettings anymore: a previously-persisted per-app path (~/.lee/amm-wallet)
|
||||
// would otherwise override the default and pin the app to the old keystore.
|
||||
|
||||
// A wallet exists on disk if its storage file is present (drives whether
|
||||
// the navbar "Connect" reconnects or offers to create a wallet).
|
||||
const QString effectiveStorage = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
||||
setWalletExists(QFileInfo::exists(effectiveStorage));
|
||||
|
||||
// ui-host runs our constructor inside initLogos(), synchronously, BEFORE
|
||||
// it enables remoting and emits READY. Any blocking RPC here would stall
|
||||
// ui-host startup past its ready watchdog. Defer the open+refresh chain to
|
||||
// the first event-loop tick so ui-host finishes wiring itself up first.
|
||||
QTimer::singleShot(0, this, [this]() { openOrAdoptWallet(); });
|
||||
|
||||
// Save wallet on quit; host may not call destructors so this is best-effort.
|
||||
connect(qApp, &QCoreApplication::aboutToQuit, this,
|
||||
[this]() { saveWallet(); }, Qt::DirectConnection);
|
||||
QTimer::singleShot(0, this, &AmmUiBackend::openOrAdoptWallet);
|
||||
}
|
||||
|
||||
AmmUiBackend::~AmmUiBackend()
|
||||
{
|
||||
saveWallet();
|
||||
delete m_logos;
|
||||
}
|
||||
AmmUiBackend::~AmmUiBackend() = default;
|
||||
|
||||
void AmmUiBackend::openOrAdoptWallet()
|
||||
{
|
||||
// Respect an explicit user disconnect: stay locked, show "Connect".
|
||||
if (QSettings(SETTINGS_ORG, SETTINGS_APP).value(DISCONNECTED_KEY, false).toBool())
|
||||
return;
|
||||
|
||||
// In Basecamp the logos_execution_zone module is a single shared instance,
|
||||
// so the wallet may already be open (e.g. opened by the dedicated wallet
|
||||
// app). Adopt that wallet instead of fighting over it: mirror its state
|
||||
// rather than re-opening from disk, which could clobber unsaved in-memory
|
||||
// accounts the other app holds. A freshly-created shared wallet can be open
|
||||
// with zero accounts, so we can't key off list_accounts() alone (see
|
||||
// sharedWalletIsOpen).
|
||||
if (sharedWalletIsOpen()) {
|
||||
const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
|
||||
qDebug() << "AmmUiBackend: adopting already-open shared wallet"
|
||||
<< existing.size() << "accounts";
|
||||
setIsWalletOpen(true);
|
||||
m_accountModel->replaceFromJsonArray(existing);
|
||||
refreshBalances();
|
||||
refreshSequencerAddr();
|
||||
const QString config = defaultConfigPath();
|
||||
const QString storage = defaultStoragePath();
|
||||
const WalletSession session = m_wallet->connect({ config, storage });
|
||||
if (session.failure == WalletFailure::WalletMissing)
|
||||
return;
|
||||
if (!session.ok()) {
|
||||
qWarning() << "AmmUiBackend: wallet connection failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return;
|
||||
}
|
||||
|
||||
// Standalone (own core instance): auto-open a previously-created wallet.
|
||||
// Use persisted paths if the user picked custom ones, else the per-app
|
||||
// default. Only open if the storage actually exists, otherwise stay closed
|
||||
// so QML shows the "Connect" entry point (no noisy FFI errors on first run).
|
||||
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
||||
const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
||||
if (!QFileInfo::exists(stg))
|
||||
return; // No wallet yet — QML shows "Connect".
|
||||
|
||||
qDebug() << "AmmUiBackend: opening wallet with config" << cfg << "storage" << stg;
|
||||
const int err = m_logos->logos_execution_zone.open(cfg, stg);
|
||||
if (err == WALLET_FFI_SUCCESS) {
|
||||
persistConfigPath(cfg);
|
||||
persistStoragePath(stg);
|
||||
setIsWalletOpen(true);
|
||||
refreshAccounts();
|
||||
refreshBlockHeights();
|
||||
refreshSequencerAddr();
|
||||
} else {
|
||||
qWarning() << "AmmUiBackend: wallet open failed, code" << err;
|
||||
}
|
||||
}
|
||||
|
||||
bool AmmUiBackend::sharedWalletIsOpen()
|
||||
{
|
||||
// list_accounts() is non-empty only once the wallet holds accounts, so it
|
||||
// can't distinguish "no wallet open" from "open but empty" (a wallet that
|
||||
// was just created and hasn't had an account added yet). Fall back to a
|
||||
// handle-dependent, account-independent signal: an open wallet always has a
|
||||
// sequencer address (from its config, defaulted on open), while a closed
|
||||
// core returns an empty string. This lets us adopt a freshly-created shared
|
||||
// wallet instead of falling through and re-opening it from disk.
|
||||
if (!QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()).isEmpty())
|
||||
return true;
|
||||
return !m_logos->logos_execution_zone.get_sequencer_addr().isEmpty();
|
||||
persistConfigPath(config);
|
||||
persistStoragePath(storage);
|
||||
setWalletExists(QFileInfo::exists(storage) || session.adopted);
|
||||
setIsWalletOpen(true);
|
||||
applySnapshot(session.snapshot);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createNewDefault(QString password)
|
||||
{
|
||||
QDir().mkpath(defaultWalletHome());
|
||||
return createNew(defaultConfigPath(), defaultStoragePath(), password);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
|
||||
QString AmmUiBackend::createNew(QString configPath,
|
||||
QString storagePath,
|
||||
QString password)
|
||||
{
|
||||
const QString localConfig = toLocalPath(configPath);
|
||||
const QString localStorage = toLocalPath(storagePath);
|
||||
// create_new returns the new wallet's BIP39 mnemonic (empty on failure). We
|
||||
// hand it back to the caller instead of discarding it: wallet creation is
|
||||
// the only moment the seed phrase is recoverable, so the UI must force a
|
||||
// backup step before the user can proceed.
|
||||
const QString mnemonic = m_logos->logos_execution_zone.create_new(localConfig, localStorage, password);
|
||||
if (mnemonic.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend: create_new failed (empty mnemonic)";
|
||||
return QString();
|
||||
const QString config = toLocalPath(configPath);
|
||||
const QString storage = toLocalPath(storagePath);
|
||||
const WalletCreation creation = m_wallet->createWallet(
|
||||
{ config, storage }, password);
|
||||
if (creation.mnemonic.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
|
||||
persistConfigPath(localConfig);
|
||||
persistStoragePath(localStorage);
|
||||
persistConfigPath(config);
|
||||
persistStoragePath(storage);
|
||||
setWalletExists(true);
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "AmmUiBackend: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
setIsWalletOpen(true);
|
||||
refreshAccounts();
|
||||
refreshBlockHeights();
|
||||
refreshSequencerAddr();
|
||||
return mnemonic;
|
||||
applySnapshot(creation.snapshot);
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
bool AmmUiBackend::openExisting()
|
||||
{
|
||||
// Adopt a shared open wallet (Basecamp), else open our own from disk. A
|
||||
// freshly-created shared wallet can be open with zero accounts, so probe
|
||||
// open-ness rather than keying off list_accounts() alone.
|
||||
if (sharedWalletIsOpen()) {
|
||||
const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
|
||||
setIsWalletOpen(true);
|
||||
m_accountModel->replaceFromJsonArray(existing);
|
||||
refreshBalances();
|
||||
refreshSequencerAddr();
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
||||
const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
||||
if (!QFileInfo::exists(stg))
|
||||
return false;
|
||||
|
||||
const int err = m_logos->logos_execution_zone.open(cfg, stg);
|
||||
if (err != WALLET_FFI_SUCCESS) {
|
||||
qWarning() << "AmmUiBackend: openExisting failed, code" << err;
|
||||
const QString config = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
||||
const QString storage = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
||||
const WalletSession session = m_wallet->connect({ config, storage });
|
||||
if (!session.ok()) {
|
||||
qWarning() << "AmmUiBackend: wallet open failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return false;
|
||||
}
|
||||
persistConfigPath(cfg);
|
||||
persistStoragePath(stg);
|
||||
|
||||
persistConfigPath(config);
|
||||
persistStoragePath(storage);
|
||||
setWalletExists(true);
|
||||
setIsWalletOpen(true);
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
||||
refreshAccounts();
|
||||
refreshBlockHeights();
|
||||
refreshSequencerAddr();
|
||||
applySnapshot(session.snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AmmUiBackend::disconnectWallet()
|
||||
{
|
||||
// UI-local lock: persist wallet state, drop our view of it, and remember
|
||||
// the choice. We do NOT close the core module's wallet handle — in Basecamp
|
||||
// that instance is shared with other apps.
|
||||
saveWallet();
|
||||
m_wallet->disconnect();
|
||||
setIsWalletOpen(false);
|
||||
m_accountModel->replaceFromJsonArray(QJsonArray());
|
||||
m_accountModel->replaceAccounts({});
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, true);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createAccountPublic()
|
||||
{
|
||||
const QString result = m_logos->logos_execution_zone.create_account_public();
|
||||
if (!result.isEmpty())
|
||||
refreshAccounts();
|
||||
return result;
|
||||
const WalletAccountCreation creation = m_wallet->createAccount(true);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "AmmUiBackend: public account creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
if (creation.snapshot.ok())
|
||||
applySnapshot(creation.snapshot);
|
||||
else
|
||||
qWarning() << "AmmUiBackend: account refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
return creation.accountId;
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createAccountPrivate()
|
||||
{
|
||||
const QString result = m_logos->logos_execution_zone.create_account_private();
|
||||
if (!result.isEmpty())
|
||||
refreshAccounts();
|
||||
return result;
|
||||
const WalletAccountCreation creation = m_wallet->createAccount(false);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "AmmUiBackend: private account creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
if (creation.snapshot.ok())
|
||||
applySnapshot(creation.snapshot);
|
||||
else
|
||||
qWarning() << "AmmUiBackend: account refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
return creation.accountId;
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshAccounts()
|
||||
{
|
||||
const QJsonArray arr = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
|
||||
m_accountModel->replaceFromJsonArray(arr);
|
||||
refreshBalances();
|
||||
const WalletSnapshot next = m_wallet->snapshot(true);
|
||||
if (next.ok())
|
||||
applySnapshot(next);
|
||||
else
|
||||
qWarning() << "AmmUiBackend: wallet refresh failed"
|
||||
<< walletFailureCode(next.failure);
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshBalances()
|
||||
{
|
||||
refreshBlockHeights();
|
||||
if (currentBlockHeight() > 0)
|
||||
m_logos->logos_execution_zone.sync_to_block(static_cast<quint64>(currentBlockHeight()));
|
||||
|
||||
for (int i = 0; i < m_accountModel->count(); ++i) {
|
||||
const QModelIndex idx = m_accountModel->index(i, 0);
|
||||
const QString addr = m_accountModel->data(idx, AccountModel::AddressRole).toString();
|
||||
const bool isPub = m_accountModel->data(idx, AccountModel::IsPublicRole).toBool();
|
||||
m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub));
|
||||
}
|
||||
saveWallet();
|
||||
refreshAccounts();
|
||||
}
|
||||
|
||||
QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
|
||||
{
|
||||
return m_logos->logos_execution_zone.get_balance(accountIdHex, isPublic);
|
||||
const WalletSnapshot current = m_wallet->snapshot();
|
||||
for (const WalletAccount& account : current.accounts) {
|
||||
if (account.address == accountIdHex && account.isPublic == isPublic)
|
||||
return account.balance;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshBlockHeights()
|
||||
void AmmUiBackend::applySnapshot(const WalletSnapshot& snapshot)
|
||||
{
|
||||
const int lastVal = m_logos->logos_execution_zone.get_last_synced_block();
|
||||
const int currentVal = m_logos->logos_execution_zone.get_current_block_height();
|
||||
if (lastSyncedBlock() != lastVal)
|
||||
setLastSyncedBlock(lastVal);
|
||||
if (currentBlockHeight() != currentVal)
|
||||
setCurrentBlockHeight(currentVal);
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshSequencerAddr()
|
||||
{
|
||||
const QString addr = m_logos->logos_execution_zone.get_sequencer_addr();
|
||||
if (sequencerAddr() != addr)
|
||||
setSequencerAddr(addr);
|
||||
// Probe right away so the banner reflects the (possibly new) endpoint
|
||||
// without waiting for the next periodic tick.
|
||||
m_accountModel->replaceAccounts(snapshot.accounts);
|
||||
setLastSyncedBlock(static_cast<int>(snapshot.lastSyncedBlock));
|
||||
setCurrentBlockHeight(static_cast<int>(snapshot.currentBlockHeight));
|
||||
setSequencerAddr(snapshot.sequencerAddress);
|
||||
checkReachability();
|
||||
}
|
||||
|
||||
void AmmUiBackend::checkReachability()
|
||||
{
|
||||
const QString addr = sequencerAddr();
|
||||
if (addr.isEmpty())
|
||||
if (sequencerAddr().isEmpty())
|
||||
return;
|
||||
|
||||
QNetworkRequest req{QUrl(addr)};
|
||||
req.setTransferTimeout(4000);
|
||||
QNetworkReply* reply = m_net->get(req);
|
||||
QNetworkRequest request{QUrl(sequencerAddr())};
|
||||
request.setTransferTimeout(4000);
|
||||
QNetworkReply* reply = m_net->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
// Any HTTP response (even a 404) means the node is up; only a transport
|
||||
// failure (connection refused, host not found, timeout) counts as down.
|
||||
const bool gotHttpStatus =
|
||||
const bool receivedHttp =
|
||||
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
|
||||
const bool reachable = gotHttpStatus || reply->error() == QNetworkReply::NoError;
|
||||
if (sequencerReachable() != reachable)
|
||||
setSequencerReachable(reachable);
|
||||
setSequencerReachable(receivedHttp || reply->error() == QNetworkReply::NoError);
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void AmmUiBackend::saveWallet()
|
||||
{
|
||||
if (isWalletOpen())
|
||||
m_logos->logos_execution_zone.save();
|
||||
}
|
||||
|
||||
// These only update the in-session PROPs (so subsequent open/refresh calls
|
||||
// reuse the same path). They are no longer written to QSettings: the app
|
||||
// always resolves against the canonical wallet home, so there's nothing to
|
||||
// remember across launches.
|
||||
void AmmUiBackend::persistConfigPath(const QString& path)
|
||||
{
|
||||
setConfigPath(toLocalPath(path));
|
||||
@ -351,51 +246,3 @@ void AmmUiBackend::persistStoragePath(const QString& path)
|
||||
{
|
||||
setStoragePath(toLocalPath(path));
|
||||
}
|
||||
|
||||
bool AmmUiBackend::changeSequencerAddr(QString url)
|
||||
{
|
||||
const QString trimmed = url.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend: refusing to set empty sequencer_addr";
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
||||
|
||||
// Preserve the other config fields (poll timeouts, retries) — only swap the
|
||||
// endpoint. The wallet reads this file on open via from_path_or_initialize_default.
|
||||
QJsonObject obj;
|
||||
QFile in(cfg);
|
||||
if (in.open(QIODevice::ReadOnly)) {
|
||||
obj = QJsonDocument::fromJson(in.readAll()).object();
|
||||
in.close();
|
||||
}
|
||||
obj.insert(QStringLiteral("sequencer_addr"), trimmed);
|
||||
|
||||
QFile out(cfg);
|
||||
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
qWarning() << "AmmUiBackend: cannot write wallet config" << cfg;
|
||||
return false;
|
||||
}
|
||||
out.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
|
||||
out.close();
|
||||
|
||||
// Re-open so the live wallet uses the new endpoint right away.
|
||||
if (isWalletOpen()) {
|
||||
const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
||||
const int err = m_logos->logos_execution_zone.open(cfg, stg);
|
||||
if (err != WALLET_FFI_SUCCESS) {
|
||||
qWarning() << "AmmUiBackend: reopen after sequencer change failed, code" << err;
|
||||
return false;
|
||||
}
|
||||
refreshSequencerAddr();
|
||||
refreshAccounts();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AmmUiBackend::copyToClipboard(QString text)
|
||||
{
|
||||
if (QGuiApplication::clipboard())
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
@ -1,52 +1,41 @@
|
||||
#ifndef AMM_UI_BACKEND_H
|
||||
#define AMM_UI_BACKEND_H
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "rep_AmmUiBackend_source.h"
|
||||
|
||||
#include "AccountModel.h"
|
||||
#include "WalletAccountModel.h"
|
||||
|
||||
class LogosAPI;
|
||||
struct LogosModules;
|
||||
class LogosWalletProvider;
|
||||
class QNetworkAccessManager;
|
||||
class QTimer;
|
||||
|
||||
// Source-side implementation of the AmmUiBackend .rep interface.
|
||||
// Inheriting from AmmUiBackendSimpleSource gives us the generated PROPs and
|
||||
// SLOTs from AmmUiBackend.rep — all the simple ones flow over QtRO. Talks to
|
||||
// the core logos_execution_zone wallet module via LogosModules.
|
||||
class AmmUiBackend : public AmmUiBackendSimpleSource {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(AccountModel* accountModel READ accountModel CONSTANT)
|
||||
Q_PROPERTY(WalletAccountModel* accountModel READ accountModel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit AmmUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||
~AmmUiBackend() override;
|
||||
|
||||
AccountModel* accountModel() const { return m_accountModel; }
|
||||
WalletAccountModel* accountModel() const { return m_accountModel; }
|
||||
|
||||
public slots:
|
||||
// Overrides of the pure-virtual slots generated from the .rep.
|
||||
QString createAccountPublic() override;
|
||||
QString createAccountPrivate() override;
|
||||
void refreshAccounts() override;
|
||||
void refreshBalances() override;
|
||||
QString getBalance(QString accountIdHex, bool isPublic) override;
|
||||
// Return the new wallet's BIP39 mnemonic (empty string on failure) so the
|
||||
// UI can force a one-time seed-phrase backup step.
|
||||
QString createNewDefault(QString password) override;
|
||||
QString createNew(QString configPath, QString storagePath, QString password) override;
|
||||
bool openExisting() override;
|
||||
void disconnectWallet() override;
|
||||
bool changeSequencerAddr(QString url) override;
|
||||
void copyToClipboard(QString text) override;
|
||||
|
||||
private:
|
||||
// Per-app wallet home (kept distinct from the wallet's canonical
|
||||
// ~/.lee/wallet so standalone instances stay isolated; Basecamp sharing
|
||||
// is handled by adopting an already-open shared wallet on startup).
|
||||
static QString defaultWalletHome();
|
||||
QString defaultConfigPath() const;
|
||||
QString defaultStoragePath() const;
|
||||
@ -54,22 +43,12 @@ private:
|
||||
void persistConfigPath(const QString& path);
|
||||
void persistStoragePath(const QString& path);
|
||||
void openOrAdoptWallet();
|
||||
// True when the shared core already has a wallet open — including a freshly
|
||||
// created one with zero accounts. See the definition for why list_accounts()
|
||||
// alone is insufficient.
|
||||
bool sharedWalletIsOpen();
|
||||
void refreshBlockHeights();
|
||||
void refreshSequencerAddr();
|
||||
void saveWallet();
|
||||
|
||||
// Probe the configured sequencer over HTTP and update sequencerReachable.
|
||||
void applySnapshot(const WalletSnapshot& snapshot);
|
||||
void checkReachability();
|
||||
|
||||
AccountModel* m_accountModel;
|
||||
|
||||
WalletAccountModel* m_accountModel;
|
||||
LogosAPI* m_logosAPI;
|
||||
LogosModules* m_logos;
|
||||
|
||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||
QNetworkAccessManager* m_net;
|
||||
QTimer* m_reachabilityTimer;
|
||||
};
|
||||
|
||||
@ -37,10 +37,4 @@ class AmmUiBackend
|
||||
// Basecamp, does not close the wallet other apps share.
|
||||
SLOT(void disconnectWallet())
|
||||
|
||||
// Settings. Rewrites the wallet config's sequencer_addr and re-opens the
|
||||
// wallet so the new network takes effect immediately.
|
||||
SLOT(bool changeSequencerAddr(QString url))
|
||||
|
||||
// Misc
|
||||
SLOT(void copyToClipboard(QString text))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user