feat(amm): complete wallet-backed AMM flow

This commit is contained in:
Ricardo Guilherme Schmidt 2026-06-30 11:42:01 -03:00
parent 35c2dd707e
commit 295c4efece
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
38 changed files with 2852 additions and 322 deletions

View File

@ -1,6 +1,25 @@
cmake_minimum_required(VERSION 3.14)
project(AmmUiPlugin LANGUAGES CXX)
include(GNUInstallDirs)
add_compile_definitions(AMM_UI_ASSET_DIR="${CMAKE_INSTALL_FULL_LIBDIR}")
set(AMM_UI_PROGRAM_DIR "" CACHE PATH "Directory containing amm.bin and token.bin")
if(NOT AMM_UI_PROGRAM_DIR AND DEFINED ENV{AMM_UI_PROGRAM_DIR})
set(AMM_UI_PROGRAM_DIR "$ENV{AMM_UI_PROGRAM_DIR}" CACHE PATH "Directory containing amm.bin and token.bin" FORCE)
endif()
if(AMM_UI_PROGRAM_DIR)
foreach(_program_binary IN ITEMS amm.bin token.bin)
if(NOT EXISTS "${AMM_UI_PROGRAM_DIR}/${_program_binary}")
message(FATAL_ERROR "Required AMM UI program binary not found: ${AMM_UI_PROGRAM_DIR}/${_program_binary}")
endif()
endforeach()
else()
message(WARNING "AMM_UI_PROGRAM_DIR not set; amm.bin and token.bin will not be installed")
endif()
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else()
@ -26,4 +45,17 @@ logos_module(
LINK_LIBRARIES
Qt6::Gui
Qt6::Network
${CMAKE_DL_LIBS}
)
install(DIRECTORY config
DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
if(AMM_UI_PROGRAM_DIR)
install(FILES
${AMM_UI_PROGRAM_DIR}/amm.bin
${AMM_UI_PROGRAM_DIR}/token.bin
DESTINATION ${CMAKE_INSTALL_LIBDIR}/programs
)
endif()

View File

@ -27,6 +27,16 @@ Account/keystore sharing follows the runtime:
startup the backend **adopts** the already-open wallet (see
`openOrAdoptWallet()`), surfacing **shared** accounts across apps.
Deployment config is chain-aware. `config/supported-chains.json` stores each
supported chain identity, including the deterministic block-1 fingerprint
(`genesisBlockHash` + `genesisBlockSignature`). Each `config/*-programs.json`
deployment uses a short `chainRef` plus program-specific IDs and transaction
hashes. When a wallet connects, the backend reads the wallet's current
`sequencer_addr`, probes block 1, selects the matching chain deployment, and
verifies configured deployment transactions. If no matching AMM deployment is
configured or deployed on that chain, the UI shows **Unsupported chain** instead
of submitting transactions.
> Follow-up: the wallet FFI requires explicit `config_path`/`storage_path` even
> though the wallet crate already defines defaults (`~/.lee/wallet`,
> `from_path_or_initialize_default`). A `wallet_ffi_create_new_default()` /

View File

@ -0,0 +1,31 @@
{
"chains": [
{
"chainRef": "local.v0.2.0-rc5",
"programs": [
{
"name": "AMM Program",
"id": "EEkCMCyv7at1incttPxmc6Y1cgwmnzNPKhMwB5ypnKZv",
"imageIdHex": "c4ad5271f1d1a076b6c9bcb5ca4e62812c88728b6c3cd00f6634f7305ca5ddf5",
"deploymentTransaction": "35c8b75d26e2d1e84f81a77f69e1d17fe31b1e57f824a43599fa5f84926385cf",
"tokenProgram": "2NPgqw4JxSrkV4MiMAQ4MgW6vcbxXrWkGoWmMMxSaYRV",
"twapOracleProgram": "AjAJzR2rrEyCvwpemcjC9tbzre38WnXTX14YuT6cWugF",
"abi": "legacy-v0.2.0-rc3",
"configs": [],
"pools": [
{
"name": "AMM197A/AMM197B",
"account": "FPCZi416PrSbWBo8gMAuDqXk3DH8nmXw5PFn3NbCsod6",
"tokenA": "AMM197A",
"tokenB": "AMM197B",
"vaultA": "DmoYscM1fKv5fur9kw6wtmVGkxLvjsBvD7L3Tdjiaxoe",
"vaultB": "EkVPMUmGQbfo7zZ7dWnwnjMZCQqySEZ16vjN99PEC3dN",
"lpDefinitionAccount": "2P5kBN1SfVcjJYCGFggzCDUHKZvVaMsbM9tFG4xj4NVy",
"creationTransaction": "4ff22fcc9b70ae0b748b1cb186b547a1b35f76fd55eb96ff21a780b660322ac6"
}
]
}
]
}
]
}

View File

@ -0,0 +1,16 @@
{
"chains": [
{
"chainRef": "local.v0.2.0-rc5",
"programs": [
{
"name": "Associated Token Account Program",
"id": "D7TdWT1wVpDiH9wfLBdLfE2R1Kgy8FNW9PWP62uRqu9x",
"imageIdHex": "b3f3d685c4bc2ed261503262fd650be78d104c1179ce30ef35738b36d7e11d97",
"deploymentTransaction": "988f6f08ac72df65c479e06d475ec08d0dc42c4dc060c4b179f2754094821449",
"tokenProgram": "2NPgqw4JxSrkV4MiMAQ4MgW6vcbxXrWkGoWmMMxSaYRV"
}
]
}
]
}

View File

@ -0,0 +1,17 @@
{
"chains": [
{
"chainRef": "local.v0.2.0-rc5",
"programs": [
{
"name": "Stablecoin Program",
"id": "2hMHPg6CRwyNqcou3sP6RTnNpJXnVPBWE8Za8aUFAMQu",
"imageIdHex": "1931da57b56bc15a1c26a7965e1632f59dd2895affbf1258ce49e0268265a322",
"deploymentTransaction": "85ee89661f478cdad87b4516b381b800f9185b55276c21d37666b30682e171be",
"tokenProgram": "2NPgqw4JxSrkV4MiMAQ4MgW6vcbxXrWkGoWmMMxSaYRV",
"twapOracleProgram": "AjAJzR2rrEyCvwpemcjC9tbzre38WnXTX14YuT6cWugF"
}
]
}
]
}

View File

@ -0,0 +1,14 @@
{
"chains": [
{
"alias": "local.v0.2.0-rc5",
"name": "LEZ local",
"network": "http://127.0.0.1:3040",
"chainId": "lez:block-1:e437c44cac2511b603b96195e6b490217da6ba47a326a1fa6c939167ec3e8656:032f82b1ba318b0028f853d25bbc2da49ace15c100b7337fc06eddcf4bfa777906409810c8e09e57831000462a857c26631bb1b8087391e34a00ab3d2ebf8739",
"genesisBlockId": 1,
"genesisBlockHash": "e437c44cac2511b603b96195e6b490217da6ba47a326a1fa6c939167ec3e8656",
"genesisBlockSignature": "032f82b1ba318b0028f853d25bbc2da49ace15c100b7337fc06eddcf4bfa777906409810c8e09e57831000462a857c26631bb1b8087391e34a00ab3d2ebf8739",
"default": true
}
]
}

View File

@ -0,0 +1,35 @@
{
"chains": [
{
"chainRef": "local.v0.2.0-rc5",
"programs": [
{
"name": "Token Program",
"id": "2NPgqw4JxSrkV4MiMAQ4MgW6vcbxXrWkGoWmMMxSaYRV",
"imageIdHex": "14568940ba11698d23703dff8d9df6703c32e2412f89ff32efc46a5cad70f500",
"deploymentTransaction": "27401f74353c9863fecb27889d2e0fdaccb7e0a652a284929c67bf0fdc6c5470",
"definitions": [
{
"symbol": "AMM197A",
"name": "AMM197A",
"color": "#2775ca",
"letter": "A",
"usdPrice": 1,
"definitionAccount": "HjkLvjKqUt7PhUCs6CUEbqmrjAAbUJmeQg6TGkTAzLHS",
"mintTransaction": "ec430fa48a6990dd31fa3f075eb16f842392bf62d0d6701b46574a3fd4549187"
},
{
"symbol": "AMM197B",
"name": "AMM197B",
"color": "#26a17b",
"letter": "B",
"usdPrice": 1,
"definitionAccount": "2b4UuHsax4pELYzQBjQe5DieHWN3zn9BrrtTGhpbYX71",
"mintTransaction": "2edf40aefe53a8dcb2e3df8e22426841c4c9503d42179665cb8ab94434365e2a"
}
]
}
]
}
]
}

View File

@ -0,0 +1,15 @@
{
"chains": [
{
"chainRef": "local.v0.2.0-rc5",
"programs": [
{
"name": "TWAP Oracle Program",
"id": "AjAJzR2rrEyCvwpemcjC9tbzre38WnXTX14YuT6cWugF",
"imageIdHex": "90861a75ed743edaf5d2ab7dcc7563157435072b6c10f3db5cf50aa93322018c",
"deploymentTransaction": "54bc196c66beefe4b8ea5ab8564743332cc6b690c0b1908c55aa1407347af488"
}
]
}
]
}

10
apps/amm/flake.lock generated
View File

@ -26763,7 +26763,15 @@
"root": {
"inputs": {
"logos-module-builder": "logos-module-builder",
"logos_execution_zone": "logos_execution_zone"
"logos-standalone-app": [
"logos-module-builder",
"logos-standalone-app"
],
"logos_execution_zone": "logos_execution_zone",
"nixpkgs": [
"logos-module-builder",
"nixpkgs"
]
}
},
"rust-overlay": {

View File

@ -3,18 +3,79 @@
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
nixpkgs.follows = "logos-module-builder/nixpkgs";
logos-standalone-app.follows = "logos-module-builder/logos-standalone-app";
# 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
# it as a module dependency. This rev pins LEZ (lssa) at d2e9400, 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";
};
outputs = inputs@{ logos-module-builder, ... }:
outputs = inputs@{ logos-module-builder, logos-standalone-app, nixpkgs, ... }:
let
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; }));
fixedStandalonePackages = forAllSystems (pkgs:
let
base = logos-standalone-app.packages.${pkgs.system};
qtLibPath = pkgs.lib.makeLibraryPath ([
pkgs.qt6.qtbase
pkgs.qt6.qtremoteobjects
pkgs.qt6.qtdeclarative
pkgs.qt6.qtwebview
pkgs.zstd
pkgs.krb5
pkgs.zlib
pkgs.glib
pkgs.stdenv.cc.cc
pkgs.freetype
pkgs.fontconfig
pkgs.boost
pkgs.openssl
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
pkgs.libglvnd
pkgs.mesa
pkgs.xorg.libX11
pkgs.xorg.libXext
pkgs.xorg.libXrender
pkgs.xorg.libXrandr
pkgs.xorg.libXcursor
pkgs.xorg.libXi
pkgs.xorg.libXfixes
pkgs.xorg.libxcb
]);
fixedApp = base.default.overrideAttrs (old: {
inherit qtLibPath;
qtWrapperArgs = (old.qtWrapperArgs or [ ]) ++ [
"--prefix" "LD_LIBRARY_PATH" ":" qtLibPath
"--prefix" "QT_PLUGIN_PATH" ":" old.qtPluginPath
"--prefix" "QML_IMPORT_PATH" ":" old.qmlImportPath
"--prefix" "QML2_IMPORT_PATH" ":" old.qmlImportPath
];
});
in base // {
app = fixedApp;
default = fixedApp;
});
fixedStandalone = logos-standalone-app // {
packages = fixedStandalonePackages;
};
in
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
logosStandalone = fixedStandalone;
preConfigure = ''
export AMM_UI_PROGRAM_DIR=${inputs.logos_execution_zone.inputs."logos-execution-zone"}/artifacts/program_methods
'';
postInstall = ''
mkdir -p "$out/lib/config"
cp -r ${./config}/. "$out/lib/config/"
'';
};
}

View File

@ -14,6 +14,18 @@ Item {
readonly property var accountModel: logos.model("amm_ui", "accountModel")
property bool ready: false
readonly property bool deploymentNetworkMatched: !root.ready || !root.backend || root.backend.deploymentNetworkMatched
readonly property bool deploymentIdentityPending: root.ready
&& root.backend
&& root.backend.isWalletOpen
&& root.backend.deploymentIdentityPending
readonly property bool unsupportedChain: root.ready
&& root.backend
&& root.backend.isWalletOpen
&& !root.backend.deploymentIdentityPending
&& !root.backend.deploymentNetworkMatched
readonly property var deploymentTokens: root.deploymentNetworkMatched && root.backend ? root.backend.deploymentTokens : []
readonly property var deploymentPoolConfig: root.deploymentNetworkMatched && root.backend ? root.backend.deploymentPool : ({})
Connections {
target: logos
@ -36,20 +48,25 @@ Item {
anchors.right: parent.right
z: 101
readonly property bool show: root.ready
&& root.backend
&& root.backend.isWalletOpen
&& root.backend.sequencerAddr.length > 0
&& !root.backend.sequencerReachable
readonly property bool show: root.deploymentIdentityPending
|| root.unsupportedChain
|| (root.ready
&& root.backend
&& root.backend.isWalletOpen
&& root.backend.sequencerAddr.length > 0
&& !root.backend.sequencerReachable)
height: show ? 32 : 0
visible: height > 0
clip: true
color: Theme.palette.warning
Accessible.role: Accessible.AlertMessage
Accessible.name: bannerText.text
Behavior on height { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Text {
id: bannerText
anchors.centerIn: parent
width: parent.width - 40
horizontalAlignment: Text.AlignHCenter
@ -57,7 +74,11 @@ Item {
font.pixelSize: 12
font.weight: Font.Medium
color: Theme.palette.background
text: qsTr("Unable to connect to network")
text: root.deploymentIdentityPending
? qsTr("Checking chain")
: root.unsupportedChain
? qsTr("Unsupported chain")
: qsTr("Unable to connect to network")
}
}
@ -82,11 +103,20 @@ Item {
SwapPage {
anchors.fill: parent
backend: root.ready ? root.backend : null
tokens: root.deploymentTokens
poolConfig: root.deploymentPoolConfig
unsupportedChain: root.unsupportedChain
selectedWalletAccount: navbar.selectedAddress
visible: navbar.currentIndex === 0
}
LiquidityPage {
anchors.fill: parent
backend: root.ready ? root.backend : null
poolConfig: root.deploymentPoolConfig
unsupportedChain: root.unsupportedChain
selectedWalletAccount: navbar.selectedAddress
visible: navbar.currentIndex === 1
}
}

View File

@ -24,6 +24,11 @@ Item {
implicitHeight: 56
function selectTab(index) {
root.currentIndex = index
root.tabChanged(index)
}
Rectangle {
anchors.fill: parent
color: Theme.palette.background
@ -63,12 +68,20 @@ Item {
delegate: Rectangle {
readonly property bool active: root.currentIndex === index
activeFocusOnTab: true
height: 36
width: tabLabel.implicitWidth + 28
radius: 18
color: active ? Theme.palette.backgroundSecondary : "transparent"
border.width: activeFocus ? 1 : 0
border.color: Theme.palette.overlayOrange
Accessible.role: Accessible.PageTab
Accessible.name: modelData
Accessible.checked: active
Behavior on color { ColorAnimation { duration: 150 } }
Keys.onReturnPressed: root.selectTab(index)
Keys.onSpacePressed: root.selectTab(index)
Text {
id: tabLabel
@ -84,10 +97,7 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentIndex = index
root.tabChanged(index)
}
onClicked: root.selectTab(index)
}
}
}

View File

@ -7,7 +7,7 @@ import "../../state"
Rectangle {
id: root
required property DummyPoolState poolState
required property PoolState poolState
property real slippageTolerancePercent: 0.5
property string amountA: ""
@ -24,7 +24,7 @@ Rectangle {
readonly property bool zeroTokenDeposit: root.hasAnyAmount && (root.preview.actualA === 0 || root.preview.actualB === 0)
readonly property bool zeroLpDeposit: root.preview.actualA > 0 && root.preview.actualB > 0 && root.preview.deltaLp === 0
readonly property bool canSubmit: root.hasAnyAmount && !root.amountAOverBalance && !root.amountBOverBalance && !root.minReceivedIsZero && !root.zeroTokenDeposit && !root.zeroLpDeposit
readonly property string submitButtonText: !root.hasAnyAmount ? qsTr("Enter an amount") : root.amountAOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenA) : root.amountBOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenB) : root.zeroTokenDeposit ? qsTr("Amount rounds to zero") : root.zeroLpDeposit ? qsTr("LP output is 0") : root.minReceivedIsZero ? qsTr("Minimum received is 0") : qsTr("Add Liquidity")
readonly property string submitButtonText: !root.hasAnyAmount ? qsTr("Enter an amount") : root.amountAOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenA) : root.amountBOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenB) : root.zeroTokenDeposit ? qsTr("Amount rounds to zero") : root.zeroLpDeposit ? qsTr("LP output is 0") : root.minReceivedIsZero ? qsTr("Minimum received is 0") : qsTr("Add liquidity")
readonly property string warningText: root.zeroTokenDeposit ? qsTr("Deposit would be rejected because one token amount rounds to zero") : root.zeroLpDeposit ? qsTr("Deposit would mint 0 LP tokens") : ""
signal slippageToleranceChangeRequested(real tolerancePercent)
@ -207,13 +207,16 @@ Rectangle {
return {
"action": "add",
"actualA": root.preview.actualA,
"actualAValue": Math.floor(root.preview.actualA),
"actualB": root.preview.actualB,
"actualBValue": Math.floor(root.preview.actualB),
"currentRatio": qsTr("1 %1 = %2 %3").arg(root.poolState.tokenB).arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA),
"deltaLp": root.preview.deltaLp,
"depositA": root.poolState.formatTokenAmount(root.preview.actualA, root.poolState.tokenA),
"depositB": root.poolState.formatTokenAmount(root.preview.actualB, root.poolState.tokenB),
"feeTier": root.poolState.feeTier,
"minLpReceived": root.poolState.formatLpAmount(root.minLpReceived),
"minLpReceivedAmount": root.minLpReceived,
"slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
"tokenA": root.poolState.tokenA,
"tokenB": root.poolState.tokenB

View File

@ -206,7 +206,7 @@ FocusScope {
activeFocusOnTab: true
focusPolicy: Qt.StrongFocus
hoverEnabled: true
text: qsTr("Confirm")
text: qsTr("Submit")
Accessible.name: confirmButton.text

View File

@ -5,8 +5,8 @@ import "../../state"
Rectangle {
id: root
required property DummyPoolState poolState
readonly property string estimateHelp: qsTr("This value is an estimate from the current dummy reserves and your share of total LP supply.")
required property PoolState poolState
readonly property string estimateHelp: qsTr("This value is estimated from the current testnet reserves and your share of total LP supply.")
color: "#151515"
implicitHeight: content.implicitHeight + 20

View File

@ -7,7 +7,7 @@ import "../../state"
Rectangle {
id: root
required property DummyPoolState poolState
required property PoolState poolState
property real slippageTolerancePercent: 0.5
property int burnAmount: 0
@ -23,7 +23,7 @@ Rectangle {
readonly property bool minReceivedIsZero: root.burnAmount > 0 && (root.minTokenAReceived === 0 || root.minTokenBReceived === 0)
readonly property bool canSubmit: root.hasLpTokens && root.burnAmount > 0 && !root.minReceivedIsZero
readonly property string estimateHelp: qsTr("Estimated with the same integer floor math used by the remove-liquidity contract path.")
readonly property string submitButtonText: !root.hasLpTokens ? qsTr("No LP balance") : root.burnAmount === 0 ? qsTr("Enter an amount") : root.minReceivedIsZero ? qsTr("Minimum received is 0") : qsTr("Remove Liquidity")
readonly property string submitButtonText: !root.hasLpTokens ? qsTr("No LP balance") : root.burnAmount === 0 ? qsTr("Enter an amount") : root.minReceivedIsZero ? qsTr("Minimum received is 0") : qsTr("Remove liquidity")
signal slippageToleranceChangeRequested(real tolerancePercent)
signal removeLiquidityRequested(var snapshot)
@ -478,7 +478,9 @@ Rectangle {
"burnPercent": root.poolState.formatPercent(root.removePercent),
"burnText": root.poolState.formatLpAmount(root.preview.burnedLp),
"minTokenAReceived": root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA),
"minTokenAReceivedAmount": root.minTokenAReceived,
"minTokenBReceived": root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB),
"minTokenBReceivedAmount": root.minTokenBReceived,
"postRemovalShare": root.poolState.formatPoolShare(root.preview.newUserShare),
"slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
"tokenA": root.poolState.tokenA,

View File

@ -69,13 +69,13 @@ Rectangle {
color: "#E7E1D8"
font.bold: true
font.pixelSize: 18
inputMethodHints: Qt.ImhFormattedNumbersOnly
inputMethodHints: Qt.ImhDigitsOnly
placeholderText: qsTr("0")
selectByMouse: true
selectedTextColor: "#151515"
selectionColor: "#F26A21"
validator: RegularExpressionValidator {
regularExpression: /[0-9]*([.][0-9]*)?/
regularExpression: /[0-9]*/
}
Accessible.name: root.label

View File

@ -1,4 +1,5 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Item {
@ -6,8 +7,11 @@ Item {
property string message: ""
property string detail: ""
property string transactionId: ""
property string status: "success"
property bool copied: false
property bool open: false
property int duration: 3600
property int duration: 7000
height: implicitHeight
implicitHeight: toast.implicitHeight
@ -15,9 +19,28 @@ Item {
visible: root.open || fadeOut.running
z: 30
function show(nextMessage, nextDetail) {
TextEdit { id: clipboardProxy; visible: false }
function shortTransactionId(value) {
return value.length > 18 ? value.substring(0, 10) + "..." + value.slice(-8) : value;
}
function copyTransactionId() {
if (root.transactionId.length === 0) return;
clipboardProxy.text = root.transactionId;
clipboardProxy.selectAll();
clipboardProxy.copy();
clipboardProxy.deselect();
clipboardProxy.text = "";
root.copied = true;
}
function show(nextMessage, nextDetail, nextTransactionId, nextStatus) {
root.message = nextMessage;
root.detail = nextDetail || "";
root.transactionId = nextTransactionId || "";
root.status = nextStatus || "success";
root.copied = false;
root.open = true;
dismissTimer.restart();
}
@ -28,7 +51,12 @@ Item {
interval: root.duration
repeat: false
onTriggered: root.open = false
onTriggered: {
if (copyButton.activeFocus || hoverGuard.containsMouse)
restart()
else
root.open = false
}
}
Behavior on opacity {
@ -47,8 +75,19 @@ Item {
color: "#20201F"
implicitHeight: Math.max(50, toastContent.implicitHeight + 18)
radius: 8
border.color: "#4D3A2E"
border.color: root.status === "error" ? "#6A2E2E" : "#4D3A2E"
border.width: 1
Accessible.role: Accessible.AlertMessage
Accessible.name: root.detail.length > 0
? qsTr("%1. %2").arg(root.message).arg(root.detail)
: root.message
MouseArea {
id: hoverGuard
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
}
RowLayout {
id: toastContent
@ -62,7 +101,7 @@ Item {
}
Rectangle {
color: "#78C88D"
color: root.status === "error" ? "#D75C5C" : "#78C88D"
radius: 6
Layout.alignment: Qt.AlignTop
@ -90,14 +129,59 @@ Item {
Text {
color: "#B8ADA3"
elide: Text.ElideRight
font.pixelSize: 12
maximumLineCount: 3
text: root.detail
visible: root.detail.length > 0
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Text {
color: "#F2D8C7"
elide: Text.ElideMiddle
font.pixelSize: 12
text: qsTr("Tx %1").arg(root.shortTransactionId(root.transactionId))
visible: root.transactionId.length > 0
Layout.fillWidth: true
}
}
Button {
id: copyButton
Accessible.name: root.copied ? qsTr("Transaction id copied") : qsTr("Copy transaction id")
Accessible.role: Accessible.Button
activeFocusOnTab: true
focusPolicy: Qt.StrongFocus
text: root.copied ? qsTr("Copied") : qsTr("Copy tx")
visible: root.transactionId.length > 0
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 30
Layout.preferredWidth: copyText.implicitWidth + 18
contentItem: Text {
id: copyText
color: "#E7E1D8"
font.bold: true
font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter
text: copyButton.text
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
border.color: copyButton.activeFocus ? "#F2D8C7" : "transparent"
color: copyButton.hovered ? "#F26A21" : "#2B2724"
radius: 6
}
onClicked: root.copyTransactionId()
}
}
}
}

View File

@ -15,10 +15,11 @@ Rectangle {
property string buyInput: ""
property string editingSide: "sell"
property real slippageTolerancePercent: 0.5
property int feeBps: 30
DummySwapState {
SwapState {
id: swapState
feeBps: 30
feeBps: root.feeBps
}
signal requestTokenSelect(string side)
@ -77,9 +78,11 @@ Rectangle {
}
function formatAmountValue(val) {
if (val >= 1) return val.toFixed(2)
if (val >= 0.0001) return val.toFixed(6)
return val.toFixed(8)
return Math.floor(val).toString()
}
function formatMaxSentValue(val) {
return Math.ceil(val).toString()
}
readonly property string sellDisplay: editingSide === "sell"
@ -107,8 +110,12 @@ Rectangle {
"sellToken": sellToken ? sellToken.symbol : "",
"buyToken": buyToken ? buyToken.symbol : "",
"sellAmount": formatAmountValue(parsedSellAmount),
"sellAmountValue": formatAmountValue(parsedSellAmount),
"buyAmount": formatAmountValue(parsedBuyAmount),
"buyAmountValue": formatAmountValue(parsedBuyAmount),
"minReceived": formatAmountValue(minReceivedAmount),
"minReceivedAmountValue": formatAmountValue(minReceivedAmount),
"maxSentAmountValue": formatMaxSentValue(swapState.maxSent(parsedSellAmount, slippageTolerancePercent)),
"feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""),
"priceImpactPercent": swapState.formatPercent(priceImpactPercent),
"priceImpactPercentValue": priceImpactPercent,

View File

@ -195,7 +195,7 @@ FocusScope {
activeFocusOnTab: true
focusPolicy: Qt.StrongFocus
hoverEnabled: true
text: qsTr("Confirm Swap")
text: qsTr("Submit")
Layout.fillWidth: true
Layout.minimumHeight: 48
onClicked: root.confirm()

View File

@ -46,7 +46,7 @@ Rectangle {
Item {
Layout.fillWidth: true
height: 44
Layout.preferredHeight: 44
TextInput {
id: tiInput
@ -58,7 +58,7 @@ Rectangle {
clip: true
onTextEdited: root.inputEdited(text)
validator: RegularExpressionValidator {
regularExpression: /^[0-9]*\.?[0-9]*$/
regularExpression: /^[0-9]*$/
}
}
@ -81,10 +81,10 @@ Rectangle {
}
Rectangle {
height: 40
Layout.preferredHeight: 40
radius: 20
color: tokenBtnHover.containsMouse ? theme.colors.panelHoverBg : theme.colors.panelBg
implicitWidth: tokenBtnRow.implicitWidth + 24
Layout.preferredWidth: tokenBtnRow.implicitWidth + 24
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
@ -93,7 +93,9 @@ Rectangle {
spacing: 6
Rectangle {
width: 24; height: 24; radius: 12
Layout.preferredWidth: 24
Layout.preferredHeight: 24
radius: 12
color: root.token ? root.token.color : theme.colors.noTokenCircle
visible: root.token !== null
Text {

View File

@ -1,7 +1,7 @@
import QtQuick
import QtQml
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick 2.15
import QtQml 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Logos.Theme
import Logos.Controls
@ -12,8 +12,8 @@ import Logos.Controls
// 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.
// selectedAddress stays in the wallet module's raw hex format for backend
// calls; selectedDisplayAddress is the base58 format shown/copied in the UI.
Item {
id: root
@ -22,11 +22,13 @@ Item {
property var accountModel: null
readonly property bool connected: backend !== null && backend.isWalletOpen
readonly property real viewportMargin: Theme.spacing.medium
// 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
property string selectedAccountId: ""
// Non-visual mirror of the account model: realizes every row regardless of
// popup visibility, so the active account is addressable by index at all
@ -36,10 +38,13 @@ Item {
model: root.accountModel
delegate: QtObject {
readonly property string address: model.address ?? ""
readonly property string displayAddress: model.displayAddress ?? ""
readonly property string name: model.name ?? ""
readonly property string balance: model.balance ?? ""
readonly property bool isPublic: model.isPublic ?? false
}
onObjectAdded: root.clampSelection()
onObjectRemoved: root.clampSelection()
}
function entryAt(i) {
@ -48,26 +53,75 @@ Item {
readonly property string selectedAddress: {
const e = root.entryAt(root.selectedIndex)
return e ? e.address : ""
return e && e.isPublic ? e.address : ""
}
readonly property string selectedDisplayAddress: {
const e = root.entryAt(root.selectedIndex)
return e && e.isPublic ? e.displayAddress : ""
}
readonly property string selectedName: {
const e = root.entryAt(root.selectedIndex)
return e ? e.name : ""
return e && e.isPublic ? e.name : qsTr("No public account")
}
readonly property string selectedBalance: {
const e = root.entryAt(root.selectedIndex)
return e ? e.balance : ""
return e && e.isPublic ? 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 firstPublicIndex() {
for (let i = 0; i < accounts.count; ++i) {
const e = root.entryAt(i)
if (e && e.isPublic)
return i
}
return -1
}
function publicIndexForAddress(address) {
if (!address)
return -1
for (let i = 0; i < accounts.count; ++i) {
const e = root.entryAt(i)
if (e && e.isPublic && e.address === address)
return i
}
return -1
}
function selectIndex(index) {
const e = root.entryAt(index)
root.selectedIndex = index
root.selectedAccountId = e.address
}
// Keep the selection within bounds as accounts are added/removed.
function clampSelection() {
if (accounts.count === 0) { root.selectedIndex = 0; return }
if (accounts.count === 0) {
root.selectedIndex = 0
root.selectedAccountId = ""
return
}
const rememberedIndex = root.publicIndexForAddress(root.selectedAccountId)
if (rememberedIndex >= 0) {
root.selectedIndex = rememberedIndex
return
}
if (root.selectedIndex < 0) root.selectedIndex = 0
else if (root.selectedIndex >= accounts.count) root.selectedIndex = accounts.count - 1
const selected = root.entryAt(root.selectedIndex)
if (selected.isPublic) {
root.selectedAccountId = selected.address
return
}
const firstPublic = root.firstPublicIndex()
if (firstPublic >= 0) {
root.selectIndex(firstPublic)
} else {
root.selectedAccountId = ""
}
}
Connections {
target: root.accountModel
@ -97,9 +151,25 @@ Item {
clipboardProxy.text = ""
}
function handleOpenExistingFailure(error) {
connectErrorDialog.message = error || qsTr("Could not open the existing wallet at %1.")
.arg(root.backend ? root.backend.walletHome : "")
if (root.backend && root.backend.walletExists)
connectErrorDialog.open()
else
createWalletDialog.open()
}
implicitWidth: root.connected ? connectedButton.width : connectButton.width
implicitHeight: 40
Component.onCompleted: root.clampSelection()
function clampedOverlayWidth(maxWidth) {
const overlay = Overlay.overlay
return Math.min(maxWidth, Math.max(0, overlay ? overlay.width - root.viewportMargin * 2 : maxWidth))
}
// Disconnected: Connect
LogosButton {
id: connectButton
@ -110,36 +180,37 @@ Item {
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()
if (!root.backend) return
logos.watch(root.backend.openExisting(),
function(ok) {
if (!ok) root.handleOpenExistingFailure("")
},
function(error) {
console.warn("openExisting error:", error)
root.handleOpenExistingFailure(qsTr("Could not open the existing wallet: %1").arg(error))
})
}
}
// Connected: address pill that toggles the wallet menu
Rectangle {
Button {
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"
implicitWidth: connectedRow.implicitWidth + leftPadding + rightPadding
leftPadding: Theme.spacing.medium
rightPadding: Theme.spacing.medium
topPadding: 0
bottomPadding: 0
text: root.selectedDisplayAddress.length > 0
? root.truncated(root.selectedDisplayAddress)
: qsTr("No public account")
Accessible.name: text
RowLayout {
contentItem: RowLayout {
id: connectedRow
anchors.centerIn: parent
spacing: Theme.spacing.small
Rectangle {
@ -149,7 +220,7 @@ Item {
color: "#39c06a"
}
LogosText {
text: root.truncated(root.selectedAddress) || qsTr("Connected")
text: connectedButton.text
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.text
}
@ -160,19 +231,26 @@ Item {
}
}
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()
}
background: Rectangle {
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 || connectedButton.activeFocus ? Theme.palette.overlayOrange : "transparent"
}
// 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()
}
}
@ -181,8 +259,18 @@ Item {
id: walletMenu
parent: connectedButton
y: connectedButton.height + Theme.spacing.small
x: connectedButton.width - width // right-align under the button
width: 360
x: {
const overlay = Overlay.overlay
if (!overlay)
return connectedButton.width - width
const buttonLeft = connectedButton.mapToItem(overlay, 0, 0).x
const buttonRight = buttonLeft + connectedButton.width
const desiredOverlayX = buttonRight - width
const minX = root.viewportMargin
const maxX = Math.max(minX, overlay.width - width - root.viewportMargin)
return Math.max(minX, Math.min(desiredOverlayX, maxX)) - buttonLeft
}
width: root.clampedOverlayWidth(360)
padding: Theme.spacing.medium
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
@ -239,14 +327,17 @@ Item {
WalletIconButton {
iconSource: Qt.resolvedUrl("icons/account.svg")
accessibleName: qsTr("Show accounts")
onClicked: viewStack.push(accountsView)
}
WalletIconButton {
iconSource: Qt.resolvedUrl("icons/settings.svg")
accessibleName: qsTr("Open wallet settings")
onClicked: viewStack.push(settingsView)
}
WalletIconButton {
iconSource: Qt.resolvedUrl("icons/power.svg")
accessibleName: qsTr("Disconnect wallet")
onClicked: {
walletMenu.close()
if (root.backend) root.backend.disconnectWallet()
@ -279,12 +370,13 @@ Item {
Rectangle {
Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
Layout.preferredHeight: tagLabel.implicitHeight + 4
visible: root.selectedAddress.length > 0
radius: 4
color: Theme.palette.backgroundSecondary
LogosText {
id: tagLabel
anchors.centerIn: parent
text: root.selectedIsPublic ? qsTr("Public") : qsTr("Private")
text: qsTr("Public")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
@ -302,7 +394,7 @@ Item {
LogosText {
Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter
text: root.selectedAddress
text: root.selectedDisplayAddress
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textMuted
elide: Text.ElideMiddle
@ -310,8 +402,9 @@ Item {
LogosCopyButton {
Layout.preferredHeight: 40
Layout.preferredWidth: 40
visible: root.selectedAddress.length > 0
onCopyText: root.copyToClipboard(root.selectedAddress)
accessibleName: qsTr("Copy selected account address")
visible: root.selectedDisplayAddress.length > 0
onCopyText: root.copyToClipboard(root.selectedDisplayAddress)
icon.color: Theme.palette.textMuted
}
}
@ -334,6 +427,7 @@ Item {
WalletIconButton {
iconSource: Qt.resolvedUrl("icons/back.svg")
accessibleName: qsTr("Back")
onClicked: viewStack.pop()
}
LogosText {
@ -356,9 +450,12 @@ Item {
delegate: AccountDelegate {
width: ListView.view.width
highlighted: index === root.selectedIndex
selectable: model.isPublic ?? false
highlighted: selectable && index === root.selectedIndex
onClicked: {
root.selectedIndex = index
if (!selectable)
return
root.selectIndex(index)
viewStack.pop()
}
onCopyRequested: (text) => root.copyToClipboard(text)
@ -367,7 +464,7 @@ Item {
LogosButton {
Layout.fillWidth: true
height: 40
Layout.preferredHeight: 40
text: qsTr("Add")
// Leave the wallet menu open behind the (modal) dialog.
onClicked: createAccountDialog.open()
@ -389,6 +486,7 @@ Item {
WalletIconButton {
iconSource: Qt.resolvedUrl("icons/back.svg")
accessibleName: qsTr("Back")
onClicked: viewStack.pop()
}
LogosText {
@ -422,11 +520,13 @@ Item {
font.pixelSize: Theme.typography.secondaryText
property bool ok: false
color: ok ? Theme.palette.success : Theme.palette.error
Accessible.role: Accessible.AlertMessage
Accessible.name: text
}
LogosButton {
Layout.fillWidth: true
height: 40
Layout.preferredHeight: 40
text: qsTr("Save")
onClicked: {
if (!root.backend) return
@ -454,16 +554,95 @@ Item {
onCreateWallet: function(password) {
if (!root.backend) return
logos.watch(root.backend.createNewDefault(password),
function(ok) {
if (ok) createWalletDialog.close()
else createWalletDialog.createError = qsTr("Failed to create wallet. Please try again.")
function(result) {
const mnemonic = String(result || "")
if (mnemonic.length > 0) {
createWalletDialog.close()
backupWalletDialog.mnemonic = mnemonic
backupWalletDialog.open()
} else {
createWalletDialog.createError = qsTr("Failed to create wallet. Please try again.")
}
},
function(error) {
createWalletDialog.createError = qsTr("Error creating wallet: %1").arg(error)
})
})
}
}
Popup {
id: connectErrorDialog
property string message: ""
modal: true
dim: true
padding: Theme.spacing.large
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
parent: Overlay.overlay
anchors.centerIn: parent
width: root.clampedOverlayWidth(380)
background: Rectangle {
color: Theme.palette.backgroundSecondary
radius: Theme.spacing.radiusXlarge
border.color: Theme.palette.backgroundElevated
}
contentItem: ColumnLayout {
width: connectErrorDialog.availableWidth
spacing: Theme.spacing.large
LogosText {
Layout.fillWidth: true
text: qsTr("Wallet connection failed")
font.pixelSize: Theme.typography.titleText
font.weight: Theme.typography.weightBold
color: Theme.palette.text
}
LogosText {
Layout.fillWidth: true
text: connectErrorDialog.message
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
LogosText {
Layout.fillWidth: true
text: qsTr("Creating a new wallet will use the same wallet home. Continue only if you do not need the existing wallet files.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.error
wrapMode: Text.WordWrap
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.medium
LogosButton {
Layout.fillWidth: true
text: qsTr("Cancel")
onClicked: connectErrorDialog.close()
}
LogosButton {
Layout.fillWidth: true
text: qsTr("Create New")
onClicked: {
connectErrorDialog.close()
createWalletDialog.open()
}
}
}
}
}
BackupWalletDialog {
id: backupWalletDialog
}
CreateAccountDialog {
id: createAccountDialog
onCreatePublicRequested: {

View File

@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Logos.Theme
import Logos.Controls
@ -13,6 +13,18 @@ ItemDelegate {
// to its QML-side clipboard helper (AccountControl.copyToClipboard).
signal copyRequested(string text)
property bool selectable: model.isPublic ?? false
readonly property string displayAddress: model.displayAddress ?? ""
focusPolicy: root.selectable ? Qt.StrongFocus : Qt.NoFocus
activeFocusOnTab: root.selectable
Accessible.role: root.selectable ? Accessible.RadioButton : Accessible.ListItem
Accessible.name: root.selectable
? qsTr("Select account %1").arg(root.displayAddress)
: qsTr("Private account %1").arg(root.displayAddress)
Accessible.description: root.selectable ? "" : qsTr("Private accounts cannot be used for AMM actions")
Accessible.checked: root.highlighted
leftPadding: Theme.spacing.medium
rightPadding: Theme.spacing.medium
topPadding: Theme.spacing.medium
@ -67,7 +79,7 @@ ItemDelegate {
id: addressLabel
Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter
text: model.address ?? ""
text: root.displayAddress
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textMuted
elide: Text.ElideMiddle
@ -75,8 +87,9 @@ ItemDelegate {
LogosCopyButton {
Layout.preferredHeight: 40
Layout.preferredWidth: 40
onCopyText: root.copyRequested(model.address)
visible: addressLabel.text
accessibleName: qsTr("Copy account address")
onCopyText: root.copyRequested(root.displayAddress)
visible: root.displayAddress.length > 0
icon.color: Theme.palette.textMuted
}
}

View File

@ -0,0 +1,93 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Logos.Theme
import Logos.Controls
Popup {
id: root
property string mnemonic: ""
readonly property real viewportMargin: Theme.spacing.large
modal: true
dim: true
padding: Theme.spacing.large
closePolicy: Popup.NoAutoClose
parent: Overlay.overlay
anchors.centerIn: parent
width: Math.min(460, Math.max(0, parent ? parent.width - root.viewportMargin * 2 : 460))
onOpened: savedCheck.checked = false
onClosed: root.mnemonic = ""
background: Rectangle {
color: Theme.palette.backgroundSecondary
radius: Theme.spacing.radiusXlarge
border.color: Theme.palette.backgroundElevated
}
contentItem: ColumnLayout {
width: root.availableWidth
spacing: Theme.spacing.large
LogosText {
text: qsTr("Back up your wallet")
font.pixelSize: Theme.typography.titleText
font.weight: Theme.typography.weightBold
color: Theme.palette.text
}
LogosText {
Layout.fillWidth: true
text: qsTr("Save this recovery phrase now. You need it to restore this wallet.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: phraseText.implicitHeight + Theme.spacing.medium * 2
radius: Theme.spacing.radiusLarge
color: Theme.palette.backgroundMuted
border.color: Theme.palette.backgroundElevated
TextEdit {
id: phraseText
anchors.fill: parent
anchors.margins: Theme.spacing.medium
readOnly: true
selectByMouse: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
text: root.mnemonic
color: Theme.palette.text
font.pixelSize: Theme.typography.secondaryText
background: null
Accessible.name: qsTr("Recovery phrase")
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.medium
CheckBox {
id: savedCheck
Layout.fillWidth: true
text: qsTr("I saved this recovery phrase")
checked: false
}
}
LogosButton {
Layout.fillWidth: true
height: 40
text: qsTr("Continue")
enabled: savedCheck.checked
onClicked: root.close()
}
}
}

View File

@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Logos.Theme
import Logos.Controls
@ -9,6 +9,8 @@ import Logos.Controls
Popup {
id: root
readonly property real viewportMargin: Theme.spacing.large
signal createPublicRequested()
signal createPrivateRequested()
@ -21,7 +23,7 @@ Popup {
// this popup is declared inside.
parent: Overlay.overlay
anchors.centerIn: parent
width: 360
width: Math.min(360, Math.max(0, parent ? parent.width - root.viewportMargin * 2 : 360))
background: Rectangle {
color: Theme.palette.backgroundSecondary
@ -74,6 +76,8 @@ Popup {
LogosSwitch {
id: privateSwitch
checked: false
Accessible.name: qsTr("Create private account")
Accessible.description: qsTr("Private balance and activity.")
}
}

View File

@ -1,19 +1,20 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Logos.Theme
import Logos.Controls
// Password-only wallet creation modal. Storage/config live at the per-app
// default (backend.walletHome) no path picking. Opened from the navbar
// "Connect" button.
// Password-only wallet creation modal. Storage/config live at the canonical
// LEZ wallet home (backend.walletHome), with 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: ""
readonly property real viewportMargin: Theme.spacing.large
signal createWallet(string password)
@ -25,7 +26,7 @@ Popup {
// this popup is declared inside.
parent: Overlay.overlay
anchors.centerIn: parent
width: 380
width: Math.min(380, Math.max(0, parent ? parent.width - root.viewportMargin * 2 : 380))
onOpened: {
passwordField.text = ""

View File

@ -1,5 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick 2.15
import QtQuick.Controls 2.15
import Logos.Theme
@ -8,13 +8,17 @@ Button {
signal copyText()
property string accessibleName: ""
property string iconSource: Qt.resolvedUrl("icons/copy.svg")
property bool copied: false
implicitWidth: 24
implicitHeight: 24
text: root.copied ? qsTr("Copied") : root.accessibleName
Accessible.name: text
display: AbstractButton.IconOnly
flat: true
property string iconSource: Qt.resolvedUrl("icons/copy.svg")
icon.source: root.iconSource
icon.width: 24
icon.height: 24
@ -22,6 +26,7 @@ Button {
function reset() {
iconSource = Qt.resolvedUrl("icons/copy.svg")
copied = false
}
Timer {
@ -34,6 +39,7 @@ Button {
onClicked: {
root.copyText()
root.iconSource = Qt.resolvedUrl("icons/checkmark.svg")
root.copied = true
resetTimer.restart()
}
}

View File

@ -1,5 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick 2.15
import QtQuick.Controls 2.15
import Logos.Theme
@ -12,9 +12,12 @@ Button {
property url iconSource
property color iconColor: Theme.palette.textSecondary
property int iconSize: 18
property string accessibleName: ""
implicitWidth: 32
implicitHeight: 32
text: root.accessibleName
Accessible.name: root.accessibleName
display: AbstractButton.IconOnly
flat: true

View File

@ -7,19 +7,27 @@ import "../state"
Item {
id: root
property var poolConfig: ({})
property var backend: null
property bool unsupportedChain: false
property string selectedWalletAccount: ""
property int activeLiquidityTab: 0
property real slippageTolerancePercent: 0.5
readonly property bool hasPoolConfig: !root.unsupportedChain && !!root.poolConfig.account
readonly property int pageMargin: 16
readonly property int preferredCardWidth: 492
readonly property int pageCardY: pageCard.implicitHeight + root.pageMargin * 2 <= scroll.height ? Math.round((scroll.height - pageCard.implicitHeight) / 2) : root.pageMargin
onPoolConfigChanged: poolState.loadConfig(root.poolConfig)
width: parent ? parent.width : implicitWidth
height: parent ? parent.height : implicitHeight
implicitWidth: root.preferredCardWidth + root.pageMargin * 2
implicitHeight: pageCard.implicitHeight + root.pageMargin * 2
DummyPoolState {
PoolState {
id: poolState
Component.onCompleted: loadConfig(root.poolConfig)
}
Rectangle {
@ -32,6 +40,7 @@ Item {
anchors.fill: parent
clip: true
visible: !root.unsupportedChain
contentHeight: Math.max(height, pageCard.y + pageCard.implicitHeight + root.pageMargin)
contentWidth: width
enabled: !confirmationDialog.visible
@ -54,6 +63,7 @@ Item {
anchors.fill: parent
anchors.margins: 12
enabled: root.hasPoolConfig
spacing: 10
RowLayout {
@ -162,6 +172,15 @@ Item {
}
}
Text {
anchors.centerIn: parent
visible: root.unsupportedChain
text: qsTr("Unsupported chain")
color: "#E7E1D8"
font.pixelSize: 18
font.bold: true
}
LiquidityConfirmationDialog {
id: confirmationDialog
@ -173,17 +192,69 @@ Item {
}
function confirmLiquidityAction(snapshot) {
if (!root.backend) {
successToast.show(qsTr("Transaction failed"), qsTr("Backend is not ready"), "", "error");
return;
}
snapshot.selectedWalletAccount = root.selectedWalletAccount;
const expectedSubmissionToken = root.submissionToken();
logos.watch(root.backend.submitLiquidity(snapshot),
function (resultJson) {
if (root.submissionToken() !== expectedSubmissionToken)
return;
const result = root.parseTransactionResult(resultJson);
if (!result.success) {
successToast.show(qsTr("Transaction failed"),
result.error || qsTr("Transaction rejected"),
"",
"error");
return;
}
root.applyConfirmedLiquidityAction(snapshot, result.tx_hash || "");
},
function (error) {
if (root.submissionToken() !== expectedSubmissionToken)
return;
successToast.show(qsTr("Transaction failed"), String(error), "", "error");
});
}
function submissionToken() {
if (!root.backend)
return "";
return [
root.backend.isWalletOpen ? "open" : "closed",
root.backend.sequencerAddr || "",
root.backend.deploymentNetworkMatched ? "matched" : "unmatched",
root.selectedWalletAccount || "",
root.activeLiquidityTab
].join("|");
}
function parseTransactionResult(resultJson) {
try {
return JSON.parse(resultJson);
} catch (err) {
return { "success": false, "tx_hash": "", "error": String(err) };
}
}
function applyConfirmedLiquidityAction(snapshot, transactionId) {
if (snapshot.action === "add") {
poolState.applyAddLiquidity(snapshot.actualA, snapshot.actualB, snapshot.deltaLp);
addLiquidityForm.resetForm();
successToast.show(qsTr("Liquidity added"), qsTr("Position updated"));
successToast.show(qsTr("Add liquidity submitted"),
qsTr("Position refreshed from chain"),
transactionId);
return;
}
if (snapshot.action === "remove") {
poolState.applyRemoveLiquidity(snapshot.withdrawA, snapshot.withdrawB, snapshot.burnAmount);
removeLiquidityForm.resetForm();
successToast.show(qsTr("Liquidity removed"), qsTr("Position updated"));
successToast.show(qsTr("Remove liquidity submitted"),
qsTr("Position refreshed from chain"),
transactionId);
}
}
}

View File

@ -1,21 +1,61 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import "../components/shared"
import "../components/swap"
import "../state"
Item {
id: root
property var tokens: [
{ symbol: "TOK1", name: "Token 1", color: "#627eea", letter: "E", address: "0x0000000000000000000000000000000000000000", usdPrice: 2392.70, balance: 4.25, reserve: 850 },
{ symbol: "TOK2", name: "Token 2", color: "#2775ca", letter: "$", address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", usdPrice: 1.00, balance: 12480, reserve: 2400000 },
{ symbol: "TOK3", name: "Token 3", color: "#26a17b", letter: "T", address: "0xdac17f958d2ee523a2206206994597c13d831ec7", usdPrice: 1.00, balance: 320, reserve: 1800000 },
{ symbol: "TOK4", name: "Token 4", color: "#f7931a", letter: "B", address: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", usdPrice: 63500, balance: 0.18, reserve: 42 },
{ symbol: "TOK5", name: "Token 5", color: "#627eea", letter: "E", address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", usdPrice: 2392.70, balance: 0, reserve: 600 },
{ symbol: "TOK6", name: "Token 6", color: "#9b59b6", letter: "L", address: "0x1337000000000000000000000000000000000cafe", usdPrice: 0.42, balance: 5400, reserve: 950000 }
]
property var tokens: []
property var poolConfig: ({})
property var backend: null
property bool unsupportedChain: false
property string selectedWalletAccount: ""
readonly property string poolAccount: poolConfig.account || ""
readonly property string poolAccountShort: poolAccount.length > 14
? poolAccount.substring(0, 8) + "..." + poolAccount.slice(-6)
: poolAccount
onTokensChanged: Qt.callLater(selectDefaultTokens)
Component.onCompleted: Qt.callLater(selectDefaultTokens)
function selectDefaultTokens() {
if (root.tokens.length < 2) {
swapCard.setToken("sell", null);
swapCard.setToken("buy", null);
swapCard.resetAmounts();
return;
}
if (!swapCard.sellToken || swapCard.sellToken.address !== root.tokens[0].address)
swapCard.setToken("sell", root.tokens[0]);
if (!swapCard.buyToken || swapCard.buyToken.address !== root.tokens[1].address)
swapCard.setToken("buy", root.tokens[1]);
}
function parseTransactionResult(resultJson) {
try {
return JSON.parse(resultJson);
} catch (err) {
return { "success": false, "tx_hash": "", "error": String(err) };
}
}
function withSelectedWalletAccount(snapshot) {
snapshot.selectedWalletAccount = root.selectedWalletAccount
return snapshot
}
function submissionToken() {
if (!root.backend)
return ""
return [
root.backend.isWalletOpen ? "open" : "closed",
root.backend.sequencerAddr || "",
root.backend.deploymentNetworkMatched ? "matched" : "unmatched",
root.selectedWalletAccount || ""
].join("|")
}
QtObject {
id: theme
@ -92,10 +132,12 @@ Item {
SwapCard {
id: swapCard
visible: !root.unsupportedChain
Layout.alignment: Qt.AlignHCenter
theme: theme
tokens: root.tokens
width: Math.min(480, root.width - 32)
feeBps: Number(root.poolConfig.feeBps) || 0
Layout.preferredWidth: Math.min(480, root.width - 32)
onRequestTokenSelect: function(side) {
tokenModal.targetSide = side
@ -108,13 +150,26 @@ Item {
}
Text {
visible: !root.unsupportedChain
Layout.alignment: Qt.AlignHCenter
text: "Buy and sell crypto on <font color='" + theme.colors.textPrimary + "'>LEZ</font>."
text: "Pool <font color='" + theme.colors.textPrimary + "'>" +
root.poolAccountShort +
"</font>"
textFormat: Text.RichText
color: theme.colors.textSecondary
font.pixelSize: 15
horizontalAlignment: Text.AlignHCenter
}
Text {
visible: root.unsupportedChain
Layout.alignment: Qt.AlignHCenter
text: qsTr("Unsupported chain")
color: theme.colors.textPrimary
font.pixelSize: 18
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
}
TokenSelectorModal {
@ -150,13 +205,39 @@ Item {
theme: theme
onConfirmed: function(snapshot) {
swapCard.resetAmounts()
swapToast.show(qsTr("Swap submitted"),
qsTr("%1 %2 → %3 %4")
.arg(snapshot.sellAmount)
.arg(snapshot.sellToken)
.arg(snapshot.minReceived)
.arg(snapshot.buyToken))
if (!root.backend) {
swapToast.show(qsTr("Swap failed"), qsTr("Backend is not ready"), "", "error")
return
}
const expectedSubmissionToken = root.submissionToken()
logos.watch(root.backend.submitSwap(root.withSelectedWalletAccount(snapshot)),
function(resultJson) {
if (root.submissionToken() !== expectedSubmissionToken)
return
const result = root.parseTransactionResult(resultJson)
if (!result.success) {
swapToast.show(qsTr("Swap failed"),
result.error || qsTr("Transaction rejected"),
"",
"error")
return
}
swapCard.resetAmounts()
swapToast.show(qsTr("Swap submitted"),
qsTr("%1 %2 → %3 %4")
.arg(snapshot.sellAmount)
.arg(snapshot.sellToken)
.arg(snapshot.minReceived)
.arg(snapshot.buyToken),
result.tx_hash || "")
},
function(error) {
if (root.submissionToken() !== expectedSubmissionToken)
return
swapToast.show(qsTr("Swap failed"), String(error), "", "error")
})
}
}
}

View File

@ -3,15 +3,15 @@ import QtQuick 2.15
QtObject {
id: root
property string tokenA: "USDC"
property string tokenB: "ETH"
property string feeTier: "0.30%"
property real userLpBalance: 1118033
property real reserveA: 1000000
property real reserveB: 500
property real totalLpSupply: 22360679
property real walletBalanceA: 60000
property real walletBalanceB: 20
property string tokenA: ""
property string tokenB: ""
property string feeTier: "0%"
property real userLpBalance: 0
property real reserveA: 0
property real reserveB: 0
property real totalLpSupply: 0
property real walletBalanceA: 0
property real walletBalanceB: 0
readonly property real minimumLiquidity: 1000
readonly property real poolShare: totalLpSupply > 0 ? userLpBalance / totalLpSupply : 0
@ -19,38 +19,16 @@ QtObject {
readonly property real userOwnedB: reserveB * poolShare
readonly property real tokenAPerTokenB: reserveB > 0 ? Math.floor(reserveA / reserveB) : 0
function applyAddLiquidity(actualA, actualB, mintedLp) {
const safeA = Math.max(0, Number(actualA) || 0);
const safeB = Math.max(0, Number(actualB) || 0);
const safeLp = Math.max(0, Number(mintedLp) || 0);
reserveA += safeA;
reserveB += safeB;
totalLpSupply += safeLp;
userLpBalance += safeLp;
}
function applyRemoveLiquidity(withdrawA, withdrawB, burnedLp) {
const safeA = Math.max(0, Number(withdrawA) || 0);
const safeB = Math.max(0, Number(withdrawB) || 0);
const safeLp = Math.max(0, Number(burnedLp) || 0);
reserveA = Math.max(0, reserveA - safeA);
reserveB = Math.max(0, reserveB - safeB);
totalLpSupply = Math.max(0, totalLpSupply - safeLp);
userLpBalance = Math.max(0, userLpBalance - safeLp);
}
function resetDummyState() {
tokenA = "USDC";
tokenB = "ETH";
feeTier = "0.30%";
userLpBalance = 1118033;
reserveA = 1000000;
reserveB = 500;
totalLpSupply = 22360679;
walletBalanceA = 60000;
walletBalanceB = 20;
function loadConfig(config) {
tokenA = config.tokenA || "";
tokenB = config.tokenB || "";
feeTier = config.feeTier || "0%";
userLpBalance = Number(config.userLpBalance) || 0;
reserveA = Number(config.reserveA) || 0;
reserveB = Number(config.reserveB) || 0;
totalLpSupply = Number(config.totalLpSupply) || 0;
walletBalanceA = Number(config.walletBalanceA) || 0;
walletBalanceB = Number(config.walletBalanceB) || 0;
}
function parseAmount(value) {
@ -66,7 +44,7 @@ QtObject {
return 0;
}
return reserveB * parseAmount(amountA) / reserveA;
return Math.floor(reserveB * parseAmount(amountA) / reserveA);
}
function amountAForB(amountB) {
@ -74,16 +52,16 @@ QtObject {
return 0;
}
return reserveA * parseAmount(amountB) / reserveB;
return Math.floor(reserveA * parseAmount(amountB) / reserveB);
}
function addLiquidityPreview(maxA, maxB) {
const safeMaxA = parseAmount(maxA);
const safeMaxB = parseAmount(maxB);
const safeMaxA = floorAmount(maxA);
const safeMaxB = floorAmount(maxB);
const idealA = reserveB > 0 ? reserveA * safeMaxB / reserveB : 0;
const idealB = reserveA > 0 ? reserveB * safeMaxA / reserveA : 0;
const actualA = Math.min(idealA, safeMaxA);
const actualB = Math.min(idealB, safeMaxB);
const actualA = Math.floor(Math.min(idealA, safeMaxA));
const actualB = Math.floor(Math.min(idealB, safeMaxB));
const lpFromA = reserveA > 0 ? Math.floor(totalLpSupply * actualA / reserveA) : 0;
const lpFromB = reserveB > 0 ? Math.floor(totalLpSupply * actualB / reserveB) : 0;
@ -101,7 +79,7 @@ QtObject {
}
function clampBurnAmount(value) {
return Math.min(floorAmount(value), Math.max(0, floorAmount(userLpBalance)));
return Math.min(floorAmount(value), floorAmount(userLpBalance));
}
function clampSlippageTolerancePercent(value) {
@ -118,10 +96,6 @@ QtObject {
function burnAmountForPercent(percent) {
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
if (safePercent === 100) {
return clampBurnAmount(userLpBalance);
}
return clampBurnAmount(Math.floor(userLpBalance * safePercent / 100));
}

View File

@ -1 +0,0 @@
/nix/store/05xmkf4hdg4dpk4hjanq8ik8pl7r74ym-logos-amm_ui-module

View File

@ -16,26 +16,28 @@ int AccountModel::rowCount(const QModelIndex& parent) const
QVariant AccountModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size())
if (!index.isValid() || 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();
case NameRole: return e.name;
case AddressRole: return e.address;
case DisplayAddressRole: return e.displayAddress;
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" }
{ NameRole, "name" },
{ AddressRole, "address" },
{ DisplayAddressRole, "displayAddress" },
{ BalanceRole, "balance" },
{ IsPublicRole, "isPublic" }
};
}
@ -52,9 +54,13 @@ void AccountModel::replaceFromJsonArray(const QJsonArray& arr)
if (v.isObject()) {
const QJsonObject obj = v.toObject();
e.address = obj.value(QStringLiteral("account_id")).toString();
e.displayAddress = obj.value(QStringLiteral("display_account_id")).toString();
if (e.displayAddress.isEmpty())
e.displayAddress = e.address;
e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true);
} else {
e.address = v.toString();
e.displayAddress = e.address;
e.isPublic = true;
}
m_entries.append(e);

View File

@ -11,6 +11,7 @@
struct AccountEntry {
QString name;
QString address;
QString displayAddress;
QString balance;
bool isPublic = true;
};
@ -24,6 +25,7 @@ public:
enum Role {
NameRole = Qt::UserRole + 1,
AddressRole,
DisplayAddressRole,
BalanceRole,
IsPublicRole
};

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,11 @@
#define AMM_UI_BACKEND_H
#include <QObject>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVariantList>
#include <QVariantMap>
#include "rep_AmmUiBackend_source.h"
@ -34,35 +38,85 @@ public slots:
void refreshAccounts() override;
void refreshBalances() override;
QString getBalance(QString accountIdHex, bool isPublic) override;
bool createNewDefault(QString password) override;
bool createNew(QString configPath, QString storagePath, QString password) override;
QString submitSwap(QVariantMap snapshot) override;
QString submitLiquidity(QVariantMap snapshot) override;
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).
// Canonical LEZ wallet home shared with the wallet UI and other apps.
static QString defaultWalletHome();
QString defaultConfigPath() const;
QString defaultStoragePath() const;
void persistConfigPath(const QString& path);
void persistStoragePath(const QString& path);
QJsonArray listAccounts();
void openOrAdoptWallet();
bool adoptOpenWallet();
void refreshBlockHeights();
void refreshSequencerAddr();
void loadDeploymentConfig();
void selectDeploymentForNetwork(const QString& network);
void selectDeploymentForChain(const QString& network,
const QString& blockHash,
const QString& blockSignature);
void clearDeploymentSelection(const QString& network);
void setDeploymentIdentityPendingIfNeeded(bool pending);
void verifyDeploymentTransactions();
void refreshDeploymentWalletState();
void updateDeploymentNetworkMatched();
QJsonObject configuredTokenDefinition(const QString& symbol, int fallbackIndex) const;
QString accountIdHex(const QString& accountId) const;
QStringList accountIdHexList(const QStringList& accountIds, QString* error) const;
struct PoolChainState {
double reserveA = 0;
double reserveB = 0;
double totalLpSupply = 0;
double feeBps = 0;
bool found = false;
};
PoolChainState poolChainState() const;
struct WalletFungibleHolding {
QString accountIdHex;
double balance = 0;
bool found = false;
bool ambiguous = false;
};
WalletFungibleHolding walletFungibleHolding(const QString& definitionAccountId,
const QString& accountIdFilterHex = {}) const;
QString selectedWalletAccountIdHex(const QVariantMap& snapshot, QString* error) const;
QString submitAmmTransaction(const QStringList& accountIds,
const QVariantList& signingRequirements,
const QVariantList& instruction);
void saveWallet();
// Probe the configured sequencer over HTTP and update sequencerReachable.
void checkReachability();
void probeChainIdentity(const QString& network);
AccountModel* m_accountModel;
LogosAPI* m_logosAPI;
LogosModules* m_logos;
QJsonArray m_tokenChains;
QJsonArray m_ammChains;
QJsonArray m_programChainGroups;
QString m_activeDeploymentNetwork;
bool m_activeDeploymentConfigured = false;
bool m_activeDeploymentDeployed = false;
bool m_identityProbeInFlight = false;
QStringList m_requiredDeploymentTransactions;
int m_pendingDeploymentChecks = 0;
quint64 m_deploymentCheckGeneration = 0;
quint64 m_reachabilityProbeGeneration = 0;
quint64 m_chainIdentityProbeGeneration = 0;
bool m_deploymentChecksFailed = false;
QJsonArray m_tokenDefinitions;
QJsonObject m_poolConfig;
QNetworkAccessManager* m_net;
QTimer* m_reachabilityTimer;

View File

@ -16,6 +16,16 @@ class AmmUiBackend
// Defaults true so the UI doesn't flash a warning before the first check.
PROP(bool sequencerReachable READONLY)
// Chain-backed deployment state for the configured AMM pool.
PROP(QVariantList deploymentTokens READONLY)
PROP(QVariantMap deploymentPool READONLY)
PROP(bool deploymentNetworkMatched READONLY)
PROP(bool deploymentIdentityPending READONLY)
// AMM transactions. Returns wallet JSON: { success, tx_hash, error }.
SLOT(QString submitSwap(QVariantMap snapshot))
SLOT(QString submitLiquidity(QVariantMap snapshot))
// Account management
SLOT(QString createAccountPublic())
SLOT(QString createAccountPrivate())
@ -24,10 +34,12 @@ class AmmUiBackend
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
// fresh per-app wallet at walletHome with no path picking. createNew()
// keeps explicit paths for an "advanced" flow.
SLOT(bool createNewDefault(QString password))
SLOT(bool createNew(QString configPath, QString storagePath, QString password))
// fresh wallet at the canonical LEZ wallet home with no path picking.
// createNew() exists for the QtRO contract but only accepts canonical
// config/storage filenames. Both return the new BIP39 mnemonic, or an
// empty string on failure.
SLOT(QString createNewDefault(QString password))
SLOT(QString createNew(QString configPath, QString storagePath, QString password))
// Re-open the existing on-disk wallet after a disconnect.
SLOT(bool openExisting())
@ -38,7 +50,4 @@ class AmmUiBackend
// 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))
}