diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt
index 10f4346..0f96484 100644
--- a/apps/amm/CMakeLists.txt
+++ b/apps/amm/CMakeLists.txt
@@ -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()
diff --git a/apps/amm/README.md b/apps/amm/README.md
index e1d8ff1..ee48b87 100644
--- a/apps/amm/README.md
+++ b/apps/amm/README.md
@@ -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()` /
diff --git a/apps/amm/config/amm-programs.json b/apps/amm/config/amm-programs.json
new file mode 100644
index 0000000..a4a78bc
--- /dev/null
+++ b/apps/amm/config/amm-programs.json
@@ -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"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/apps/amm/config/ata-programs.json b/apps/amm/config/ata-programs.json
new file mode 100644
index 0000000..17e73b6
--- /dev/null
+++ b/apps/amm/config/ata-programs.json
@@ -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"
+ }
+ ]
+ }
+ ]
+}
diff --git a/apps/amm/config/stablecoin-programs.json b/apps/amm/config/stablecoin-programs.json
new file mode 100644
index 0000000..afbf609
--- /dev/null
+++ b/apps/amm/config/stablecoin-programs.json
@@ -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"
+ }
+ ]
+ }
+ ]
+}
diff --git a/apps/amm/config/supported-chains.json b/apps/amm/config/supported-chains.json
new file mode 100644
index 0000000..87b6f46
--- /dev/null
+++ b/apps/amm/config/supported-chains.json
@@ -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
+ }
+ ]
+}
diff --git a/apps/amm/config/token-programs.json b/apps/amm/config/token-programs.json
new file mode 100644
index 0000000..f32e867
--- /dev/null
+++ b/apps/amm/config/token-programs.json
@@ -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"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/apps/amm/config/twap-oracle-programs.json b/apps/amm/config/twap-oracle-programs.json
new file mode 100644
index 0000000..51d5e08
--- /dev/null
+++ b/apps/amm/config/twap-oracle-programs.json
@@ -0,0 +1,15 @@
+{
+ "chains": [
+ {
+ "chainRef": "local.v0.2.0-rc5",
+ "programs": [
+ {
+ "name": "TWAP Oracle Program",
+ "id": "AjAJzR2rrEyCvwpemcjC9tbzre38WnXTX14YuT6cWugF",
+ "imageIdHex": "90861a75ed743edaf5d2ab7dcc7563157435072b6c10f3db5cf50aa93322018c",
+ "deploymentTransaction": "54bc196c66beefe4b8ea5ab8564743332cc6b690c0b1908c55aa1407347af488"
+ }
+ ]
+ }
+ ]
+}
diff --git a/apps/amm/flake.lock b/apps/amm/flake.lock
index 9bc7175..000a3f4 100644
--- a/apps/amm/flake.lock
+++ b/apps/amm/flake.lock
@@ -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": {
diff --git a/apps/amm/flake.nix b/apps/amm/flake.nix
index 0811f1b..c97f5bf 100644
--- a/apps/amm/flake.nix
+++ b/apps/amm/flake.nix
@@ -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/"
+ '';
};
}
diff --git a/apps/amm/qml/Main.qml b/apps/amm/qml/Main.qml
index 792f011..09d4a92 100644
--- a/apps/amm/qml/Main.qml
+++ b/apps/amm/qml/Main.qml
@@ -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
}
}
diff --git a/apps/amm/qml/NavBar.qml b/apps/amm/qml/NavBar.qml
index f5b7857..3378134 100644
--- a/apps/amm/qml/NavBar.qml
+++ b/apps/amm/qml/NavBar.qml
@@ -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)
}
}
}
diff --git a/apps/amm/qml/components/liquidity/AddLiquidityForm.qml b/apps/amm/qml/components/liquidity/AddLiquidityForm.qml
index db8c59b..9b5ce7e 100644
--- a/apps/amm/qml/components/liquidity/AddLiquidityForm.qml
+++ b/apps/amm/qml/components/liquidity/AddLiquidityForm.qml
@@ -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
diff --git a/apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml b/apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml
index 16b0dc6..c4dd276 100644
--- a/apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml
+++ b/apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml
@@ -206,7 +206,7 @@ FocusScope {
activeFocusOnTab: true
focusPolicy: Qt.StrongFocus
hoverEnabled: true
- text: qsTr("Confirm")
+ text: qsTr("Submit")
Accessible.name: confirmButton.text
diff --git a/apps/amm/qml/components/liquidity/PoolPositionSummary.qml b/apps/amm/qml/components/liquidity/PoolPositionSummary.qml
index 2466ae7..937359b 100644
--- a/apps/amm/qml/components/liquidity/PoolPositionSummary.qml
+++ b/apps/amm/qml/components/liquidity/PoolPositionSummary.qml
@@ -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
diff --git a/apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml b/apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml
index b916a74..4d3e244 100644
--- a/apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml
+++ b/apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml
@@ -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,
diff --git a/apps/amm/qml/components/liquidity/TokenAmountInput.qml b/apps/amm/qml/components/liquidity/TokenAmountInput.qml
index 120be57..cce723b 100644
--- a/apps/amm/qml/components/liquidity/TokenAmountInput.qml
+++ b/apps/amm/qml/components/liquidity/TokenAmountInput.qml
@@ -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
diff --git a/apps/amm/qml/components/shared/SuccessToast.qml b/apps/amm/qml/components/shared/SuccessToast.qml
index 0c24253..1091d6e 100644
--- a/apps/amm/qml/components/shared/SuccessToast.qml
+++ b/apps/amm/qml/components/shared/SuccessToast.qml
@@ -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()
+ }
}
}
}
diff --git a/apps/amm/qml/components/swap/SwapCard.qml b/apps/amm/qml/components/swap/SwapCard.qml
index 5a3f601..dc6dce8 100644
--- a/apps/amm/qml/components/swap/SwapCard.qml
+++ b/apps/amm/qml/components/swap/SwapCard.qml
@@ -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,
diff --git a/apps/amm/qml/components/swap/SwapConfirmationDialog.qml b/apps/amm/qml/components/swap/SwapConfirmationDialog.qml
index 54f61b4..9a633a3 100644
--- a/apps/amm/qml/components/swap/SwapConfirmationDialog.qml
+++ b/apps/amm/qml/components/swap/SwapConfirmationDialog.qml
@@ -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()
diff --git a/apps/amm/qml/components/swap/TokenInput.qml b/apps/amm/qml/components/swap/TokenInput.qml
index 07b2362..06ebf12 100644
--- a/apps/amm/qml/components/swap/TokenInput.qml
+++ b/apps/amm/qml/components/swap/TokenInput.qml
@@ -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 {
diff --git a/apps/amm/qml/components/wallet/AccountControl.qml b/apps/amm/qml/components/wallet/AccountControl.qml
index 0c7d068..b02e33b 100644
--- a/apps/amm/qml/components/wallet/AccountControl.qml
+++ b/apps/amm/qml/components/wallet/AccountControl.qml
@@ -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: {
diff --git a/apps/amm/qml/components/wallet/AccountDelegate.qml b/apps/amm/qml/components/wallet/AccountDelegate.qml
index 14c03ee..427b83a 100644
--- a/apps/amm/qml/components/wallet/AccountDelegate.qml
+++ b/apps/amm/qml/components/wallet/AccountDelegate.qml
@@ -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
}
}
diff --git a/apps/amm/qml/components/wallet/BackupWalletDialog.qml b/apps/amm/qml/components/wallet/BackupWalletDialog.qml
new file mode 100644
index 0000000..fb1f225
--- /dev/null
+++ b/apps/amm/qml/components/wallet/BackupWalletDialog.qml
@@ -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()
+ }
+ }
+}
diff --git a/apps/amm/qml/components/wallet/CreateAccountDialog.qml b/apps/amm/qml/components/wallet/CreateAccountDialog.qml
index 8070795..b2a8227 100644
--- a/apps/amm/qml/components/wallet/CreateAccountDialog.qml
+++ b/apps/amm/qml/components/wallet/CreateAccountDialog.qml
@@ -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.")
}
}
diff --git a/apps/amm/qml/components/wallet/CreateWalletDialog.qml b/apps/amm/qml/components/wallet/CreateWalletDialog.qml
index ac40e8d..719f357 100644
--- a/apps/amm/qml/components/wallet/CreateWalletDialog.qml
+++ b/apps/amm/qml/components/wallet/CreateWalletDialog.qml
@@ -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 = ""
diff --git a/apps/amm/qml/components/wallet/LogosCopyButton.qml b/apps/amm/qml/components/wallet/LogosCopyButton.qml
index 7c1d7bd..3d7cfda 100644
--- a/apps/amm/qml/components/wallet/LogosCopyButton.qml
+++ b/apps/amm/qml/components/wallet/LogosCopyButton.qml
@@ -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()
}
}
diff --git a/apps/amm/qml/components/wallet/WalletIconButton.qml b/apps/amm/qml/components/wallet/WalletIconButton.qml
index 61545e0..da21abe 100644
--- a/apps/amm/qml/components/wallet/WalletIconButton.qml
+++ b/apps/amm/qml/components/wallet/WalletIconButton.qml
@@ -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
diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml
index f471952..beafb68 100644
--- a/apps/amm/qml/pages/LiquidityPage.qml
+++ b/apps/amm/qml/pages/LiquidityPage.qml
@@ -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);
}
}
}
diff --git a/apps/amm/qml/pages/SwapPage.qml b/apps/amm/qml/pages/SwapPage.qml
index 75b42d4..6351011 100644
--- a/apps/amm/qml/pages/SwapPage.qml
+++ b/apps/amm/qml/pages/SwapPage.qml
@@ -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 LEZ."
+ text: "Pool " +
+ root.poolAccountShort +
+ ""
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")
+ })
}
}
}
diff --git a/apps/amm/qml/state/DummyPoolState.qml b/apps/amm/qml/state/PoolState.qml
similarity index 71%
rename from apps/amm/qml/state/DummyPoolState.qml
rename to apps/amm/qml/state/PoolState.qml
index 7e4d573..b545369 100644
--- a/apps/amm/qml/state/DummyPoolState.qml
+++ b/apps/amm/qml/state/PoolState.qml
@@ -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));
}
diff --git a/apps/amm/qml/state/DummySwapState.qml b/apps/amm/qml/state/SwapState.qml
similarity index 100%
rename from apps/amm/qml/state/DummySwapState.qml
rename to apps/amm/qml/state/SwapState.qml
diff --git a/apps/amm/result b/apps/amm/result
deleted file mode 120000
index 9cf2dfb..0000000
--- a/apps/amm/result
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/05xmkf4hdg4dpk4hjanq8ik8pl7r74ym-logos-amm_ui-module
\ No newline at end of file
diff --git a/apps/amm/src/AccountModel.cpp b/apps/amm/src/AccountModel.cpp
index 63992f8..c22f57e 100644
--- a/apps/amm/src/AccountModel.cpp
+++ b/apps/amm/src/AccountModel.cpp
@@ -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 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);
diff --git a/apps/amm/src/AccountModel.h b/apps/amm/src/AccountModel.h
index 918839c..038eeb9 100644
--- a/apps/amm/src/AccountModel.h
+++ b/apps/amm/src/AccountModel.h
@@ -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
};
diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp
index b8d9a05..e5538b8 100644
--- a/apps/amm/src/AmmUiBackend.cpp
+++ b/apps/amm/src/AmmUiBackend.cpp
@@ -1,21 +1,35 @@
#include "AmmUiBackend.h"
-#include
+#include
+#include
+#include
+
+#include
#include
#include
#include
#include
#include
-#include
#include
#include
#include
+#include
+#include
+#include
#include
#include
#include
+#include
#include
+#include
#include
#include
+#include
+#include
+
+#ifdef Q_OS_UNIX
+#include
+#endif
#include "logos_api.h"
#include "logos_sdk.h"
@@ -28,15 +42,870 @@ namespace {
const char DISCONNECTED_KEY[] = "disconnected";
const int WALLET_FFI_SUCCESS = 0;
- // Wallet home env override. Mirrors LEZ's own var so the app shares the
- // canonical wallet (~/.lee/wallet) used by the wallet UI and other apps.
- const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
+ // Wallet home env override. Prefer the wallet CLI var, but keep the LEZ UI
+ // var as a compatibility fallback.
+ const char WALLET_HOME_ENV[] = "NSSA_WALLET_HOME_DIR";
+ const char LEGACY_WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
+ const char DEPLOYMENT_CONFIG_DIR_ENV[] = "AMM_UI_CONFIG_DIR";
+ const char DEPLOYMENT_PROGRAM_DIR_ENV[] = "AMM_UI_PROGRAM_DIR";
+ const char DEFAULT_SEQUENCER[] = "https://testnet.lez.logos.co";
+ const char LEGACY_AMM_ABI[] = "legacy-v0.2.0-rc3";
+ const char ACCOUNT_ID_KEY[] = "account_id";
+ const char DISPLAY_ACCOUNT_ID_KEY[] = "display_account_id";
+ const double MAX_SAFE_QML_INTEGER = 9007199254740991.0;
+ const int CHAIN_IDENTITY_BLOCK = 1;
+ const int BLOCK_HASH_OFFSET = 40;
+ const int BLOCK_HASH_SIZE = 32;
+ const int BLOCK_SIGNATURE_OFFSET = 80;
+ const int BLOCK_SIGNATURE_SIZE = 64;
- // Normalise file:// URLs and OS paths to a plain local path.
- QString toLocalPath(const QString& path) {
- if (path.startsWith("file://") || path.contains("/"))
- return QUrl::fromUserInput(path).toLocalFile();
- return path;
+ struct ChainFingerprint {
+ QString blockHash;
+ QString blockSignature;
+
+ bool isValid() const
+ {
+ return !blockHash.isEmpty() && !blockSignature.isEmpty();
+ }
+ };
+
+ QString txResultJson(bool success, const QString& txHash, const QString& error)
+ {
+ QJsonObject object;
+ object.insert(QStringLiteral("success"), success);
+ object.insert(QStringLiteral("tx_hash"), txHash);
+ object.insert(QStringLiteral("error"), error);
+ return QString::fromUtf8(QJsonDocument(object).toJson(QJsonDocument::Compact));
+ }
+
+ QString txError(const QString& error)
+ {
+ return txResultJson(false, {}, error);
+ }
+
+ // Normalise file:// URLs to a plain local path; leave other inputs intact so
+ // invalid URLs don't silently collapse to an empty path.
+ QString toLocalPath(const QString& path)
+ {
+ const QString trimmed = path.trimmed();
+ const QUrl url(trimmed);
+ if (url.scheme().compare(QStringLiteral("file"), Qt::CaseInsensitive) == 0) {
+ const QString local = url.toLocalFile();
+ return local.isEmpty() ? trimmed : local;
+ }
+ return trimmed;
+ }
+
+ QString pluginDirPath()
+ {
+#ifdef Q_OS_UNIX
+ Dl_info info;
+ if (dladdr(reinterpret_cast(&pluginDirPath), &info) != 0 && info.dli_fname != nullptr)
+ return QFileInfo(QString::fromLocal8Bit(info.dli_fname)).absolutePath();
+#endif
+ return QCoreApplication::applicationDirPath();
+ }
+
+ QString assetDirPath()
+ {
+#ifdef AMM_UI_ASSET_DIR
+ return QStringLiteral(AMM_UI_ASSET_DIR);
+#else
+ return {};
+#endif
+ }
+
+ QJsonObject loadConfigObject(const QString& fileName)
+ {
+ QStringList candidates;
+ const QString envConfigDir = QString::fromLocal8Bit(qgetenv(DEPLOYMENT_CONFIG_DIR_ENV));
+ if (!envConfigDir.isEmpty())
+ candidates.append(QDir(envConfigDir).filePath(fileName));
+ const QString assetDir = assetDirPath();
+ if (!assetDir.isEmpty())
+ candidates.append(QDir(assetDir).filePath(QStringLiteral("config/") + fileName));
+ candidates.append({
+ QDir(pluginDirPath()).filePath(QStringLiteral("config/") + fileName),
+ QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("config/") + fileName),
+ QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("../lib/config/") + fileName),
+ QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("../lib64/config/") + fileName),
+ });
+
+ for (const QString& path : candidates) {
+ QFile file(path);
+ if (!file.exists())
+ continue;
+ if (!file.open(QIODevice::ReadOnly)) {
+ qWarning() << "AmmUiBackend: cannot open deployment config" << path;
+ continue;
+ }
+
+ QJsonParseError error;
+ const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
+ if (error.error != QJsonParseError::NoError || !doc.isObject()) {
+ qWarning() << "AmmUiBackend: invalid deployment config" << path << error.errorString();
+ continue;
+ }
+
+ return doc.object();
+ }
+
+ qWarning() << "AmmUiBackend: deployment config not found" << fileName;
+ return {};
+ }
+
+ QByteArray loadProgramBinary(const QString& fileName, QString* error)
+ {
+ QStringList candidates;
+ const QString envProgramDir = QString::fromLocal8Bit(qgetenv(DEPLOYMENT_PROGRAM_DIR_ENV));
+ if (!envProgramDir.isEmpty())
+ candidates.append(QDir(envProgramDir).filePath(fileName));
+ const QString assetDir = assetDirPath();
+ if (!assetDir.isEmpty())
+ candidates.append(QDir(assetDir).filePath(QStringLiteral("programs/") + fileName));
+ candidates.append({
+ QDir(pluginDirPath()).filePath(QStringLiteral("programs/") + fileName),
+ QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("programs/") + fileName),
+ QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("../lib/programs/") + fileName),
+ QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("../lib64/programs/") + fileName),
+ });
+
+ for (const QString& path : candidates) {
+ QFile file(path);
+ if (!file.exists())
+ continue;
+ if (!file.open(QIODevice::ReadOnly)) {
+ qWarning() << "AmmUiBackend: cannot open program binary" << path;
+ continue;
+ }
+
+ const QByteArray bytes = file.readAll();
+ if (!bytes.isEmpty())
+ return bytes;
+ }
+
+ if (error)
+ *error = QStringLiteral("Program binary not found: %1").arg(fileName);
+ return {};
+ }
+
+ QJsonObject objectAt(const QJsonArray& array, int index)
+ {
+ if (index < 0 || index >= array.size())
+ return {};
+ return array.at(index).toObject();
+ }
+
+ QJsonObject firstObject(const QJsonObject& object, const QString& arrayKey)
+ {
+ return objectAt(object.value(arrayKey).toArray(), 0);
+ }
+
+ QString stringValue(const QJsonObject& object, const QString& key, const QString& fallback = {})
+ {
+ const QString value = object.value(key).toString().trimmed();
+ return value.isEmpty() ? fallback : value;
+ }
+
+ QJsonObject tokenDefinition(const QJsonArray& definitions, const QString& symbol, int fallbackIndex)
+ {
+ for (const QJsonValue& value : definitions) {
+ const QJsonObject definition = value.toObject();
+ if (stringValue(definition, QStringLiteral("symbol")) == symbol)
+ return definition;
+ }
+ return objectAt(definitions, fallbackIndex);
+ }
+
+ QString normalizedUrl(QString url)
+ {
+ url = url.trimmed();
+ while (url.endsWith(QLatin1Char('/')))
+ url.chop(1);
+ return url;
+ }
+
+ QString canonicalHex(QString value)
+ {
+ value = value.trimmed().toLower();
+ if (value.startsWith(QStringLiteral("0x")))
+ value.remove(0, 2);
+ return value;
+ }
+
+ void appendUnique(QStringList* values, const QString& value)
+ {
+ const QString normalized = canonicalHex(value);
+ if (!normalized.isEmpty() && !values->contains(normalized))
+ values->append(normalized);
+ }
+
+ QStringList deploymentTransactionHashes(const QJsonObject& program)
+ {
+ QStringList hashes;
+ appendUnique(&hashes, stringValue(program, QStringLiteral("deploymentTransaction")));
+ appendUnique(&hashes, stringValue(program, QStringLiteral("deploymentTx")));
+ appendUnique(&hashes, stringValue(program, QStringLiteral("transaction")));
+
+ const QJsonArray array = program.value(QStringLiteral("deploymentTransactions")).toArray();
+ for (const QJsonValue& value : array)
+ appendUnique(&hashes, value.toString());
+
+ return hashes;
+ }
+
+ QStringList poolCreationTransactionHashes(const QJsonObject& pool)
+ {
+ QStringList hashes;
+ appendUnique(&hashes, stringValue(pool, QStringLiteral("creationTransaction")));
+ appendUnique(&hashes, stringValue(pool, QStringLiteral("creationTx")));
+ appendUnique(&hashes, stringValue(pool, QStringLiteral("transaction")));
+
+ const QJsonArray array = pool.value(QStringLiteral("creationTransactions")).toArray();
+ for (const QJsonValue& value : array)
+ appendUnique(&hashes, value.toString());
+
+ return hashes;
+ }
+
+ void appendDeploymentTransactions(const QJsonObject& chain, QStringList* hashes)
+ {
+ const QJsonArray programs = chain.value(QStringLiteral("programs")).toArray();
+ for (const QJsonValue& value : programs) {
+ const QJsonObject program = value.toObject();
+ for (const QString& hash : deploymentTransactionHashes(program))
+ appendUnique(hashes, hash);
+ const QJsonArray pools = program.value(QStringLiteral("pools")).toArray();
+ for (const QJsonValue& poolValue : pools) {
+ for (const QString& hash : poolCreationTransactionHashes(poolValue.toObject()))
+ appendUnique(hashes, hash);
+ }
+ }
+ }
+
+ QString hexSlice(const QByteArray& bytes, int offset, int size)
+ {
+ if (bytes.size() < offset + size)
+ return {};
+ return QString::fromLatin1(bytes.mid(offset, size).toHex());
+ }
+
+ QByteArray jsonRpcBody(const QString& method, const QJsonArray& params)
+ {
+ QJsonObject body;
+ body.insert(QStringLiteral("jsonrpc"), QStringLiteral("2.0"));
+ body.insert(QStringLiteral("id"), 1);
+ body.insert(QStringLiteral("method"), method);
+ body.insert(QStringLiteral("params"), params);
+ return QJsonDocument(body).toJson(QJsonDocument::Compact);
+ }
+
+ ChainFingerprint chainFingerprintFromGetBlockResponse(const QByteArray& payload)
+ {
+ QJsonParseError error;
+ const QJsonDocument doc = QJsonDocument::fromJson(payload, &error);
+ if (error.error != QJsonParseError::NoError || !doc.isObject())
+ return {};
+
+ const QString block = doc.object().value(QStringLiteral("result")).toString();
+ if (block.isEmpty())
+ return {};
+
+ const QByteArray raw = QByteArray::fromBase64(block.toLatin1());
+ return {
+ hexSlice(raw, BLOCK_HASH_OFFSET, BLOCK_HASH_SIZE),
+ hexSlice(raw, BLOCK_SIGNATURE_OFFSET, BLOCK_SIGNATURE_SIZE),
+ };
+ }
+
+ enum class TransactionLookupStatus {
+ Found,
+ Missing,
+ TransientFailure,
+ };
+
+ TransactionLookupStatus transactionLookupStatus(const QByteArray& payload)
+ {
+ QJsonParseError error;
+ const QJsonDocument doc = QJsonDocument::fromJson(payload, &error);
+ if (error.error != QJsonParseError::NoError || !doc.isObject())
+ return TransactionLookupStatus::TransientFailure;
+ const QJsonObject object = doc.object();
+ if (object.contains(QStringLiteral("error")))
+ return TransactionLookupStatus::TransientFailure;
+ const QJsonValue result = object.value(QStringLiteral("result"));
+ return !result.isNull() && !result.isUndefined()
+ ? TransactionLookupStatus::Found
+ : TransactionLookupStatus::Missing;
+ }
+
+ QJsonObject supportedChainByRef(const QJsonArray& supportedChains, const QString& chainRef)
+ {
+ const QString expected = chainRef.trimmed();
+ for (const QJsonValue& value : supportedChains) {
+ const QJsonObject chain = value.toObject();
+ if (stringValue(chain, QStringLiteral("alias")) == expected)
+ return chain;
+ }
+
+ return {};
+ }
+
+ QJsonObject resolveSupportedChain(const QJsonObject& chain, const QJsonArray& supportedChains)
+ {
+ const QString chainRef = stringValue(chain, QStringLiteral("chainRef"));
+ if (chainRef.isEmpty())
+ return chain;
+
+ QJsonObject resolved = supportedChainByRef(supportedChains, chainRef);
+ if (resolved.isEmpty()) {
+ qWarning() << "AmmUiBackend: unsupported chainRef in deployment config" << chainRef;
+ return {};
+ }
+
+ for (auto it = chain.begin(); it != chain.end(); ++it) {
+ if (it.key() != QStringLiteral("chainRef"))
+ resolved.insert(it.key(), it.value());
+ }
+
+ return resolved;
+ }
+
+ QJsonArray deploymentChains(const QJsonObject& root,
+ const QString& fallbackNetwork,
+ const QJsonArray& supportedChains)
+ {
+ const QJsonArray chains = root.value(QStringLiteral("chains")).toArray();
+ if (!chains.isEmpty()) {
+ QJsonArray result;
+ for (const QJsonValue& value : chains) {
+ const QJsonObject chain =
+ resolveSupportedChain(value.toObject(), supportedChains);
+ if (!chain.isEmpty())
+ result.append(chain);
+ }
+ return result;
+ }
+
+ const QJsonArray programs = root.value(QStringLiteral("programs")).toArray();
+ if (programs.isEmpty())
+ return {};
+
+ QJsonObject chain;
+ const QString chainRef = stringValue(root, QStringLiteral("chainRef"));
+ const QString network = stringValue(
+ root, QStringLiteral("network"), chainRef.isEmpty() ? fallbackNetwork : QString{});
+ if (!network.isEmpty())
+ chain.insert(QStringLiteral("network"), network);
+ chain.insert(QStringLiteral("programs"), programs);
+ if (!chainRef.isEmpty())
+ chain.insert(QStringLiteral("chainRef"), chainRef);
+
+ QJsonArray result;
+ const QJsonObject resolved = resolveSupportedChain(chain, supportedChains);
+ if (!resolved.isEmpty())
+ result.append(resolved);
+ return result;
+ }
+
+ QString chainNetwork(const QJsonObject& chain)
+ {
+ const QString network = normalizedUrl(stringValue(chain, QStringLiteral("network")));
+ if (!network.isEmpty())
+ return network;
+
+ const QJsonArray sequencers = chain.value(QStringLiteral("sequencers")).toArray();
+ for (const QJsonValue& value : sequencers) {
+ const QString sequencer = normalizedUrl(value.toString());
+ if (!sequencer.isEmpty())
+ return sequencer;
+ }
+
+ return {};
+ }
+
+ bool chainMatchesNetwork(const QJsonObject& chain, const QString& network)
+ {
+ const QString expected = normalizedUrl(network);
+ if (chainNetwork(chain) == expected)
+ return true;
+
+ for (const QString& key : { QStringLiteral("networks"), QStringLiteral("sequencers") }) {
+ const QJsonArray values = chain.value(key).toArray();
+ for (const QJsonValue& value : values) {
+ if (normalizedUrl(value.toString()) == expected)
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ QString configuredChainFingerprint(const QJsonObject& chain)
+ {
+ const QString explicitFingerprint =
+ stringValue(chain, QStringLiteral("chainFingerprint")).toLower();
+ if (!explicitFingerprint.isEmpty())
+ return explicitFingerprint;
+
+ const QString hash = canonicalHex(stringValue(chain, QStringLiteral("genesisBlockHash")));
+ const QString signature =
+ canonicalHex(stringValue(chain, QStringLiteral("genesisBlockSignature")));
+ if (!hash.isEmpty() && !signature.isEmpty())
+ return hash + QStringLiteral(":") + signature;
+
+ return {};
+ }
+
+ bool chainHasDeterministicIdentity(const QJsonObject& chain)
+ {
+ return !configuredChainFingerprint(chain).isEmpty()
+ || !stringValue(chain, QStringLiteral("genesisBlockHash")).isEmpty()
+ || !stringValue(chain, QStringLiteral("genesisBlockSignature")).isEmpty();
+ }
+
+ bool chainsHaveDeterministicIdentity(const QJsonArray& chains)
+ {
+ for (const QJsonValue& value : chains) {
+ if (chainHasDeterministicIdentity(value.toObject()))
+ return true;
+ }
+ return false;
+ }
+
+ bool chainMatchesFingerprint(const QJsonObject& chain,
+ const QString& blockHash,
+ const QString& blockSignature)
+ {
+ const QString observedHash = canonicalHex(blockHash);
+ const QString observedSignature = canonicalHex(blockSignature);
+ const QString fingerprint = configuredChainFingerprint(chain);
+ if (!fingerprint.isEmpty())
+ return fingerprint == observedHash + QStringLiteral(":") + observedSignature;
+
+ const QString expectedHash =
+ canonicalHex(stringValue(chain, QStringLiteral("genesisBlockHash")));
+ if (!expectedHash.isEmpty() && expectedHash != observedHash)
+ return false;
+
+ const QString expectedSignature =
+ canonicalHex(stringValue(chain, QStringLiteral("genesisBlockSignature")));
+ if (!expectedSignature.isEmpty() && expectedSignature != observedSignature)
+ return false;
+
+ return chainHasDeterministicIdentity(chain);
+ }
+
+ QJsonObject chainForNetwork(const QJsonArray& chains, const QString& network)
+ {
+ const QString requested = normalizedUrl(network);
+ if (requested.isEmpty()) {
+ for (const QJsonValue& value : chains) {
+ const QJsonObject chain = value.toObject();
+ if (chain.value(QStringLiteral("default")).toBool())
+ return chain;
+ }
+ return chains.isEmpty() ? QJsonObject{} : chains.at(0).toObject();
+ }
+
+ for (const QJsonValue& value : chains) {
+ const QJsonObject chain = value.toObject();
+ if (chainMatchesNetwork(chain, requested))
+ return chain;
+ }
+
+ return {};
+ }
+
+ QJsonObject chainForFingerprint(const QJsonArray& chains,
+ const QString& network,
+ const QString& blockHash,
+ const QString& blockSignature)
+ {
+ const QString requested = normalizedUrl(network);
+ if (!blockHash.isEmpty() || !blockSignature.isEmpty()) {
+ for (const QJsonValue& value : chains) {
+ const QJsonObject chain = value.toObject();
+ if (chainMatchesFingerprint(chain, blockHash, blockSignature))
+ return chain;
+ }
+ return {};
+ }
+
+ // Legacy configs had only URLs. Once a config declares deterministic
+ // chain identity, never silently trust the endpoint URL alone.
+ if (!requested.isEmpty() && chainsHaveDeterministicIdentity(chains))
+ return {};
+
+ return chainForNetwork(chains, requested);
+ }
+
+ double numberValue(const QJsonObject& object, const QString& key, double fallback = 0)
+ {
+ const QJsonValue value = object.value(key);
+ return value.isDouble() ? value.toDouble() : fallback;
+ }
+
+ double u128LeToDouble(const QByteArray& bytes)
+ {
+ long double value = 0;
+ long double multiplier = 1;
+ for (int i = 0; i < 16; ++i) {
+ value += static_cast(bytes.at(i)) * multiplier;
+ multiplier *= 256;
+ if (value > MAX_SAFE_QML_INTEGER)
+ return MAX_SAFE_QML_INTEGER;
+ }
+ return static_cast(value);
+ }
+
+ struct DecodedPoolDefinition {
+ QString definitionTokenAIdHex;
+ QString definitionTokenBIdHex;
+ QString vaultAIdHex;
+ QString vaultBIdHex;
+ QString liquidityPoolIdHex;
+ double liquidityPoolSupply = 0;
+ double reserveA = 0;
+ double reserveB = 0;
+ double fees = 0;
+ bool active = true;
+ };
+
+ bool decodePoolDefinitionData(const QString& dataHex, DecodedPoolDefinition& pool)
+ {
+ const QString trimmed = dataHex.trimmed();
+ // PoolDefinition Borsh struct:
+ // 5 AccountId fields + liquidity_pool_supply/reserve_a/reserve_b/fees as u128.
+ // The deployed legacy AMM also has a trailing active bool.
+ if (trimmed.size() != 448 && trimmed.size() != 450)
+ return false;
+ const QByteArray data = QByteArray::fromHex(trimmed.toLatin1());
+ if (data.size() != 224 && data.size() != 225)
+ return false;
+
+ pool.definitionTokenAIdHex = QString::fromLatin1(data.mid(0, 32).toHex());
+ pool.definitionTokenBIdHex = QString::fromLatin1(data.mid(32, 32).toHex());
+ pool.vaultAIdHex = QString::fromLatin1(data.mid(64, 32).toHex());
+ pool.vaultBIdHex = QString::fromLatin1(data.mid(96, 32).toHex());
+ pool.liquidityPoolIdHex = QString::fromLatin1(data.mid(128, 32).toHex());
+ pool.liquidityPoolSupply = u128LeToDouble(data.mid(160, 16));
+ pool.reserveA = u128LeToDouble(data.mid(176, 16));
+ pool.reserveB = u128LeToDouble(data.mid(192, 16));
+ pool.fees = u128LeToDouble(data.mid(208, 16));
+ pool.active = data.size() == 224 || data.at(224) != 0;
+ return true;
+ }
+
+ bool decodeFungibleHoldingData(const QString& dataHex, QString& definitionIdHex, double& balance)
+ {
+ const QString trimmed = dataHex.trimmed();
+ // Borsh enum discriminant (u8) + AccountId (32 bytes) + u128 balance.
+ if (trimmed.size() != 98)
+ return false;
+ const QByteArray data = QByteArray::fromHex(trimmed.toLatin1());
+ if (data.size() != 49 || static_cast(data.at(0)) != 0)
+ return false;
+ definitionIdHex = QString::fromLatin1(data.mid(1, 32).toHex());
+ balance = u128LeToDouble(data.mid(33, 16));
+ return true;
+ }
+
+ QString feeTierText(double feeBps)
+ {
+ QString percent = QString::number(feeBps / 100.0, 'f', 2);
+ while (percent.contains(QLatin1Char('.')) && percent.endsWith(QLatin1Char('0')))
+ percent.chop(1);
+ if (percent.endsWith(QLatin1Char('.')))
+ percent.chop(1);
+ return percent + QStringLiteral("%");
+ }
+
+ QVariantMap tokenView(const QJsonObject& definition,
+ double reserve,
+ double balance,
+ const QString& holdingAccount)
+ {
+ const QString symbol = stringValue(definition, QStringLiteral("symbol"));
+ return {
+ { QStringLiteral("symbol"), symbol },
+ { QStringLiteral("name"), stringValue(definition, QStringLiteral("name"), symbol) },
+ { QStringLiteral("color"), stringValue(definition, QStringLiteral("color"), QStringLiteral("#627eea")) },
+ { QStringLiteral("letter"), stringValue(definition, QStringLiteral("letter"), symbol.left(1)) },
+ { QStringLiteral("address"), stringValue(definition, QStringLiteral("definitionAccount")) },
+ { QStringLiteral("holdingAccount"), holdingAccount },
+ { QStringLiteral("usdPrice"), numberValue(definition, QStringLiteral("usdPrice"), 1) },
+ { QStringLiteral("balance"), balance },
+ { QStringLiteral("reserve"), reserve }
+ };
+ }
+
+ bool isHexAccountId(const QString& value)
+ {
+ const QString trimmed = value.trimmed();
+ if (trimmed.size() != 64)
+ return false;
+ for (const QChar ch : trimmed) {
+ if (!ch.isDigit()
+ && (ch < QLatin1Char('a') || ch > QLatin1Char('f'))
+ && (ch < QLatin1Char('A') || ch > QLatin1Char('F'))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ void appendU32(QVariantList& words, quint32 word)
+ {
+ words.append(QVariant::fromValue(word));
+ }
+
+ void appendU128(QVariantList& words, quint64 value)
+ {
+ appendU32(words, static_cast(value & 0xffffffffULL));
+ appendU32(words, static_cast((value >> 32) & 0xffffffffULL));
+ appendU32(words, 0);
+ appendU32(words, 0);
+ }
+
+ void appendString(QVariantList& words, const QString& value)
+ {
+ const QByteArray bytes = value.toUtf8();
+ appendU32(words, static_cast(bytes.size()));
+ for (int i = 0; i < bytes.size(); i += 4) {
+ quint32 word = 0;
+ for (int j = 0; j < 4 && i + j < bytes.size(); ++j)
+ word |= static_cast(static_cast(bytes.at(i + j))) << (j * 8);
+ appendU32(words, word);
+ }
+ }
+
+ QVariantList boolList(std::initializer_list values)
+ {
+ QVariantList result;
+ for (const bool value : values)
+ result.append(value);
+ return result;
+ }
+
+ QVariantList byteList(const QByteArray& bytes)
+ {
+ QVariantList result;
+ result.reserve(bytes.size());
+ for (const char byte : bytes)
+ result.append(static_cast(static_cast(byte)));
+ return result;
+ }
+
+ bool walletOwnsPublicAccount(const AccountModel& model, const QString& accountIdHex)
+ {
+ for (int i = 0; i < model.count(); ++i) {
+ const QModelIndex idx = model.index(i, 0);
+ const QString address = model.data(idx, AccountModel::AddressRole).toString();
+ const bool isPublic = model.data(idx, AccountModel::IsPublicRole).toBool();
+ if (isPublic && address.compare(accountIdHex, Qt::CaseInsensitive) == 0)
+ return true;
+ }
+ return false;
+ }
+
+ bool writeFileAtomically(const QString& path, const QByteArray& bytes)
+ {
+ const QFileInfo info(path);
+ if (!QDir().mkpath(info.absolutePath()))
+ return false;
+
+ QSaveFile file(path);
+ if (!file.open(QIODevice::WriteOnly))
+ return false;
+ if (file.write(bytes) != bytes.size())
+ return false;
+ return file.commit();
+ }
+
+ bool restoreFile(const QString& path, const QByteArray& bytes, bool existed)
+ {
+ if (existed)
+ return writeFileAtomically(path, bytes);
+ if (!QFileInfo::exists(path))
+ return true;
+ return QFile::remove(path);
+ }
+
+ QString canonicalTargetPath(const QString& path)
+ {
+ const QFileInfo fileInfo(path);
+ QDir existingDir = fileInfo.absoluteDir();
+ QStringList missingDirs;
+ while (!existingDir.exists()) {
+ const QString dirName = QFileInfo(existingDir.path()).fileName();
+ if (dirName.isEmpty() || !existingDir.cdUp())
+ return {};
+ missingDirs.prepend(dirName);
+ }
+
+ QString targetPath = existingDir.canonicalPath();
+ if (targetPath.isEmpty())
+ return {};
+ for (const QString& dirName : std::as_const(missingDirs))
+ targetPath = QDir(targetPath).filePath(dirName);
+ return QDir::cleanPath(QDir(targetPath).filePath(fileInfo.fileName()));
+ }
+
+ bool pathWithinDirectory(const QString& path, const QString& directory)
+ {
+ if (path == directory)
+ return true;
+
+ QString prefix = directory;
+ if (!prefix.endsWith(QDir::separator()))
+ prefix.append(QDir::separator());
+ return path.startsWith(prefix);
+ }
+
+ QString validatedWalletFilePath(const QString& path,
+ const QString& walletHome,
+ const QString& label,
+ QString* error)
+ {
+ const QString localPath = toLocalPath(path);
+ if (localPath.isEmpty()) {
+ if (error)
+ *error = QStringLiteral("%1 path cannot be empty").arg(label);
+ return {};
+ }
+
+ if (!QDir().mkpath(walletHome)) {
+ if (error)
+ *error = QStringLiteral("Cannot create wallet home");
+ return {};
+ }
+
+ const QString walletRoot = QDir(walletHome).canonicalPath();
+ if (walletRoot.isEmpty()) {
+ if (error)
+ *error = QStringLiteral("Wallet home path is invalid");
+ return {};
+ }
+
+ if (QFileInfo(localPath).isSymLink()) {
+ if (error)
+ *error = QStringLiteral("%1 path cannot be a symbolic link").arg(label);
+ return {};
+ }
+
+ const QString targetPath = canonicalTargetPath(localPath);
+ if (targetPath.isEmpty() || !pathWithinDirectory(targetPath, walletRoot)) {
+ if (error)
+ *error = QStringLiteral("%1 path must be inside wallet home").arg(label);
+ return {};
+ }
+
+ return targetPath;
+ }
+
+ QString validatedSequencerUrl(const QString& input, QString* error)
+ {
+ const QString trimmed = normalizedUrl(input);
+ const QUrl url(trimmed, QUrl::StrictMode);
+ const QString scheme = url.scheme().toLower();
+ if (trimmed.isEmpty()) {
+ if (error)
+ *error = QStringLiteral("sequencer_addr cannot be empty");
+ return {};
+ }
+ if (!url.isValid() || url.host().isEmpty()) {
+ if (error)
+ *error = QStringLiteral("sequencer_addr must be a valid URL with a host");
+ return {};
+ }
+ if (scheme != QStringLiteral("http") && scheme != QStringLiteral("https")) {
+ if (error)
+ *error = QStringLiteral("sequencer_addr must use http or https");
+ return {};
+ }
+ return trimmed;
+ }
+
+ // Matches the legacy AMM instruction enum encoded by the pinned LEZ binary.
+ QVariantList addLiquidityInstruction(quint64 minLp, quint64 maxA, quint64 maxB)
+ {
+ QVariantList words;
+ appendU32(words, 1);
+ appendU128(words, minLp);
+ appendU128(words, maxA);
+ appendU128(words, maxB);
+ return words;
+ }
+
+ QVariantList removeLiquidityInstruction(quint64 burnLp, quint64 minA, quint64 minB)
+ {
+ QVariantList words;
+ appendU32(words, 2);
+ appendU128(words, burnLp);
+ appendU128(words, minA);
+ appendU128(words, minB);
+ return words;
+ }
+
+ QVariantList swapExactInputInstruction(quint64 amountIn, quint64 minOut, const QString& tokenDefinitionIn)
+ {
+ QVariantList words;
+ appendU32(words, 3);
+ appendU128(words, amountIn);
+ appendU128(words, minOut);
+ appendString(words, tokenDefinitionIn);
+ return words;
+ }
+
+ QVariantList swapExactOutputInstruction(quint64 exactOut, quint64 maxIn, const QString& tokenDefinitionIn)
+ {
+ QVariantList words;
+ appendU32(words, 4);
+ appendU128(words, exactOut);
+ appendU128(words, maxIn);
+ appendString(words, tokenDefinitionIn);
+ return words;
+ }
+
+ quint64 snapshotAmount(const QVariantMap& snapshot, const QString& key, QString* error)
+ {
+ bool ok = false;
+
+ const QVariant value = snapshot.value(key);
+ if (value.userType() == QMetaType::QString) {
+ const QString text = value.toString().trimmed();
+ if (text.isEmpty()) {
+ ok = false;
+ } else {
+ ok = true;
+ for (const QChar ch : text) {
+ if (!ch.isDigit()) {
+ ok = false;
+ break;
+ }
+ }
+ }
+
+ if (ok) {
+ const quint64 amount = text.toULongLong(&ok);
+ if (ok && amount <= static_cast(MAX_SAFE_QML_INTEGER))
+ return amount;
+ }
+ } else {
+ const double amount = value.toDouble(&ok);
+ if (ok
+ && std::isfinite(amount)
+ && amount >= 0
+ && amount <= MAX_SAFE_QML_INTEGER
+ && std::floor(amount) == amount) {
+ return static_cast(amount);
+ }
+ }
+
+ if (error)
+ *error = QStringLiteral("Invalid transaction amount: %1").arg(key);
+ return 0;
}
}
@@ -45,6 +914,9 @@ QString AmmUiBackend::defaultWalletHome()
const QByteArray override = qgetenv(WALLET_HOME_ENV);
if (!override.isEmpty())
return QString::fromLocal8Bit(override);
+ const QByteArray legacyOverride = qgetenv(LEGACY_WALLET_HOME_ENV);
+ if (!legacyOverride.isEmpty())
+ return QString::fromLocal8Bit(legacyOverride);
// LEZ's canonical wallet home, shared with the wallet UI and other LEZ apps
// (matches lez/wallet get_home_default_path()).
return QDir::homePath() + QStringLiteral("/.lee/wallet");
@@ -52,12 +924,16 @@ QString AmmUiBackend::defaultWalletHome()
QString AmmUiBackend::defaultConfigPath() const
{
- return defaultWalletHome() + QStringLiteral("/wallet_config.json");
+ const QString path = defaultWalletHome() + QStringLiteral("/wallet_config.json");
+ const QString canonical = canonicalTargetPath(path);
+ return canonical.isEmpty() ? QDir::cleanPath(path) : canonical;
}
QString AmmUiBackend::defaultStoragePath() const
{
- return defaultWalletHome() + QStringLiteral("/storage.json");
+ const QString path = defaultWalletHome() + QStringLiteral("/storage.json");
+ const QString canonical = canonicalTargetPath(path);
+ return canonical.isEmpty() ? QDir::cleanPath(path) : canonical;
}
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
@@ -75,23 +951,28 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
setWalletHome(defaultWalletHome());
// Assume reachable until a probe proves otherwise (avoids a startup flash).
setSequencerReachable(true);
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ setDeploymentNetworkMatched(true);
+ setDeploymentIdentityPending(false);
+ loadDeploymentConfig();
// Periodically re-probe the sequencer so the banner reacts to a node going
// up/down while the app is running. Probes are no-ops until a wallet (and
// thus a sequencer address) is open.
m_reachabilityTimer->setInterval(10000);
connect(m_reachabilityTimer, &QTimer::timeout, this, [this]() { checkReachability(); });
- m_reachabilityTimer->start();
- // Always resolve against the canonical wallet home (LEE_WALLET_HOME_DIR or
- // ~/.lee/wallet). We intentionally don't seed config/storage paths from
+ // Always resolve against the canonical wallet home (NSSA_WALLET_HOME_DIR,
+ // LEE_WALLET_HOME_DIR, or ~/.lee/wallet). We intentionally don't seed config/storage paths from
// QSettings anymore: a previously-persisted per-app path (~/.lee/amm-wallet)
// would otherwise override the default and pin the app to the old keystore.
- // A wallet exists on disk if its storage file is present (drives whether
+ // A wallet exists on disk if either canonical file is present (drives whether
// the navbar "Connect" reconnects or offers to create a wallet).
+ const QString effectiveConfig = configPath().isEmpty() ? defaultConfigPath() : configPath();
const QString effectiveStorage = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
- setWalletExists(QFileInfo::exists(effectiveStorage));
+ setWalletExists(QFileInfo::exists(effectiveConfig) || QFileInfo::exists(effectiveStorage));
// ui-host runs our constructor inside initLogos(), synchronously, BEFORE
// it enables remoting and emits READY. Any blocking RPC here would stall
@@ -116,29 +997,14 @@ void AmmUiBackend::openOrAdoptWallet()
if (QSettings(SETTINGS_ORG, SETTINGS_APP).value(DISCONNECTED_KEY, false).toBool())
return;
- // In Basecamp the logos_execution_zone module is a single shared instance,
- // so the wallet may already be open (e.g. opened by the dedicated wallet
- // app). Adopt that wallet instead of fighting over it: if list_accounts()
- // returns anything, treat the wallet as open and just mirror its state.
- const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
- if (!existing.isEmpty()) {
- qDebug() << "AmmUiBackend: adopting already-open shared wallet"
- << existing.size() << "accounts";
- setIsWalletOpen(true);
- m_accountModel->replaceFromJsonArray(existing);
- refreshBalances();
- refreshSequencerAddr();
- return;
- }
-
// Standalone (own core instance): auto-open a previously-created wallet.
- // Use persisted paths if the user picked custom ones, else the per-app
- // default. Only open if the storage actually exists, otherwise stay closed
- // so QML shows the "Connect" entry point (no noisy FFI errors on first run).
+ // If no local storage exists, still try adopting a shared Basecamp wallet.
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
- if (!QFileInfo::exists(stg))
- return; // No wallet yet — QML shows "Connect".
+ if (!QFileInfo::exists(stg)) {
+ adoptOpenWallet();
+ return;
+ }
qDebug() << "AmmUiBackend: opening wallet with config" << cfg << "storage" << stg;
const int err = m_logos->logos_execution_zone.open(cfg, stg);
@@ -146,31 +1012,68 @@ void AmmUiBackend::openOrAdoptWallet()
persistConfigPath(cfg);
persistStoragePath(stg);
setIsWalletOpen(true);
- refreshAccounts();
- refreshBlockHeights();
+ if (!m_reachabilityTimer->isActive())
+ m_reachabilityTimer->start();
refreshSequencerAddr();
- } else {
- qWarning() << "AmmUiBackend: wallet open failed, code" << err;
+ refreshAccounts();
+ return;
}
+
+ // In Basecamp the logos_execution_zone module may already have an open
+ // wallet. If opening the same disk wallet failed, try mirroring that state.
+ if (adoptOpenWallet())
+ return;
+
+ qWarning() << "AmmUiBackend: wallet open failed, code" << err;
}
-bool AmmUiBackend::createNewDefault(QString password)
+bool AmmUiBackend::adoptOpenWallet()
+{
+ const QJsonArray existing = listAccounts();
+ if (existing.isEmpty())
+ return false;
+
+ qDebug() << "AmmUiBackend: adopting already-open shared wallet"
+ << existing.size() << "accounts";
+ setIsWalletOpen(true);
+ if (!m_reachabilityTimer->isActive())
+ m_reachabilityTimer->start();
+ m_accountModel->replaceFromJsonArray(existing);
+ refreshSequencerAddr();
+ refreshBalances();
+ QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
+ return true;
+}
+
+QString AmmUiBackend::createNewDefault(QString password)
{
- QDir().mkpath(defaultWalletHome());
return createNew(defaultConfigPath(), defaultStoragePath(), password);
}
-bool AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
+QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
{
- const QString localConfig = toLocalPath(configPath);
- const QString localStorage = toLocalPath(storagePath);
- // create_new now returns the new wallet's BIP39 mnemonic (empty on failure),
- // not an int status code. TODO: surface this seed phrase to the user for
- // backup — it is currently discarded, so the wallet can't be recovered.
+ QString pathError;
+ const QString localConfig =
+ validatedWalletFilePath(configPath, defaultWalletHome(), QStringLiteral("Config"), &pathError);
+ const QString localStorage =
+ validatedWalletFilePath(storagePath, defaultWalletHome(), QStringLiteral("Storage"), &pathError);
+ if (!pathError.isEmpty()) {
+ qWarning() << "AmmUiBackend: refusing wallet path:" << pathError;
+ return QString();
+ }
+ if (localConfig != defaultConfigPath() || localStorage != defaultStoragePath()) {
+ qWarning() << "AmmUiBackend: refusing non-canonical wallet paths";
+ return QString();
+ }
+ if (QFileInfo::exists(localConfig) || QFileInfo::exists(localStorage)) {
+ qWarning() << "AmmUiBackend: refusing to create wallet over existing files";
+ return QString();
+ }
+
const QString mnemonic = m_logos->logos_execution_zone.create_new(localConfig, localStorage, password);
if (mnemonic.isEmpty()) {
qWarning() << "AmmUiBackend: create_new failed (empty mnemonic)";
- return false;
+ return QString();
}
persistConfigPath(localConfig);
@@ -178,42 +1081,35 @@ bool AmmUiBackend::createNew(QString configPath, QString storagePath, QString pa
setWalletExists(true);
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
setIsWalletOpen(true);
- refreshAccounts();
- refreshBlockHeights();
+ if (!m_reachabilityTimer->isActive())
+ m_reachabilityTimer->start();
refreshSequencerAddr();
- return true;
+ refreshAccounts();
+ return mnemonic;
}
bool AmmUiBackend::openExisting()
{
- // Adopt a shared open wallet (Basecamp), else open our own from disk.
- const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
- if (!existing.isEmpty()) {
- setIsWalletOpen(true);
- m_accountModel->replaceFromJsonArray(existing);
- refreshBalances();
- refreshSequencerAddr();
- QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
- return true;
- }
-
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
if (!QFileInfo::exists(stg))
- return false;
+ return adoptOpenWallet();
const int err = m_logos->logos_execution_zone.open(cfg, stg);
if (err != WALLET_FFI_SUCCESS) {
+ if (adoptOpenWallet())
+ return true;
qWarning() << "AmmUiBackend: openExisting failed, code" << err;
return false;
}
persistConfigPath(cfg);
persistStoragePath(stg);
setIsWalletOpen(true);
+ if (!m_reachabilityTimer->isActive())
+ m_reachabilityTimer->start();
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
- refreshAccounts();
- refreshBlockHeights();
refreshSequencerAddr();
+ refreshAccounts();
return true;
}
@@ -223,8 +1119,15 @@ void AmmUiBackend::disconnectWallet()
// the choice. We do NOT close the core module's wallet handle — in Basecamp
// that instance is shared with other apps.
saveWallet();
+ ++m_reachabilityProbeGeneration;
+ ++m_chainIdentityProbeGeneration;
+ ++m_deploymentCheckGeneration;
+ m_reachabilityTimer->stop();
setIsWalletOpen(false);
+ setSequencerAddr({});
+ setSequencerReachable(true);
m_accountModel->replaceFromJsonArray(QJsonArray());
+ selectDeploymentForNetwork({});
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, true);
}
@@ -246,7 +1149,7 @@ QString AmmUiBackend::createAccountPrivate()
void AmmUiBackend::refreshAccounts()
{
- const QJsonArray arr = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
+ const QJsonArray arr = listAccounts();
m_accountModel->replaceFromJsonArray(arr);
refreshBalances();
}
@@ -263,6 +1166,7 @@ void AmmUiBackend::refreshBalances()
const bool isPub = m_accountModel->data(idx, AccountModel::IsPublicRole).toBool();
m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub));
}
+ refreshDeploymentWalletState();
saveWallet();
}
@@ -283,31 +1187,736 @@ void AmmUiBackend::refreshBlockHeights()
void AmmUiBackend::refreshSequencerAddr()
{
+ if (!isWalletOpen()) {
+ if (!sequencerAddr().isEmpty())
+ setSequencerAddr({});
+ setSequencerReachable(true);
+ selectDeploymentForNetwork({});
+ return;
+ }
+
const QString addr = m_logos->logos_execution_zone.get_sequencer_addr();
if (sequencerAddr() != addr)
setSequencerAddr(addr);
- // Probe right away so the banner reflects the (possibly new) endpoint
- // without waiting for the next periodic tick.
- checkReachability();
+ if (addr.isEmpty()) {
+ selectDeploymentForNetwork({});
+ return;
+ }
+ clearDeploymentSelection(addr);
+ // Probe right away so the banner reflects the connected chain without
+ // trusting the endpoint URL as identity.
+ probeChainIdentity(addr);
+}
+
+void AmmUiBackend::loadDeploymentConfig()
+{
+ const QJsonObject supportedChainsRoot =
+ loadConfigObject(QStringLiteral("supported-chains.json"));
+ const QJsonArray supportedChains =
+ supportedChainsRoot.value(QStringLiteral("chains")).toArray();
+ const QJsonObject tokenRoot = loadConfigObject(QStringLiteral("token-programs.json"));
+ const QJsonObject ammRoot = loadConfigObject(QStringLiteral("amm-programs.json"));
+ const QJsonObject ataRoot = loadConfigObject(QStringLiteral("ata-programs.json"));
+ const QJsonObject twapOracleRoot =
+ loadConfigObject(QStringLiteral("twap-oracle-programs.json"));
+ const QJsonObject stablecoinRoot =
+ loadConfigObject(QStringLiteral("stablecoin-programs.json"));
+
+ const QString legacyNetwork = normalizedUrl(
+ stringValue(tokenRoot, QStringLiteral("network"), QString::fromLatin1(DEFAULT_SEQUENCER)));
+ m_tokenChains = deploymentChains(tokenRoot, legacyNetwork, supportedChains);
+ m_ammChains = deploymentChains(ammRoot, legacyNetwork, supportedChains);
+ m_programChainGroups = {};
+ for (const QJsonObject& root :
+ { tokenRoot, ammRoot, ataRoot, twapOracleRoot, stablecoinRoot }) {
+ const QJsonArray chains = deploymentChains(root, legacyNetwork, supportedChains);
+ if (!chains.isEmpty())
+ m_programChainGroups.append(chains);
+ }
+ selectDeploymentForNetwork({});
+}
+
+void AmmUiBackend::selectDeploymentForNetwork(const QString& network)
+{
+ selectDeploymentForChain(network, {}, {});
+}
+
+void AmmUiBackend::clearDeploymentSelection(const QString& network)
+{
+ m_activeDeploymentConfigured = false;
+ m_activeDeploymentDeployed = false;
+ m_identityProbeInFlight = false;
+ m_activeDeploymentNetwork = normalizedUrl(network);
+ m_tokenDefinitions = {};
+ m_poolConfig = {};
+ m_requiredDeploymentTransactions = {};
+ m_pendingDeploymentChecks = 0;
+ m_deploymentChecksFailed = false;
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ setDeploymentIdentityPending(false);
+ updateDeploymentNetworkMatched();
+}
+
+void AmmUiBackend::setDeploymentIdentityPendingIfNeeded(bool pending)
+{
+ if (deploymentIdentityPending() != pending) {
+ setDeploymentIdentityPending(pending);
+ updateDeploymentNetworkMatched();
+ }
+}
+
+void AmmUiBackend::selectDeploymentForChain(const QString& network,
+ const QString& blockHash,
+ const QString& blockSignature)
+{
+ const QString requestedNetwork = normalizedUrl(network);
+ const QJsonObject tokenChain =
+ chainForFingerprint(m_tokenChains, requestedNetwork, blockHash, blockSignature);
+ const QJsonObject ammChain =
+ chainForFingerprint(m_ammChains, requestedNetwork, blockHash, blockSignature);
+
+ m_activeDeploymentConfigured = false;
+ m_activeDeploymentDeployed = false;
+ m_identityProbeInFlight = false;
+ m_activeDeploymentNetwork = requestedNetwork.isEmpty() ? chainNetwork(ammChain) : requestedNetwork;
+ m_tokenDefinitions = {};
+ m_poolConfig = {};
+ m_requiredDeploymentTransactions = {};
+ m_pendingDeploymentChecks = 0;
+ m_deploymentChecksFailed = false;
+ setDeploymentIdentityPending(false);
+
+ if (tokenChain.isEmpty() || ammChain.isEmpty()) {
+ if (!requestedNetwork.isEmpty())
+ qWarning() << "AmmUiBackend: no AMM deployment configured for chain" << requestedNetwork;
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ updateDeploymentNetworkMatched();
+ return;
+ }
+
+ if (m_activeDeploymentNetwork.isEmpty())
+ m_activeDeploymentNetwork = chainNetwork(tokenChain);
+
+ const QJsonObject tokenProgram = firstObject(tokenChain, QStringLiteral("programs"));
+ const QJsonArray definitions = tokenProgram.value(QStringLiteral("definitions")).toArray();
+ const QJsonObject ammProgram = firstObject(ammChain, QStringLiteral("programs"));
+ const QString abi = stringValue(ammProgram, QStringLiteral("abi"));
+ if (abi != QString::fromLatin1(LEGACY_AMM_ABI)) {
+ qWarning() << "AmmUiBackend: unsupported AMM ABI" << abi;
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ updateDeploymentNetworkMatched();
+ return;
+ }
+ const QJsonObject pool = firstObject(ammProgram, QStringLiteral("pools"));
+ if (definitions.isEmpty() || pool.isEmpty()) {
+ qWarning() << "AmmUiBackend: incomplete AMM deployment config for chain"
+ << m_activeDeploymentNetwork;
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ updateDeploymentNetworkMatched();
+ return;
+ }
+
+ m_tokenDefinitions = definitions;
+ m_poolConfig = pool;
+ for (const QJsonValue& value : m_programChainGroups) {
+ const QJsonObject chain =
+ chainForFingerprint(value.toArray(), requestedNetwork, blockHash, blockSignature);
+ appendDeploymentTransactions(chain, &m_requiredDeploymentTransactions);
+ }
+ m_activeDeploymentConfigured = true;
+ verifyDeploymentTransactions();
+ updateDeploymentNetworkMatched();
+}
+
+void AmmUiBackend::verifyDeploymentTransactions()
+{
+ const quint64 generation = ++m_deploymentCheckGeneration;
+ if (!isWalletOpen() || m_requiredDeploymentTransactions.isEmpty()) {
+ m_pendingDeploymentChecks = 0;
+ m_deploymentChecksFailed = false;
+ m_activeDeploymentDeployed = true;
+ refreshDeploymentWalletState();
+ return;
+ }
+
+ m_activeDeploymentDeployed = false;
+ m_deploymentChecksFailed = false;
+ m_pendingDeploymentChecks = m_requiredDeploymentTransactions.size();
+ updateDeploymentNetworkMatched();
+
+ const QString requested = normalizedUrl(sequencerAddr());
+ for (const QString& hash : std::as_const(m_requiredDeploymentTransactions)) {
+ QJsonArray params;
+ params.append(hash);
+
+ QNetworkRequest req{QUrl(requested)};
+ req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
+ req.setTransferTimeout(4000);
+ QNetworkReply* reply =
+ m_net->post(req, jsonRpcBody(QStringLiteral("getTransaction"), params));
+ connect(reply, &QNetworkReply::finished, this, [this, reply, requested, generation, hash]() {
+ if (generation != m_deploymentCheckGeneration
+ || normalizedUrl(sequencerAddr()) != requested) {
+ reply->deleteLater();
+ return;
+ }
+
+ const bool gotHttpStatus =
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
+ const bool transportOk = gotHttpStatus || reply->error() == QNetworkReply::NoError;
+ const TransactionLookupStatus status = transportOk
+ ? transactionLookupStatus(reply->readAll())
+ : TransactionLookupStatus::TransientFailure;
+ if (status == TransactionLookupStatus::TransientFailure) {
+ qWarning() << "AmmUiBackend: deployment transaction check failed"
+ << hash << "on" << requested;
+ m_deploymentChecksFailed = true;
+ setDeploymentIdentityPendingIfNeeded(true);
+ } else if (status == TransactionLookupStatus::Missing) {
+ qWarning() << "AmmUiBackend: deployment transaction not found"
+ << hash << "on" << requested;
+ m_deploymentChecksFailed = true;
+ }
+
+ --m_pendingDeploymentChecks;
+ if (m_pendingDeploymentChecks == 0) {
+ m_activeDeploymentDeployed = !m_deploymentChecksFailed;
+ if (m_activeDeploymentDeployed)
+ refreshDeploymentWalletState();
+ else if (!deploymentIdentityPending()) {
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ updateDeploymentNetworkMatched();
+ }
+ }
+ reply->deleteLater();
+ });
+ }
+}
+
+void AmmUiBackend::refreshDeploymentWalletState()
+{
+ const QJsonObject tokenA = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenA")), 0);
+ const QJsonObject tokenB = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenB")), 1);
+ if (m_poolConfig.isEmpty() || tokenA.isEmpty() || tokenB.isEmpty()) {
+ m_activeDeploymentDeployed = false;
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ updateDeploymentNetworkMatched();
+ return;
+ }
+
+ const WalletFungibleHolding holdingA = walletFungibleHolding(
+ stringValue(tokenA, QStringLiteral("definitionAccount")));
+ const WalletFungibleHolding holdingB = walletFungibleHolding(
+ stringValue(tokenB, QStringLiteral("definitionAccount")));
+ const WalletFungibleHolding holdingLp = walletFungibleHolding(
+ stringValue(m_poolConfig, QStringLiteral("lpDefinitionAccount")));
+ const PoolChainState pool = poolChainState();
+ if (isWalletOpen() && !pool.found) {
+ qWarning() << "AmmUiBackend: AMM pool not deployed on chain"
+ << normalizedUrl(sequencerAddr());
+ m_activeDeploymentDeployed = false;
+ setDeploymentTokens({});
+ setDeploymentPool({});
+ updateDeploymentNetworkMatched();
+ return;
+ }
+
+ m_activeDeploymentDeployed = true;
+
+ QVariantList tokens;
+ tokens.append(tokenView(tokenA, pool.reserveA, holdingA.balance, holdingA.accountIdHex));
+ tokens.append(tokenView(tokenB, pool.reserveB, holdingB.balance, holdingB.accountIdHex));
+ setDeploymentTokens(tokens);
+
+ const int feeBps = static_cast(pool.feeBps);
+ const QVariantMap poolView{
+ { QStringLiteral("account"), stringValue(m_poolConfig, QStringLiteral("account")) },
+ { QStringLiteral("network"), m_activeDeploymentNetwork },
+ { QStringLiteral("tokenA"), stringValue(m_poolConfig, QStringLiteral("tokenA")) },
+ { QStringLiteral("tokenB"), stringValue(m_poolConfig, QStringLiteral("tokenB")) },
+ { QStringLiteral("feeBps"), feeBps },
+ { QStringLiteral("feeTier"), feeTierText(pool.feeBps) },
+ { QStringLiteral("userLpBalance"), holdingLp.balance },
+ { QStringLiteral("reserveA"), pool.reserveA },
+ { QStringLiteral("reserveB"), pool.reserveB },
+ { QStringLiteral("totalLpSupply"), pool.totalLpSupply },
+ { QStringLiteral("walletBalanceA"), holdingA.balance },
+ { QStringLiteral("walletBalanceB"), holdingB.balance }
+ };
+ setDeploymentPool(poolView);
+ updateDeploymentNetworkMatched();
+}
+
+void AmmUiBackend::updateDeploymentNetworkMatched()
+{
+ const bool matched = !isWalletOpen()
+ || deploymentIdentityPending()
+ || (m_activeDeploymentConfigured && m_activeDeploymentDeployed);
+ if (deploymentNetworkMatched() != matched)
+ setDeploymentNetworkMatched(matched);
+}
+
+QJsonObject AmmUiBackend::configuredTokenDefinition(const QString& symbol, int fallbackIndex) const
+{
+ return tokenDefinition(m_tokenDefinitions, symbol, fallbackIndex);
+}
+
+QString AmmUiBackend::accountIdHex(const QString& accountId) const
+{
+ const QString trimmed = accountId.trimmed();
+ if (trimmed.isEmpty())
+ return {};
+ if (isHexAccountId(trimmed))
+ return trimmed.toLower();
+ return m_logos->logos_execution_zone.account_id_from_base58(trimmed);
+}
+
+QStringList AmmUiBackend::accountIdHexList(const QStringList& accountIds, QString* error) const
+{
+ QStringList result;
+ result.reserve(accountIds.size());
+ for (const QString& accountId : accountIds) {
+ const QString hex = accountIdHex(accountId);
+ if (hex.isEmpty()) {
+ if (error)
+ *error = QStringLiteral("Invalid account id in AMM config: %1").arg(accountId);
+ return {};
+ }
+ result.append(hex);
+ }
+ return result;
+}
+
+AmmUiBackend::PoolChainState AmmUiBackend::poolChainState() const
+{
+ PoolChainState result;
+ if (!isWalletOpen())
+ return result;
+
+ const QString poolAccountIdHex = accountIdHex(stringValue(m_poolConfig, QStringLiteral("account")));
+ if (poolAccountIdHex.isEmpty())
+ return result;
+
+ const QString accountJson = m_logos->logos_execution_zone.get_account_public(poolAccountIdHex);
+ const QJsonDocument doc = QJsonDocument::fromJson(accountJson.toUtf8());
+ if (!doc.isObject())
+ return result;
+
+ DecodedPoolDefinition decoded;
+ if (!decodePoolDefinitionData(
+ doc.object().value(QStringLiteral("data")).toString(),
+ decoded)) {
+ return result;
+ }
+
+ const QJsonObject tokenA = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenA")), 0);
+ const QJsonObject tokenB = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenB")), 1);
+ const QString definitionAIdHex = accountIdHex(stringValue(tokenA, QStringLiteral("definitionAccount")));
+ const QString definitionBIdHex = accountIdHex(stringValue(tokenB, QStringLiteral("definitionAccount")));
+ const QString vaultAIdHex = accountIdHex(stringValue(m_poolConfig, QStringLiteral("vaultA")));
+ const QString vaultBIdHex = accountIdHex(stringValue(m_poolConfig, QStringLiteral("vaultB")));
+ const QString lpDefinitionIdHex = accountIdHex(
+ stringValue(m_poolConfig, QStringLiteral("lpDefinitionAccount")));
+
+ const bool matchesConfig =
+ decoded.definitionTokenAIdHex.compare(definitionAIdHex, Qt::CaseInsensitive) == 0
+ && decoded.definitionTokenBIdHex.compare(definitionBIdHex, Qt::CaseInsensitive) == 0
+ && decoded.vaultAIdHex.compare(vaultAIdHex, Qt::CaseInsensitive) == 0
+ && decoded.vaultBIdHex.compare(vaultBIdHex, Qt::CaseInsensitive) == 0
+ && decoded.liquidityPoolIdHex.compare(lpDefinitionIdHex, Qt::CaseInsensitive) == 0;
+ if (!matchesConfig) {
+ qWarning() << "AmmUiBackend: pool account state does not match deployment config";
+ return result;
+ }
+ if (!decoded.active) {
+ qWarning() << "AmmUiBackend: pool account is inactive";
+ return result;
+ }
+
+ result.reserveA = decoded.reserveA;
+ result.reserveB = decoded.reserveB;
+ result.totalLpSupply = decoded.liquidityPoolSupply;
+ result.feeBps = decoded.fees;
+ result.found = true;
+ return result;
+}
+
+AmmUiBackend::WalletFungibleHolding AmmUiBackend::walletFungibleHolding(
+ const QString& definitionAccountId,
+ const QString& accountIdFilterHex) const
+{
+ WalletFungibleHolding result;
+ const QString definitionIdHex = accountIdHex(definitionAccountId);
+ if (definitionIdHex.isEmpty())
+ return result;
+ const QString requiredAccountIdHex = canonicalHex(accountIdFilterHex);
+
+ for (int i = 0; i < m_accountModel->count(); ++i) {
+ const QModelIndex idx = m_accountModel->index(i, 0);
+ if (!m_accountModel->data(idx, AccountModel::IsPublicRole).toBool())
+ continue;
+
+ const QString accountIdHex = m_accountModel->data(idx, AccountModel::AddressRole).toString();
+ if (accountIdHex.isEmpty())
+ continue;
+ if (!requiredAccountIdHex.isEmpty()
+ && accountIdHex.compare(requiredAccountIdHex, Qt::CaseInsensitive) != 0) {
+ continue;
+ }
+
+ const QString accountJson = m_logos->logos_execution_zone.get_account_public(accountIdHex);
+ const QJsonDocument doc = QJsonDocument::fromJson(accountJson.toUtf8());
+ if (!doc.isObject())
+ continue;
+
+ QString holdingDefinitionIdHex;
+ double balance = 0;
+ if (!decodeFungibleHoldingData(
+ doc.object().value(QStringLiteral("data")).toString(),
+ holdingDefinitionIdHex,
+ balance)) {
+ continue;
+ }
+
+ if (holdingDefinitionIdHex.compare(definitionIdHex, Qt::CaseInsensitive) != 0)
+ continue;
+
+ if (result.found) {
+ result.ambiguous = true;
+ return result;
+ }
+ result.accountIdHex = accountIdHex;
+ result.balance = balance;
+ result.found = true;
+ }
+
+ return result;
+}
+
+QString AmmUiBackend::selectedWalletAccountIdHex(const QVariantMap& snapshot, QString* error) const
+{
+ const QString selected =
+ snapshot.value(QStringLiteral("selectedWalletAccount")).toString().trimmed();
+ if (selected.isEmpty()) {
+ if (error)
+ *error = QStringLiteral("No wallet account selected");
+ return {};
+ }
+
+ const QString selectedHex = accountIdHex(selected);
+ if (selectedHex.isEmpty()) {
+ if (error)
+ *error = QStringLiteral("Selected wallet account is invalid");
+ return {};
+ }
+
+ if (!walletOwnsPublicAccount(*m_accountModel, selectedHex)) {
+ if (error)
+ *error = QStringLiteral("Selected wallet account is not a public account controlled by this wallet");
+ return {};
+ }
+
+ return selectedHex;
+}
+
+QString AmmUiBackend::submitAmmTransaction(const QStringList& accountIds,
+ const QVariantList& signingRequirements,
+ const QVariantList& instruction)
+{
+ if (!isWalletOpen())
+ return txError(QStringLiteral("Wallet is not connected"));
+ if (!deploymentNetworkMatched())
+ return txError(QStringLiteral("Unsupported chain"));
+ for (int i = 0; i < accountIds.size(); ++i) {
+ if (!signingRequirements.at(i).toBool())
+ continue;
+ if (walletOwnsPublicAccount(*m_accountModel, accountIds.at(i)))
+ continue;
+ const QString displayAccountId = m_logos->logos_execution_zone.account_id_to_base58(accountIds.at(i));
+ if (displayAccountId.isEmpty())
+ return txError(QStringLiteral("Internal error: required signer account cannot be displayed"));
+ return txError(QStringLiteral("Wallet does not control required signer account: %1")
+ .arg(displayAccountId));
+ }
+
+ QString error;
+ const QByteArray ammElf = loadProgramBinary(QStringLiteral("amm.bin"), &error);
+ if (ammElf.isEmpty())
+ return txError(error);
+ const QByteArray tokenElf = loadProgramBinary(QStringLiteral("token.bin"), &error);
+ if (tokenElf.isEmpty())
+ return txError(error);
+
+ QVariantList dependencies;
+ dependencies.append(QVariant::fromValue(byteList(tokenElf)));
+ const QString result = m_logos->logos_execution_zone.send_generic_public_transaction(
+ accountIds, signingRequirements, QVariant::fromValue(instruction), ammElf,
+ QVariant::fromValue(dependencies));
+ if (result.isEmpty())
+ return txError(QStringLiteral("Wallet returned an empty transaction result"));
+
+ const QJsonDocument doc = QJsonDocument::fromJson(result.toUtf8());
+ if (doc.isObject() && doc.object().value(QStringLiteral("success")).toBool())
+ refreshBalances();
+ return result;
+}
+
+QString AmmUiBackend::submitSwap(QVariantMap snapshot)
+{
+ const QJsonObject tokenA = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenA")), 0);
+ const QJsonObject tokenB = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenB")), 1);
+ if (m_poolConfig.isEmpty() || tokenA.isEmpty() || tokenB.isEmpty())
+ return txError(QStringLiteral("AMM deployment config is incomplete"));
+
+ const QString sellSymbol = snapshot.value(QStringLiteral("sellToken")).toString();
+ const bool sellA = sellSymbol == stringValue(tokenA, QStringLiteral("symbol"));
+ const bool sellB = sellSymbol == stringValue(tokenB, QStringLiteral("symbol"));
+ if (!sellA && !sellB)
+ return txError(QStringLiteral("Swap token is not part of the configured pool"));
+
+ QString error;
+ const QString selectedAccountIdHex = selectedWalletAccountIdHex(snapshot, &error);
+ if (!error.isEmpty())
+ return txError(error);
+
+ const QString tokenDefinitionIn = stringValue(
+ sellA ? tokenA : tokenB, QStringLiteral("definitionAccount"));
+ if (tokenDefinitionIn.isEmpty())
+ return txError(QStringLiteral("Token definition account missing from AMM config"));
+ QVariantList instruction;
+ if (snapshot.value(QStringLiteral("swapMode")).toString() == QStringLiteral("swap-exact-output")) {
+ instruction = swapExactOutputInstruction(
+ snapshotAmount(snapshot, QStringLiteral("buyAmountValue"), &error),
+ snapshotAmount(snapshot, QStringLiteral("maxSentAmountValue"), &error),
+ tokenDefinitionIn);
+ } else {
+ instruction = swapExactInputInstruction(
+ snapshotAmount(snapshot, QStringLiteral("sellAmountValue"), &error),
+ snapshotAmount(snapshot, QStringLiteral("minReceivedAmountValue"), &error),
+ tokenDefinitionIn);
+ }
+ if (!error.isEmpty())
+ return txError(error);
+
+ const WalletFungibleHolding selectedHolding = walletFungibleHolding(
+ tokenDefinitionIn, selectedAccountIdHex);
+ if (!selectedHolding.found) {
+ return txError(QStringLiteral("Selected account is not a %1 holding account").arg(
+ stringValue(sellA ? tokenA : tokenB, QStringLiteral("symbol"))));
+ }
+
+ const WalletFungibleHolding holdingA = sellA
+ ? selectedHolding
+ : walletFungibleHolding(stringValue(tokenA, QStringLiteral("definitionAccount")));
+ const WalletFungibleHolding holdingB = sellB
+ ? selectedHolding
+ : walletFungibleHolding(stringValue(tokenB, QStringLiteral("definitionAccount")));
+ if (!holdingA.found)
+ return txError(QStringLiteral("Wallet has no %1 holding account").arg(
+ stringValue(tokenA, QStringLiteral("symbol"))));
+ if (!holdingB.found)
+ return txError(QStringLiteral("Wallet has no %1 holding account").arg(
+ stringValue(tokenB, QStringLiteral("symbol"))));
+ if (holdingA.ambiguous)
+ return txError(QStringLiteral("Wallet has multiple %1 holding accounts").arg(
+ stringValue(tokenA, QStringLiteral("symbol"))));
+ if (holdingB.ambiguous)
+ return txError(QStringLiteral("Wallet has multiple %1 holding accounts").arg(
+ stringValue(tokenB, QStringLiteral("symbol"))));
+
+ QStringList swapAccountIds;
+ swapAccountIds << stringValue(m_poolConfig, QStringLiteral("account"))
+ << stringValue(m_poolConfig, QStringLiteral("vaultA"))
+ << stringValue(m_poolConfig, QStringLiteral("vaultB"))
+ << holdingA.accountIdHex
+ << holdingB.accountIdHex;
+ const QStringList accountIds = accountIdHexList(swapAccountIds, &error);
+ if (!error.isEmpty())
+ return txError(error);
+
+ return submitAmmTransaction(
+ accountIds,
+ boolList({ false, false, false, sellA, sellB }),
+ instruction);
+}
+
+QString AmmUiBackend::submitLiquidity(QVariantMap snapshot)
+{
+ const QJsonObject tokenA = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenA")), 0);
+ const QJsonObject tokenB = configuredTokenDefinition(
+ stringValue(m_poolConfig, QStringLiteral("tokenB")), 1);
+ if (m_poolConfig.isEmpty() || tokenA.isEmpty() || tokenB.isEmpty())
+ return txError(QStringLiteral("AMM deployment config is incomplete"));
+
+ const QString action = snapshot.value(QStringLiteral("action")).toString();
+ QString error;
+ const QString selectedAccountIdHex = selectedWalletAccountIdHex(snapshot, &error);
+ if (!error.isEmpty())
+ return txError(error);
+
+ QVariantList instruction;
+ QVariantList signingRequirements;
+ if (action == QStringLiteral("add")) {
+ instruction = addLiquidityInstruction(
+ snapshotAmount(snapshot, QStringLiteral("minLpReceivedAmount"), &error),
+ snapshotAmount(snapshot, QStringLiteral("actualAValue"), &error),
+ snapshotAmount(snapshot, QStringLiteral("actualBValue"), &error));
+ signingRequirements = boolList({ false, false, false, false, true, true, false });
+ } else if (action == QStringLiteral("remove")) {
+ instruction = removeLiquidityInstruction(
+ snapshotAmount(snapshot, QStringLiteral("burnAmount"), &error),
+ snapshotAmount(snapshot, QStringLiteral("minTokenAReceivedAmount"), &error),
+ snapshotAmount(snapshot, QStringLiteral("minTokenBReceivedAmount"), &error));
+ signingRequirements = boolList({ false, false, false, false, false, false, true });
+ } else {
+ return txError(QStringLiteral("Unknown liquidity action"));
+ }
+ if (!error.isEmpty())
+ return txError(error);
+
+ const WalletFungibleHolding selectedHoldingA = walletFungibleHolding(
+ stringValue(tokenA, QStringLiteral("definitionAccount")), selectedAccountIdHex);
+ const WalletFungibleHolding selectedHoldingB = walletFungibleHolding(
+ stringValue(tokenB, QStringLiteral("definitionAccount")), selectedAccountIdHex);
+ const WalletFungibleHolding holdingA = selectedHoldingA.found
+ ? selectedHoldingA
+ : walletFungibleHolding(stringValue(tokenA, QStringLiteral("definitionAccount")));
+ const WalletFungibleHolding holdingB = selectedHoldingB.found
+ ? selectedHoldingB
+ : walletFungibleHolding(stringValue(tokenB, QStringLiteral("definitionAccount")));
+ if (!holdingA.found)
+ return txError(QStringLiteral("Wallet has no %1 holding account").arg(
+ stringValue(tokenA, QStringLiteral("symbol"))));
+ if (!holdingB.found)
+ return txError(QStringLiteral("Wallet has no %1 holding account").arg(
+ stringValue(tokenB, QStringLiteral("symbol"))));
+ if (holdingA.ambiguous)
+ return txError(QStringLiteral("Wallet has multiple %1 holding accounts").arg(
+ stringValue(tokenA, QStringLiteral("symbol"))));
+ if (holdingB.ambiguous)
+ return txError(QStringLiteral("Wallet has multiple %1 holding accounts").arg(
+ stringValue(tokenB, QStringLiteral("symbol"))));
+
+ const WalletFungibleHolding holdingLp = walletFungibleHolding(
+ stringValue(m_poolConfig, QStringLiteral("lpDefinitionAccount")),
+ action == QStringLiteral("remove") ? selectedAccountIdHex : QString{});
+ QString lpHoldingAccountId = holdingLp.accountIdHex;
+ if (holdingLp.ambiguous)
+ return txError(QStringLiteral("Wallet has multiple LP holding accounts for this pool"));
+ if (action == QStringLiteral("add") && lpHoldingAccountId.isEmpty()) {
+ lpHoldingAccountId = createAccountPublic();
+ if (lpHoldingAccountId.isEmpty())
+ return txError(QStringLiteral("Could not create LP holding account"));
+ } else if (action == QStringLiteral("remove") && !holdingLp.found) {
+ return txError(QStringLiteral("Selected account is not an LP holding account for this pool"));
+ }
+
+ QStringList liquidityAccountIds;
+ liquidityAccountIds << stringValue(m_poolConfig, QStringLiteral("account"))
+ << stringValue(m_poolConfig, QStringLiteral("vaultA"))
+ << stringValue(m_poolConfig, QStringLiteral("vaultB"))
+ << stringValue(m_poolConfig, QStringLiteral("lpDefinitionAccount"))
+ << holdingA.accountIdHex
+ << holdingB.accountIdHex
+ << lpHoldingAccountId;
+ const QStringList accountIds = accountIdHexList(liquidityAccountIds, &error);
+ if (!error.isEmpty())
+ return txError(error);
+
+ return submitAmmTransaction(accountIds, signingRequirements, instruction);
}
void AmmUiBackend::checkReachability()
{
- const QString addr = sequencerAddr();
- if (addr.isEmpty())
+ const QString requested = normalizedUrl(sequencerAddr());
+ if (requested.isEmpty())
return;
+ const quint64 generation = ++m_reachabilityProbeGeneration;
- QNetworkRequest req{QUrl(addr)};
+ QNetworkRequest req{QUrl(requested)};
req.setTransferTimeout(4000);
QNetworkReply* reply = m_net->get(req);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+ connect(reply, &QNetworkReply::finished, this, [this, reply, requested, generation]() {
+ if (generation != m_reachabilityProbeGeneration
+ || normalizedUrl(sequencerAddr()) != requested) {
+ reply->deleteLater();
+ return;
+ }
+
// Any HTTP response (even a 404) means the node is up; only a transport
// failure (connection refused, host not found, timeout) counts as down.
+ const bool gotHttpStatus =
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
+ const bool reachable = gotHttpStatus || reply->error() == QNetworkReply::NoError;
+ const bool wasReachable = sequencerReachable();
+ if (wasReachable != reachable)
+ setSequencerReachable(reachable);
+ if (reachable
+ && (!wasReachable || deploymentIdentityPending())
+ && !m_identityProbeInFlight) {
+ probeChainIdentity(requested);
+ }
+ reply->deleteLater();
+ });
+}
+
+void AmmUiBackend::probeChainIdentity(const QString& network)
+{
+ const QString requested = normalizedUrl(network);
+ const quint64 generation = ++m_chainIdentityProbeGeneration;
+ clearDeploymentSelection(requested);
+ m_identityProbeInFlight = true;
+ setDeploymentIdentityPendingIfNeeded(true);
+
+ QJsonArray params;
+ params.append(CHAIN_IDENTITY_BLOCK);
+
+ QNetworkRequest req{QUrl(requested)};
+ req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
+ req.setTransferTimeout(4000);
+ QNetworkReply* reply =
+ m_net->post(req, jsonRpcBody(QStringLiteral("getBlock"), params));
+ connect(reply, &QNetworkReply::finished, this, [this, reply, requested, generation]() {
+ if (generation != m_chainIdentityProbeGeneration
+ || normalizedUrl(sequencerAddr()) != requested) {
+ if (generation == m_chainIdentityProbeGeneration)
+ m_identityProbeInFlight = false;
+ reply->deleteLater();
+ return;
+ }
+ m_identityProbeInFlight = false;
+
const bool gotHttpStatus =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
const bool reachable = gotHttpStatus || reply->error() == QNetworkReply::NoError;
if (sequencerReachable() != reachable)
setSequencerReachable(reachable);
+
+ const QByteArray payload = reply->readAll();
+ const ChainFingerprint fingerprint = chainFingerprintFromGetBlockResponse(payload);
+ if (!fingerprint.isValid()) {
+ qWarning() << "AmmUiBackend: could not read chain identity from" << requested;
+ setDeploymentIdentityPendingIfNeeded(false);
+ reply->deleteLater();
+ return;
+ }
+
+ setDeploymentIdentityPendingIfNeeded(false);
+ selectDeploymentForChain(
+ requested, fingerprint.blockHash, fingerprint.blockSignature);
reply->deleteLater();
});
}
@@ -332,40 +1941,95 @@ void AmmUiBackend::persistStoragePath(const QString& path)
setStoragePath(toLocalPath(path));
}
+QJsonArray AmmUiBackend::listAccounts()
+{
+ const QJsonArray raw = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
+ QJsonArray accounts;
+
+ for (const QJsonValue& value : raw) {
+ QJsonObject account = value.isObject() ? value.toObject() : QJsonObject{};
+ if (!value.isObject())
+ account.insert(QString::fromLatin1(ACCOUNT_ID_KEY), value.toString());
+
+ const QString accountIdHex = account.value(QString::fromLatin1(ACCOUNT_ID_KEY)).toString();
+ const QString accountIdBase58 = m_logos->logos_execution_zone.account_id_to_base58(accountIdHex);
+ account.insert(QString::fromLatin1(DISPLAY_ACCOUNT_ID_KEY),
+ accountIdBase58.isEmpty() ? accountIdHex : accountIdBase58);
+ accounts.append(account);
+ }
+
+ return accounts;
+}
+
bool AmmUiBackend::changeSequencerAddr(QString url)
{
- const QString trimmed = url.trimmed();
- if (trimmed.isEmpty()) {
- qWarning() << "AmmUiBackend: refusing to set empty sequencer_addr";
+ QString validationError;
+ const QString validated = validatedSequencerUrl(url, &validationError);
+ if (validated.isEmpty()) {
+ qWarning() << "AmmUiBackend: refusing sequencer_addr:" << validationError;
return false;
}
- const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
-
+ QString pathError;
+ const QString cfg = validatedWalletFilePath(
+ configPath().isEmpty() ? defaultConfigPath() : configPath(),
+ defaultWalletHome(),
+ QStringLiteral("Config"),
+ &pathError);
+ const QString stg = validatedWalletFilePath(
+ storagePath().isEmpty() ? defaultStoragePath() : storagePath(),
+ defaultWalletHome(),
+ QStringLiteral("Storage"),
+ &pathError);
+ if (!pathError.isEmpty()) {
+ qWarning() << "AmmUiBackend: refusing wallet path:" << pathError;
+ return false;
+ }
// Preserve the other config fields (poll timeouts, retries) — only swap the
// endpoint. The wallet reads this file on open via from_path_or_initialize_default.
QJsonObject obj;
+ QByteArray oldConfigBytes;
+ const bool oldConfigExists = QFileInfo::exists(cfg);
QFile in(cfg);
- if (in.open(QIODevice::ReadOnly)) {
- obj = QJsonDocument::fromJson(in.readAll()).object();
- in.close();
- }
- obj.insert(QStringLiteral("sequencer_addr"), trimmed);
+ if (oldConfigExists) {
+ if (!in.open(QIODevice::ReadOnly)) {
+ qWarning() << "AmmUiBackend: cannot read wallet config" << cfg;
+ return false;
+ }
- QFile out(cfg);
- if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
- qWarning() << "AmmUiBackend: cannot write wallet config" << cfg;
+ oldConfigBytes = in.readAll();
+ QJsonParseError parseError;
+ const QJsonDocument doc = QJsonDocument::fromJson(oldConfigBytes, &parseError);
+ in.close();
+ if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
+ qWarning() << "AmmUiBackend: invalid wallet config" << cfg
+ << parseError.errorString();
+ return false;
+ }
+ obj = doc.object();
+ }
+ obj.insert(QStringLiteral("sequencer_addr"), validated);
+ const QByteArray newConfigBytes = QJsonDocument(obj).toJson(QJsonDocument::Indented);
+
+ if (!writeFileAtomically(cfg, newConfigBytes)) {
+ qWarning() << "AmmUiBackend: cannot atomically write wallet config" << cfg;
return false;
}
- out.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
- out.close();
- // Re-open so the live wallet uses the new endpoint right away.
+ // Re-open from the final config path so later wallet saves keep using it.
if (isWalletOpen()) {
- const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
const int err = m_logos->logos_execution_zone.open(cfg, stg);
if (err != WALLET_FFI_SUCCESS) {
- qWarning() << "AmmUiBackend: reopen after sequencer change failed, code" << err;
+ qWarning() << "AmmUiBackend: final reopen after sequencer change failed, code" << err;
+ if (!restoreFile(cfg, oldConfigBytes, oldConfigExists)) {
+ qWarning() << "AmmUiBackend: rollback after sequencer change failed";
+ return false;
+ }
+ const int restoreErr = m_logos->logos_execution_zone.open(cfg, stg);
+ if (restoreErr != WALLET_FFI_SUCCESS) {
+ qWarning() << "AmmUiBackend: rollback reopen after sequencer change failed, code"
+ << restoreErr;
+ }
return false;
}
refreshSequencerAddr();
@@ -373,9 +2037,3 @@ bool AmmUiBackend::changeSequencerAddr(QString url)
}
return true;
}
-
-void AmmUiBackend::copyToClipboard(QString text)
-{
- if (QGuiApplication::clipboard())
- QGuiApplication::clipboard()->setText(text);
-}
diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h
index bd6feac..d3a54c7 100644
--- a/apps/amm/src/AmmUiBackend.h
+++ b/apps/amm/src/AmmUiBackend.h
@@ -2,7 +2,11 @@
#define AMM_UI_BACKEND_H
#include
+#include
+#include
#include
+#include
+#include
#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;
diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep
index ef3297a..b3d6b5a 100644
--- a/apps/amm/src/AmmUiBackend.rep
+++ b/apps/amm/src/AmmUiBackend.rep
@@ -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))
}