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) cmake_minimum_required(VERSION 3.14)
project(AmmUiPlugin LANGUAGES CXX) 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}) if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else() else()
@ -26,4 +45,17 @@ logos_module(
LINK_LIBRARIES LINK_LIBRARIES
Qt6::Gui Qt6::Gui
Qt6::Network 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 startup the backend **adopts** the already-open wallet (see
`openOrAdoptWallet()`), surfacing **shared** accounts across apps. `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 > Follow-up: the wallet FFI requires explicit `config_path`/`storage_path` even
> though the wallet crate already defines defaults (`~/.lee/wallet`, > though the wallet crate already defines defaults (`~/.lee/wallet`,
> `from_path_or_initialize_default`). A `wallet_ffi_create_new_default()` / > `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": { "root": {
"inputs": { "inputs": {
"logos-module-builder": "logos-module-builder", "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": { "rust-overlay": {

View File

@ -3,18 +3,79 @@
inputs = { inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder"; 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 # Core wallet module (the LEZ wallet FFI Qt plugin). The input name must
# match the metadata.json `dependencies` entry so the builder can resolve # 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. # 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"; 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 { logos-module-builder.lib.mkLogosQmlModule {
src = ./.; src = ./.;
configFile = ./metadata.json; configFile = ./metadata.json;
flakeInputs = inputs; 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") readonly property var accountModel: logos.model("amm_ui", "accountModel")
property bool ready: false 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 { Connections {
target: logos target: logos
@ -36,20 +48,25 @@ Item {
anchors.right: parent.right anchors.right: parent.right
z: 101 z: 101
readonly property bool show: root.ready readonly property bool show: root.deploymentIdentityPending
&& root.backend || root.unsupportedChain
&& root.backend.isWalletOpen || (root.ready
&& root.backend.sequencerAddr.length > 0 && root.backend
&& !root.backend.sequencerReachable && root.backend.isWalletOpen
&& root.backend.sequencerAddr.length > 0
&& !root.backend.sequencerReachable)
height: show ? 32 : 0 height: show ? 32 : 0
visible: height > 0 visible: height > 0
clip: true clip: true
color: Theme.palette.warning color: Theme.palette.warning
Accessible.role: Accessible.AlertMessage
Accessible.name: bannerText.text
Behavior on height { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } Behavior on height { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Text { Text {
id: bannerText
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - 40 width: parent.width - 40
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@ -57,7 +74,11 @@ Item {
font.pixelSize: 12 font.pixelSize: 12
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.palette.background 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 { SwapPage {
anchors.fill: parent 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 visible: navbar.currentIndex === 0
} }
LiquidityPage { LiquidityPage {
anchors.fill: parent anchors.fill: parent
backend: root.ready ? root.backend : null
poolConfig: root.deploymentPoolConfig
unsupportedChain: root.unsupportedChain
selectedWalletAccount: navbar.selectedAddress
visible: navbar.currentIndex === 1 visible: navbar.currentIndex === 1
} }
} }

View File

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

View File

@ -7,7 +7,7 @@ import "../../state"
Rectangle { Rectangle {
id: root id: root
required property DummyPoolState poolState required property PoolState poolState
property real slippageTolerancePercent: 0.5 property real slippageTolerancePercent: 0.5
property string amountA: "" 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 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 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 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") : "" 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) signal slippageToleranceChangeRequested(real tolerancePercent)
@ -207,13 +207,16 @@ Rectangle {
return { return {
"action": "add", "action": "add",
"actualA": root.preview.actualA, "actualA": root.preview.actualA,
"actualAValue": Math.floor(root.preview.actualA),
"actualB": root.preview.actualB, "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), "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, "deltaLp": root.preview.deltaLp,
"depositA": root.poolState.formatTokenAmount(root.preview.actualA, root.poolState.tokenA), "depositA": root.poolState.formatTokenAmount(root.preview.actualA, root.poolState.tokenA),
"depositB": root.poolState.formatTokenAmount(root.preview.actualB, root.poolState.tokenB), "depositB": root.poolState.formatTokenAmount(root.preview.actualB, root.poolState.tokenB),
"feeTier": root.poolState.feeTier, "feeTier": root.poolState.feeTier,
"minLpReceived": root.poolState.formatLpAmount(root.minLpReceived), "minLpReceived": root.poolState.formatLpAmount(root.minLpReceived),
"minLpReceivedAmount": root.minLpReceived,
"slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent), "slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
"tokenA": root.poolState.tokenA, "tokenA": root.poolState.tokenA,
"tokenB": root.poolState.tokenB "tokenB": root.poolState.tokenB

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import "../../state"
Rectangle { Rectangle {
id: root id: root
required property DummyPoolState poolState required property PoolState poolState
property real slippageTolerancePercent: 0.5 property real slippageTolerancePercent: 0.5
property int burnAmount: 0 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 minReceivedIsZero: root.burnAmount > 0 && (root.minTokenAReceived === 0 || root.minTokenBReceived === 0)
readonly property bool canSubmit: root.hasLpTokens && root.burnAmount > 0 && !root.minReceivedIsZero 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 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 slippageToleranceChangeRequested(real tolerancePercent)
signal removeLiquidityRequested(var snapshot) signal removeLiquidityRequested(var snapshot)
@ -478,7 +478,9 @@ Rectangle {
"burnPercent": root.poolState.formatPercent(root.removePercent), "burnPercent": root.poolState.formatPercent(root.removePercent),
"burnText": root.poolState.formatLpAmount(root.preview.burnedLp), "burnText": root.poolState.formatLpAmount(root.preview.burnedLp),
"minTokenAReceived": root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA), "minTokenAReceived": root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA),
"minTokenAReceivedAmount": root.minTokenAReceived,
"minTokenBReceived": root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB), "minTokenBReceived": root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB),
"minTokenBReceivedAmount": root.minTokenBReceived,
"postRemovalShare": root.poolState.formatPoolShare(root.preview.newUserShare), "postRemovalShare": root.poolState.formatPoolShare(root.preview.newUserShare),
"slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent), "slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
"tokenA": root.poolState.tokenA, "tokenA": root.poolState.tokenA,

View File

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

View File

@ -1,4 +1,5 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
Item { Item {
@ -6,8 +7,11 @@ Item {
property string message: "" property string message: ""
property string detail: "" property string detail: ""
property string transactionId: ""
property string status: "success"
property bool copied: false
property bool open: false property bool open: false
property int duration: 3600 property int duration: 7000
height: implicitHeight height: implicitHeight
implicitHeight: toast.implicitHeight implicitHeight: toast.implicitHeight
@ -15,9 +19,28 @@ Item {
visible: root.open || fadeOut.running visible: root.open || fadeOut.running
z: 30 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.message = nextMessage;
root.detail = nextDetail || ""; root.detail = nextDetail || "";
root.transactionId = nextTransactionId || "";
root.status = nextStatus || "success";
root.copied = false;
root.open = true; root.open = true;
dismissTimer.restart(); dismissTimer.restart();
} }
@ -28,7 +51,12 @@ Item {
interval: root.duration interval: root.duration
repeat: false repeat: false
onTriggered: root.open = false onTriggered: {
if (copyButton.activeFocus || hoverGuard.containsMouse)
restart()
else
root.open = false
}
} }
Behavior on opacity { Behavior on opacity {
@ -47,8 +75,19 @@ Item {
color: "#20201F" color: "#20201F"
implicitHeight: Math.max(50, toastContent.implicitHeight + 18) implicitHeight: Math.max(50, toastContent.implicitHeight + 18)
radius: 8 radius: 8
border.color: "#4D3A2E" border.color: root.status === "error" ? "#6A2E2E" : "#4D3A2E"
border.width: 1 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 { RowLayout {
id: toastContent id: toastContent
@ -62,7 +101,7 @@ Item {
} }
Rectangle { Rectangle {
color: "#78C88D" color: root.status === "error" ? "#D75C5C" : "#78C88D"
radius: 6 radius: 6
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
@ -90,14 +129,59 @@ Item {
Text { Text {
color: "#B8ADA3" color: "#B8ADA3"
elide: Text.ElideRight
font.pixelSize: 12 font.pixelSize: 12
maximumLineCount: 3
text: root.detail text: root.detail
visible: root.detail.length > 0 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 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 buyInput: ""
property string editingSide: "sell" property string editingSide: "sell"
property real slippageTolerancePercent: 0.5 property real slippageTolerancePercent: 0.5
property int feeBps: 30
DummySwapState { SwapState {
id: swapState id: swapState
feeBps: 30 feeBps: root.feeBps
} }
signal requestTokenSelect(string side) signal requestTokenSelect(string side)
@ -77,9 +78,11 @@ Rectangle {
} }
function formatAmountValue(val) { function formatAmountValue(val) {
if (val >= 1) return val.toFixed(2) return Math.floor(val).toString()
if (val >= 0.0001) return val.toFixed(6) }
return val.toFixed(8)
function formatMaxSentValue(val) {
return Math.ceil(val).toString()
} }
readonly property string sellDisplay: editingSide === "sell" readonly property string sellDisplay: editingSide === "sell"
@ -107,8 +110,12 @@ Rectangle {
"sellToken": sellToken ? sellToken.symbol : "", "sellToken": sellToken ? sellToken.symbol : "",
"buyToken": buyToken ? buyToken.symbol : "", "buyToken": buyToken ? buyToken.symbol : "",
"sellAmount": formatAmountValue(parsedSellAmount), "sellAmount": formatAmountValue(parsedSellAmount),
"sellAmountValue": formatAmountValue(parsedSellAmount),
"buyAmount": formatAmountValue(parsedBuyAmount), "buyAmount": formatAmountValue(parsedBuyAmount),
"buyAmountValue": formatAmountValue(parsedBuyAmount),
"minReceived": formatAmountValue(minReceivedAmount), "minReceived": formatAmountValue(minReceivedAmount),
"minReceivedAmountValue": formatAmountValue(minReceivedAmount),
"maxSentAmountValue": formatMaxSentValue(swapState.maxSent(parsedSellAmount, slippageTolerancePercent)),
"feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""), "feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""),
"priceImpactPercent": swapState.formatPercent(priceImpactPercent), "priceImpactPercent": swapState.formatPercent(priceImpactPercent),
"priceImpactPercentValue": priceImpactPercent, "priceImpactPercentValue": priceImpactPercent,

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import QtQuick import QtQuick 2.15
import QtQml import QtQml 2.15
import QtQuick.Controls import QtQuick.Controls 2.15
import QtQuick.Layouts import QtQuick.Layouts 1.15
import Logos.Theme import Logos.Theme
import Logos.Controls import Logos.Controls
@ -12,8 +12,8 @@ import Logos.Controls
// clicking it opens a popup (top-right, just under the // clicking it opens a popup (top-right, just under the
// button) holding the account selector, create-account and // button) holding the account selector, create-account and
// disconnect actions. // disconnect actions.
// The selected account address is exposed via selectedAddress for the // selectedAddress stays in the wallet module's raw hex format for backend
// trade/liquidity flows to use as the "from" account. // calls; selectedDisplayAddress is the base58 format shown/copied in the UI.
Item { Item {
id: root id: root
@ -22,11 +22,13 @@ Item {
property var accountModel: null property var accountModel: null
readonly property bool connected: backend !== null && backend.isWalletOpen 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 // Index of the active account. selectedAddress/selectedName are derived from
// the model mirror below so they stay valid while the popup (and its list) // the model mirror below so they stay valid while the popup (and its list)
// is closed. // is closed.
property int selectedIndex: 0 property int selectedIndex: 0
property string selectedAccountId: ""
// Non-visual mirror of the account model: realizes every row regardless of // Non-visual mirror of the account model: realizes every row regardless of
// popup visibility, so the active account is addressable by index at all // popup visibility, so the active account is addressable by index at all
@ -36,10 +38,13 @@ Item {
model: root.accountModel model: root.accountModel
delegate: QtObject { delegate: QtObject {
readonly property string address: model.address ?? "" readonly property string address: model.address ?? ""
readonly property string displayAddress: model.displayAddress ?? ""
readonly property string name: model.name ?? "" readonly property string name: model.name ?? ""
readonly property string balance: model.balance ?? "" readonly property string balance: model.balance ?? ""
readonly property bool isPublic: model.isPublic ?? false readonly property bool isPublic: model.isPublic ?? false
} }
onObjectAdded: root.clampSelection()
onObjectRemoved: root.clampSelection()
} }
function entryAt(i) { function entryAt(i) {
@ -48,26 +53,75 @@ Item {
readonly property string selectedAddress: { readonly property string selectedAddress: {
const e = root.entryAt(root.selectedIndex) 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: { readonly property string selectedName: {
const e = root.entryAt(root.selectedIndex) const e = root.entryAt(root.selectedIndex)
return e ? e.name : "" return e && e.isPublic ? e.name : qsTr("No public account")
} }
readonly property string selectedBalance: { readonly property string selectedBalance: {
const e = root.entryAt(root.selectedIndex) const e = root.entryAt(root.selectedIndex)
return e ? e.balance : "" return e && e.isPublic ? e.balance : ""
} }
readonly property bool selectedIsPublic: { // Keep the selection within bounds as accounts are added/removed.
const e = root.entryAt(root.selectedIndex) function firstPublicIndex() {
return e ? e.isPublic : false 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() { 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 if (root.selectedIndex < 0) root.selectedIndex = 0
else if (root.selectedIndex >= accounts.count) root.selectedIndex = accounts.count - 1 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 { Connections {
target: root.accountModel target: root.accountModel
@ -97,9 +151,25 @@ Item {
clipboardProxy.text = "" 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 implicitWidth: root.connected ? connectedButton.width : connectButton.width
implicitHeight: 40 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 // Disconnected: Connect
LogosButton { LogosButton {
id: connectButton id: connectButton
@ -110,36 +180,37 @@ Item {
enabled: root.backend !== null enabled: root.backend !== null
text: qsTr("Connect") text: qsTr("Connect")
onClicked: { onClicked: {
// Re-open an existing wallet; only show the create modal on first run. if (!root.backend) return
if (root.backend && root.backend.walletExists) logos.watch(root.backend.openExisting(),
logos.watch(root.backend.openExisting(), function(ok) {
function(ok) { if (!ok) console.warn("openExisting failed") }, if (!ok) root.handleOpenExistingFailure("")
function(error) { console.warn("openExisting error:", error) }) },
else function(error) {
createWalletDialog.open() 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 // Connected: address pill that toggles the wallet menu
Rectangle { Button {
id: connectedButton id: connectedButton
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.connected visible: root.connected
implicitHeight: 40 implicitHeight: 40
implicitWidth: connectedRow.implicitWidth + Theme.spacing.medium * 2 implicitWidth: connectedRow.implicitWidth + leftPadding + rightPadding
radius: height / 2 leftPadding: Theme.spacing.medium
// Keep an opaque dark fill in both states: the navbar is white, and the rightPadding: Theme.spacing.medium
// active "muted" fill is translucent gray, which renders light over white topPadding: 0
// and makes the white label unreadable. Signal "open" with an accent bottomPadding: 0
// border instead. text: root.selectedDisplayAddress.length > 0
color: Theme.palette.backgroundSecondary ? root.truncated(root.selectedDisplayAddress)
border.width: 1 : qsTr("No public account")
border.color: walletMenu.opened ? Theme.palette.overlayOrange : "transparent" Accessible.name: text
RowLayout { contentItem: RowLayout {
id: connectedRow id: connectedRow
anchors.centerIn: parent
spacing: Theme.spacing.small spacing: Theme.spacing.small
Rectangle { Rectangle {
@ -149,7 +220,7 @@ Item {
color: "#39c06a" color: "#39c06a"
} }
LogosText { LogosText {
text: root.truncated(root.selectedAddress) || qsTr("Connected") text: connectedButton.text
font.pixelSize: Theme.typography.secondaryText font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.text color: Theme.palette.text
} }
@ -160,19 +231,26 @@ Item {
} }
} }
MouseArea { background: Rectangle {
anchors.fill: parent radius: height / 2
cursorShape: Qt.PointingHandCursor // Keep an opaque dark fill in both states: the navbar is white, and the
// CloseOnPressOutside already dismisses the popup on this same press // active "muted" fill is translucent gray, which renders light over white
// (the button is outside it), so `opened` is false by the time this // and makes the white label unreadable. Signal "open" with an accent
// fires. Without the recency guard the dismissing click would just // border instead.
// reopen it. If it just closed, leave it closed. color: Theme.palette.backgroundSecondary
onClicked: { border.width: 1
if (walletMenu.opened || (Date.now() - walletMenu.lastClosedMs) < 200) border.color: walletMenu.opened || connectedButton.activeFocus ? Theme.palette.overlayOrange : "transparent"
walletMenu.close() }
else
walletMenu.open() // 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 id: walletMenu
parent: connectedButton parent: connectedButton
y: connectedButton.height + Theme.spacing.small y: connectedButton.height + Theme.spacing.small
x: connectedButton.width - width // right-align under the button x: {
width: 360 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 padding: Theme.spacing.medium
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
@ -239,14 +327,17 @@ Item {
WalletIconButton { WalletIconButton {
iconSource: Qt.resolvedUrl("icons/account.svg") iconSource: Qt.resolvedUrl("icons/account.svg")
accessibleName: qsTr("Show accounts")
onClicked: viewStack.push(accountsView) onClicked: viewStack.push(accountsView)
} }
WalletIconButton { WalletIconButton {
iconSource: Qt.resolvedUrl("icons/settings.svg") iconSource: Qt.resolvedUrl("icons/settings.svg")
accessibleName: qsTr("Open wallet settings")
onClicked: viewStack.push(settingsView) onClicked: viewStack.push(settingsView)
} }
WalletIconButton { WalletIconButton {
iconSource: Qt.resolvedUrl("icons/power.svg") iconSource: Qt.resolvedUrl("icons/power.svg")
accessibleName: qsTr("Disconnect wallet")
onClicked: { onClicked: {
walletMenu.close() walletMenu.close()
if (root.backend) root.backend.disconnectWallet() if (root.backend) root.backend.disconnectWallet()
@ -279,12 +370,13 @@ Item {
Rectangle { Rectangle {
Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2 Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
Layout.preferredHeight: tagLabel.implicitHeight + 4 Layout.preferredHeight: tagLabel.implicitHeight + 4
visible: root.selectedAddress.length > 0
radius: 4 radius: 4
color: Theme.palette.backgroundSecondary color: Theme.palette.backgroundSecondary
LogosText { LogosText {
id: tagLabel id: tagLabel
anchors.centerIn: parent anchors.centerIn: parent
text: root.selectedIsPublic ? qsTr("Public") : qsTr("Private") text: qsTr("Public")
font.pixelSize: Theme.typography.secondaryText font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary color: Theme.palette.textSecondary
} }
@ -302,7 +394,7 @@ Item {
LogosText { LogosText {
Layout.fillWidth: true Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: root.selectedAddress text: root.selectedDisplayAddress
font.pixelSize: Theme.typography.secondaryText font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textMuted color: Theme.palette.textMuted
elide: Text.ElideMiddle elide: Text.ElideMiddle
@ -310,8 +402,9 @@ Item {
LogosCopyButton { LogosCopyButton {
Layout.preferredHeight: 40 Layout.preferredHeight: 40
Layout.preferredWidth: 40 Layout.preferredWidth: 40
visible: root.selectedAddress.length > 0 accessibleName: qsTr("Copy selected account address")
onCopyText: root.copyToClipboard(root.selectedAddress) visible: root.selectedDisplayAddress.length > 0
onCopyText: root.copyToClipboard(root.selectedDisplayAddress)
icon.color: Theme.palette.textMuted icon.color: Theme.palette.textMuted
} }
} }
@ -334,6 +427,7 @@ Item {
WalletIconButton { WalletIconButton {
iconSource: Qt.resolvedUrl("icons/back.svg") iconSource: Qt.resolvedUrl("icons/back.svg")
accessibleName: qsTr("Back")
onClicked: viewStack.pop() onClicked: viewStack.pop()
} }
LogosText { LogosText {
@ -356,9 +450,12 @@ Item {
delegate: AccountDelegate { delegate: AccountDelegate {
width: ListView.view.width width: ListView.view.width
highlighted: index === root.selectedIndex selectable: model.isPublic ?? false
highlighted: selectable && index === root.selectedIndex
onClicked: { onClicked: {
root.selectedIndex = index if (!selectable)
return
root.selectIndex(index)
viewStack.pop() viewStack.pop()
} }
onCopyRequested: (text) => root.copyToClipboard(text) onCopyRequested: (text) => root.copyToClipboard(text)
@ -367,7 +464,7 @@ Item {
LogosButton { LogosButton {
Layout.fillWidth: true Layout.fillWidth: true
height: 40 Layout.preferredHeight: 40
text: qsTr("Add") text: qsTr("Add")
// Leave the wallet menu open behind the (modal) dialog. // Leave the wallet menu open behind the (modal) dialog.
onClicked: createAccountDialog.open() onClicked: createAccountDialog.open()
@ -389,6 +486,7 @@ Item {
WalletIconButton { WalletIconButton {
iconSource: Qt.resolvedUrl("icons/back.svg") iconSource: Qt.resolvedUrl("icons/back.svg")
accessibleName: qsTr("Back")
onClicked: viewStack.pop() onClicked: viewStack.pop()
} }
LogosText { LogosText {
@ -422,11 +520,13 @@ Item {
font.pixelSize: Theme.typography.secondaryText font.pixelSize: Theme.typography.secondaryText
property bool ok: false property bool ok: false
color: ok ? Theme.palette.success : Theme.palette.error color: ok ? Theme.palette.success : Theme.palette.error
Accessible.role: Accessible.AlertMessage
Accessible.name: text
} }
LogosButton { LogosButton {
Layout.fillWidth: true Layout.fillWidth: true
height: 40 Layout.preferredHeight: 40
text: qsTr("Save") text: qsTr("Save")
onClicked: { onClicked: {
if (!root.backend) return if (!root.backend) return
@ -454,16 +554,95 @@ Item {
onCreateWallet: function(password) { onCreateWallet: function(password) {
if (!root.backend) return if (!root.backend) return
logos.watch(root.backend.createNewDefault(password), logos.watch(root.backend.createNewDefault(password),
function(ok) { function(result) {
if (ok) createWalletDialog.close() const mnemonic = String(result || "")
else createWalletDialog.createError = qsTr("Failed to create wallet. Please try again.") if (mnemonic.length > 0) {
createWalletDialog.close()
backupWalletDialog.mnemonic = mnemonic
backupWalletDialog.open()
} else {
createWalletDialog.createError = qsTr("Failed to create wallet. Please try again.")
}
}, },
function(error) { function(error) {
createWalletDialog.createError = qsTr("Error creating wallet: %1").arg(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 { CreateAccountDialog {
id: createAccountDialog id: createAccountDialog
onCreatePublicRequested: { onCreatePublicRequested: {

View File

@ -1,6 +1,6 @@
import QtQuick import QtQuick 2.15
import QtQuick.Controls import QtQuick.Controls 2.15
import QtQuick.Layouts import QtQuick.Layouts 1.15
import Logos.Theme import Logos.Theme
import Logos.Controls import Logos.Controls
@ -13,6 +13,18 @@ ItemDelegate {
// to its QML-side clipboard helper (AccountControl.copyToClipboard). // to its QML-side clipboard helper (AccountControl.copyToClipboard).
signal copyRequested(string text) 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 leftPadding: Theme.spacing.medium
rightPadding: Theme.spacing.medium rightPadding: Theme.spacing.medium
topPadding: Theme.spacing.medium topPadding: Theme.spacing.medium
@ -67,7 +79,7 @@ ItemDelegate {
id: addressLabel id: addressLabel
Layout.fillWidth: true Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: model.address ?? "" text: root.displayAddress
font.pixelSize: Theme.typography.secondaryText font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textMuted color: Theme.palette.textMuted
elide: Text.ElideMiddle elide: Text.ElideMiddle
@ -75,8 +87,9 @@ ItemDelegate {
LogosCopyButton { LogosCopyButton {
Layout.preferredHeight: 40 Layout.preferredHeight: 40
Layout.preferredWidth: 40 Layout.preferredWidth: 40
onCopyText: root.copyRequested(model.address) accessibleName: qsTr("Copy account address")
visible: addressLabel.text onCopyText: root.copyRequested(root.displayAddress)
visible: root.displayAddress.length > 0
icon.color: Theme.palette.textMuted 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 2.15
import QtQuick.Controls import QtQuick.Controls 2.15
import QtQuick.Layouts import QtQuick.Layouts 1.15
import Logos.Theme import Logos.Theme
import Logos.Controls import Logos.Controls
@ -9,6 +9,8 @@ import Logos.Controls
Popup { Popup {
id: root id: root
readonly property real viewportMargin: Theme.spacing.large
signal createPublicRequested() signal createPublicRequested()
signal createPrivateRequested() signal createPrivateRequested()
@ -21,7 +23,7 @@ Popup {
// this popup is declared inside. // this popup is declared inside.
parent: Overlay.overlay parent: Overlay.overlay
anchors.centerIn: parent anchors.centerIn: parent
width: 360 width: Math.min(360, Math.max(0, parent ? parent.width - root.viewportMargin * 2 : 360))
background: Rectangle { background: Rectangle {
color: Theme.palette.backgroundSecondary color: Theme.palette.backgroundSecondary
@ -74,6 +76,8 @@ Popup {
LogosSwitch { LogosSwitch {
id: privateSwitch id: privateSwitch
checked: false 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 2.15
import QtQuick.Controls import QtQuick.Controls 2.15
import QtQuick.Layouts import QtQuick.Layouts 1.15
import Logos.Theme import Logos.Theme
import Logos.Controls import Logos.Controls
// Password-only wallet creation modal. Storage/config live at the per-app // Password-only wallet creation modal. Storage/config live at the canonical
// default (backend.walletHome) no path picking. Opened from the navbar // LEZ wallet home (backend.walletHome), with no path picking. Opened from the
// "Connect" button. // navbar "Connect" button.
Popup { Popup {
id: root id: root
// Where the wallet will be stored, shown for transparency. // Where the wallet will be stored, shown for transparency.
property string walletHome: "" property string walletHome: ""
property string createError: "" property string createError: ""
readonly property real viewportMargin: Theme.spacing.large
signal createWallet(string password) signal createWallet(string password)
@ -25,7 +26,7 @@ Popup {
// this popup is declared inside. // this popup is declared inside.
parent: Overlay.overlay parent: Overlay.overlay
anchors.centerIn: parent anchors.centerIn: parent
width: 380 width: Math.min(380, Math.max(0, parent ? parent.width - root.viewportMargin * 2 : 380))
onOpened: { onOpened: {
passwordField.text = "" passwordField.text = ""

View File

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

View File

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

View File

@ -7,19 +7,27 @@ import "../state"
Item { Item {
id: root id: root
property var poolConfig: ({})
property var backend: null
property bool unsupportedChain: false
property string selectedWalletAccount: ""
property int activeLiquidityTab: 0 property int activeLiquidityTab: 0
property real slippageTolerancePercent: 0.5 property real slippageTolerancePercent: 0.5
readonly property bool hasPoolConfig: !root.unsupportedChain && !!root.poolConfig.account
readonly property int pageMargin: 16 readonly property int pageMargin: 16
readonly property int preferredCardWidth: 492 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 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 width: parent ? parent.width : implicitWidth
height: parent ? parent.height : implicitHeight height: parent ? parent.height : implicitHeight
implicitWidth: root.preferredCardWidth + root.pageMargin * 2 implicitWidth: root.preferredCardWidth + root.pageMargin * 2
implicitHeight: pageCard.implicitHeight + root.pageMargin * 2 implicitHeight: pageCard.implicitHeight + root.pageMargin * 2
DummyPoolState { PoolState {
id: poolState id: poolState
Component.onCompleted: loadConfig(root.poolConfig)
} }
Rectangle { Rectangle {
@ -32,6 +40,7 @@ Item {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
visible: !root.unsupportedChain
contentHeight: Math.max(height, pageCard.y + pageCard.implicitHeight + root.pageMargin) contentHeight: Math.max(height, pageCard.y + pageCard.implicitHeight + root.pageMargin)
contentWidth: width contentWidth: width
enabled: !confirmationDialog.visible enabled: !confirmationDialog.visible
@ -54,6 +63,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 anchors.margins: 12
enabled: root.hasPoolConfig
spacing: 10 spacing: 10
RowLayout { 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 { LiquidityConfirmationDialog {
id: confirmationDialog id: confirmationDialog
@ -173,17 +192,69 @@ Item {
} }
function confirmLiquidityAction(snapshot) { 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") { if (snapshot.action === "add") {
poolState.applyAddLiquidity(snapshot.actualA, snapshot.actualB, snapshot.deltaLp);
addLiquidityForm.resetForm(); addLiquidityForm.resetForm();
successToast.show(qsTr("Liquidity added"), qsTr("Position updated")); successToast.show(qsTr("Add liquidity submitted"),
qsTr("Position refreshed from chain"),
transactionId);
return; return;
} }
if (snapshot.action === "remove") { if (snapshot.action === "remove") {
poolState.applyRemoveLiquidity(snapshot.withdrawA, snapshot.withdrawB, snapshot.burnAmount);
removeLiquidityForm.resetForm(); 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 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import "../components/shared" import "../components/shared"
import "../components/swap" import "../components/swap"
import "../state"
Item { Item {
id: root id: root
property var tokens: [ property var tokens: []
{ symbol: "TOK1", name: "Token 1", color: "#627eea", letter: "E", address: "0x0000000000000000000000000000000000000000", usdPrice: 2392.70, balance: 4.25, reserve: 850 }, property var poolConfig: ({})
{ symbol: "TOK2", name: "Token 2", color: "#2775ca", letter: "$", address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", usdPrice: 1.00, balance: 12480, reserve: 2400000 }, property var backend: null
{ symbol: "TOK3", name: "Token 3", color: "#26a17b", letter: "T", address: "0xdac17f958d2ee523a2206206994597c13d831ec7", usdPrice: 1.00, balance: 320, reserve: 1800000 }, property bool unsupportedChain: false
{ symbol: "TOK4", name: "Token 4", color: "#f7931a", letter: "B", address: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", usdPrice: 63500, balance: 0.18, reserve: 42 }, property string selectedWalletAccount: ""
{ symbol: "TOK5", name: "Token 5", color: "#627eea", letter: "E", address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", usdPrice: 2392.70, balance: 0, reserve: 600 }, readonly property string poolAccount: poolConfig.account || ""
{ symbol: "TOK6", name: "Token 6", color: "#9b59b6", letter: "L", address: "0x1337000000000000000000000000000000000cafe", usdPrice: 0.42, balance: 5400, reserve: 950000 } 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 { QtObject {
id: theme id: theme
@ -92,10 +132,12 @@ Item {
SwapCard { SwapCard {
id: swapCard id: swapCard
visible: !root.unsupportedChain
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
theme: theme theme: theme
tokens: root.tokens 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) { onRequestTokenSelect: function(side) {
tokenModal.targetSide = side tokenModal.targetSide = side
@ -108,13 +150,26 @@ Item {
} }
Text { Text {
visible: !root.unsupportedChain
Layout.alignment: Qt.AlignHCenter 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 textFormat: Text.RichText
color: theme.colors.textSecondary color: theme.colors.textSecondary
font.pixelSize: 15 font.pixelSize: 15
horizontalAlignment: Text.AlignHCenter 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 { TokenSelectorModal {
@ -150,13 +205,39 @@ Item {
theme: theme theme: theme
onConfirmed: function(snapshot) { onConfirmed: function(snapshot) {
swapCard.resetAmounts() if (!root.backend) {
swapToast.show(qsTr("Swap submitted"), swapToast.show(qsTr("Swap failed"), qsTr("Backend is not ready"), "", "error")
qsTr("%1 %2 → %3 %4") return
.arg(snapshot.sellAmount) }
.arg(snapshot.sellToken)
.arg(snapshot.minReceived) const expectedSubmissionToken = root.submissionToken()
.arg(snapshot.buyToken)) 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 { QtObject {
id: root id: root
property string tokenA: "USDC" property string tokenA: ""
property string tokenB: "ETH" property string tokenB: ""
property string feeTier: "0.30%" property string feeTier: "0%"
property real userLpBalance: 1118033 property real userLpBalance: 0
property real reserveA: 1000000 property real reserveA: 0
property real reserveB: 500 property real reserveB: 0
property real totalLpSupply: 22360679 property real totalLpSupply: 0
property real walletBalanceA: 60000 property real walletBalanceA: 0
property real walletBalanceB: 20 property real walletBalanceB: 0
readonly property real minimumLiquidity: 1000 readonly property real minimumLiquidity: 1000
readonly property real poolShare: totalLpSupply > 0 ? userLpBalance / totalLpSupply : 0 readonly property real poolShare: totalLpSupply > 0 ? userLpBalance / totalLpSupply : 0
@ -19,38 +19,16 @@ QtObject {
readonly property real userOwnedB: reserveB * poolShare readonly property real userOwnedB: reserveB * poolShare
readonly property real tokenAPerTokenB: reserveB > 0 ? Math.floor(reserveA / reserveB) : 0 readonly property real tokenAPerTokenB: reserveB > 0 ? Math.floor(reserveA / reserveB) : 0
function applyAddLiquidity(actualA, actualB, mintedLp) { function loadConfig(config) {
const safeA = Math.max(0, Number(actualA) || 0); tokenA = config.tokenA || "";
const safeB = Math.max(0, Number(actualB) || 0); tokenB = config.tokenB || "";
const safeLp = Math.max(0, Number(mintedLp) || 0); feeTier = config.feeTier || "0%";
userLpBalance = Number(config.userLpBalance) || 0;
reserveA += safeA; reserveA = Number(config.reserveA) || 0;
reserveB += safeB; reserveB = Number(config.reserveB) || 0;
totalLpSupply += safeLp; totalLpSupply = Number(config.totalLpSupply) || 0;
userLpBalance += safeLp; walletBalanceA = Number(config.walletBalanceA) || 0;
} walletBalanceB = Number(config.walletBalanceB) || 0;
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 parseAmount(value) { function parseAmount(value) {
@ -66,7 +44,7 @@ QtObject {
return 0; return 0;
} }
return reserveB * parseAmount(amountA) / reserveA; return Math.floor(reserveB * parseAmount(amountA) / reserveA);
} }
function amountAForB(amountB) { function amountAForB(amountB) {
@ -74,16 +52,16 @@ QtObject {
return 0; return 0;
} }
return reserveA * parseAmount(amountB) / reserveB; return Math.floor(reserveA * parseAmount(amountB) / reserveB);
} }
function addLiquidityPreview(maxA, maxB) { function addLiquidityPreview(maxA, maxB) {
const safeMaxA = parseAmount(maxA); const safeMaxA = floorAmount(maxA);
const safeMaxB = parseAmount(maxB); const safeMaxB = floorAmount(maxB);
const idealA = reserveB > 0 ? reserveA * safeMaxB / reserveB : 0; const idealA = reserveB > 0 ? reserveA * safeMaxB / reserveB : 0;
const idealB = reserveA > 0 ? reserveB * safeMaxA / reserveA : 0; const idealB = reserveA > 0 ? reserveB * safeMaxA / reserveA : 0;
const actualA = Math.min(idealA, safeMaxA); const actualA = Math.floor(Math.min(idealA, safeMaxA));
const actualB = Math.min(idealB, safeMaxB); const actualB = Math.floor(Math.min(idealB, safeMaxB));
const lpFromA = reserveA > 0 ? Math.floor(totalLpSupply * actualA / reserveA) : 0; const lpFromA = reserveA > 0 ? Math.floor(totalLpSupply * actualA / reserveA) : 0;
const lpFromB = reserveB > 0 ? Math.floor(totalLpSupply * actualB / reserveB) : 0; const lpFromB = reserveB > 0 ? Math.floor(totalLpSupply * actualB / reserveB) : 0;
@ -101,7 +79,7 @@ QtObject {
} }
function clampBurnAmount(value) { function clampBurnAmount(value) {
return Math.min(floorAmount(value), Math.max(0, floorAmount(userLpBalance))); return Math.min(floorAmount(value), floorAmount(userLpBalance));
} }
function clampSlippageTolerancePercent(value) { function clampSlippageTolerancePercent(value) {
@ -118,10 +96,6 @@ QtObject {
function burnAmountForPercent(percent) { function burnAmountForPercent(percent) {
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0)); const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
if (safePercent === 100) {
return clampBurnAmount(userLpBalance);
}
return clampBurnAmount(Math.floor(userLpBalance * safePercent / 100)); 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 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(); return QVariant();
const AccountEntry& e = m_entries.at(index.row()); const AccountEntry& e = m_entries.at(index.row());
switch (role) { switch (role) {
case NameRole: return e.name; case NameRole: return e.name;
case AddressRole: return e.address; case AddressRole: return e.address;
case BalanceRole: return e.balance; case DisplayAddressRole: return e.displayAddress;
case IsPublicRole: return e.isPublic; case BalanceRole: return e.balance;
default: return QVariant(); case IsPublicRole: return e.isPublic;
default: return QVariant();
} }
} }
QHash<int, QByteArray> AccountModel::roleNames() const QHash<int, QByteArray> AccountModel::roleNames() const
{ {
return { return {
{ NameRole, "name" }, { NameRole, "name" },
{ AddressRole, "address" }, { AddressRole, "address" },
{ BalanceRole, "balance" }, { DisplayAddressRole, "displayAddress" },
{ IsPublicRole, "isPublic" } { BalanceRole, "balance" },
{ IsPublicRole, "isPublic" }
}; };
} }
@ -52,9 +54,13 @@ void AccountModel::replaceFromJsonArray(const QJsonArray& arr)
if (v.isObject()) { if (v.isObject()) {
const QJsonObject obj = v.toObject(); const QJsonObject obj = v.toObject();
e.address = obj.value(QStringLiteral("account_id")).toString(); 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); e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true);
} else { } else {
e.address = v.toString(); e.address = v.toString();
e.displayAddress = e.address;
e.isPublic = true; e.isPublic = true;
} }
m_entries.append(e); m_entries.append(e);

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,11 @@
#define AMM_UI_BACKEND_H #define AMM_UI_BACKEND_H
#include <QObject> #include <QObject>
#include <QJsonArray>
#include <QJsonObject>
#include <QString> #include <QString>
#include <QVariantList>
#include <QVariantMap>
#include "rep_AmmUiBackend_source.h" #include "rep_AmmUiBackend_source.h"
@ -34,35 +38,85 @@ public slots:
void refreshAccounts() override; void refreshAccounts() override;
void refreshBalances() override; void refreshBalances() override;
QString getBalance(QString accountIdHex, bool isPublic) override; QString getBalance(QString accountIdHex, bool isPublic) override;
bool createNewDefault(QString password) override; QString submitSwap(QVariantMap snapshot) override;
bool createNew(QString configPath, QString storagePath, QString password) override; QString submitLiquidity(QVariantMap snapshot) override;
QString createNewDefault(QString password) override;
QString createNew(QString configPath, QString storagePath, QString password) override;
bool openExisting() override; bool openExisting() override;
void disconnectWallet() override; void disconnectWallet() override;
bool changeSequencerAddr(QString url) override; bool changeSequencerAddr(QString url) override;
void copyToClipboard(QString text) override;
private: private:
// Per-app wallet home (kept distinct from the wallet's canonical // Canonical LEZ wallet home shared with the wallet UI and other apps.
// ~/.lee/wallet so standalone instances stay isolated; Basecamp sharing
// is handled by adopting an already-open shared wallet on startup).
static QString defaultWalletHome(); static QString defaultWalletHome();
QString defaultConfigPath() const; QString defaultConfigPath() const;
QString defaultStoragePath() const; QString defaultStoragePath() const;
void persistConfigPath(const QString& path); void persistConfigPath(const QString& path);
void persistStoragePath(const QString& path); void persistStoragePath(const QString& path);
QJsonArray listAccounts();
void openOrAdoptWallet(); void openOrAdoptWallet();
bool adoptOpenWallet();
void refreshBlockHeights(); void refreshBlockHeights();
void refreshSequencerAddr(); 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(); void saveWallet();
// Probe the configured sequencer over HTTP and update sequencerReachable. // Probe the configured sequencer over HTTP and update sequencerReachable.
void checkReachability(); void checkReachability();
void probeChainIdentity(const QString& network);
AccountModel* m_accountModel; AccountModel* m_accountModel;
LogosAPI* m_logosAPI; LogosAPI* m_logosAPI;
LogosModules* m_logos; 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; QNetworkAccessManager* m_net;
QTimer* m_reachabilityTimer; 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. // Defaults true so the UI doesn't flash a warning before the first check.
PROP(bool sequencerReachable READONLY) 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 // Account management
SLOT(QString createAccountPublic()) SLOT(QString createAccountPublic())
SLOT(QString createAccountPrivate()) SLOT(QString createAccountPrivate())
@ -24,10 +34,12 @@ class AmmUiBackend
SLOT(QString getBalance(QString accountIdHex, bool isPublic)) SLOT(QString getBalance(QString accountIdHex, bool isPublic))
// Wallet lifecycle. createNewDefault() is the happy path: it creates a // Wallet lifecycle. createNewDefault() is the happy path: it creates a
// fresh per-app wallet at walletHome with no path picking. createNew() // fresh wallet at the canonical LEZ wallet home with no path picking.
// keeps explicit paths for an "advanced" flow. // createNew() exists for the QtRO contract but only accepts canonical
SLOT(bool createNewDefault(QString password)) // config/storage filenames. Both return the new BIP39 mnemonic, or an
SLOT(bool createNew(QString configPath, QString storagePath, QString password)) // 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. // Re-open the existing on-disk wallet after a disconnect.
SLOT(bool openExisting()) SLOT(bool openExisting())
@ -38,7 +50,4 @@ class AmmUiBackend
// Settings. Rewrites the wallet config's sequencer_addr and re-opens the // Settings. Rewrites the wallet config's sequencer_addr and re-opens the
// wallet so the new network takes effect immediately. // wallet so the new network takes effect immediately.
SLOT(bool changeSequencerAddr(QString url)) SLOT(bool changeSequencerAddr(QString url))
// Misc
SLOT(void copyToClipboard(QString text))
} }