From dfd19a97964c5ca18f954a269a90da9b48c19c4d Mon Sep 17 00:00:00 2001
From: Ricardo Guilherme Schmidt <3esmit@gmail.com>
Date: Wed, 15 Jul 2026 11:55:39 -0300
Subject: [PATCH 01/10] feat(wallet): add reusable wallet modules
Add program-neutral wallet access, a stable account model, reusable QML controls, transaction confirmation, submitted-transaction presentation, and isolated contract tests.
---
apps/amm/CMakeLists.txt | 19 +-
apps/amm/flake.lock | 41 +-
apps/amm/flake.nix | 43 +-
apps/amm/qml/Logos/Wallet/qmldir | 5 +
apps/amm/qml/NavBar.qml | 11 +-
.../liquidity/LiquidityConfirmationDialog.qml | 238 ---------
.../LiquidityConfirmationSummary.qml | 91 ++++
.../swap/SwapConfirmationDialog.qml | 225 --------
.../swap/SwapConfirmationSummary.qml | 88 +++
.../qml/components/wallet/AccountControl.qml | 490 -----------------
.../qml/components/wallet/AccountDelegate.qml | 84 ---
.../components/wallet/CreateAccountDialog.qml | 102 ----
.../components/wallet/CreateWalletDialog.qml | 201 -------
.../qml/components/wallet/LogosCopyButton.qml | 39 --
.../components/wallet/WalletIconButton.qml | 25 -
.../qml/components/wallet/icons/settings.svg | 1 -
apps/amm/qml/pages/LiquidityPage.qml | 15 +-
apps/amm/qml/pages/SwapPage.qml | 42 +-
apps/amm/src/AccountModel.cpp | 79 ---
apps/amm/src/AccountModel.h | 47 --
apps/amm/src/AmmUiBackend.cpp | 375 ++-----------
apps/amm/src/AmmUiBackend.h | 58 +-
apps/amm/src/AmmUiBackend.rep | 7 -
apps/shared/wallet/CMakeLists.txt | 168 ++++++
.../wallet/qml/SubmittedTransaction.qml | 86 +++
.../qml/TransactionConfirmationDialog.qml | 169 ++++++
apps/shared/wallet/qml/WalletControl.qml | 503 ++++++++++++++++++
.../wallet/qml/internal/AccountDelegate.qml | 77 +++
.../shared/wallet/qml/internal/CopyButton.qml | 26 +
.../qml/internal/CreateAccountDialog.qml | 94 ++++
.../qml/internal/CreateWalletDialog.qml | 190 +++++++
.../wallet/qml/internal/WalletIconButton.qml | 31 ++
.../qml/internal/WalletMessageDialog.qml | 52 ++
.../wallet/qml/internal}/icons/account.svg | 0
.../wallet/qml/internal}/icons/back.svg | 0
.../wallet/qml/internal}/icons/checkmark.svg | 0
.../wallet/qml/internal}/icons/copy.svg | 0
.../wallet/qml/internal}/icons/power.svg | 0
.../shared/wallet/src/LogosWalletProvider.cpp | 382 +++++++++++++
apps/shared/wallet/src/LogosWalletProvider.h | 37 ++
apps/shared/wallet/src/WalletAccountModel.cpp | 61 +++
apps/shared/wallet/src/WalletAccountModel.h | 42 ++
apps/shared/wallet/src/WalletController.cpp | 238 +++++++++
apps/shared/wallet/src/WalletController.h | 67 +++
apps/shared/wallet/src/WalletProvider.cpp | 26 +
apps/shared/wallet/src/WalletProvider.h | 107 ++++
.../tests/cpp/LogosWalletProviderTest.cpp | 452 ++++++++++++++++
.../wallet/tests/cpp/fixtures/logos_sdk.h | 118 ++++
apps/shared/wallet/tests/qml/main.cpp | 3 +
.../tests/qml/tst_SubmittedTransaction.qml | 43 ++
.../qml/tst_TransactionConfirmationDialog.qml | 125 +++++
.../wallet/tests/qml/tst_WalletControl.qml | 339 ++++++++++++
.../wallet/tests/support/FakeWalletProvider.h | 75 +++
flake.nix | 21 +
54 files changed, 3899 insertions(+), 1959 deletions(-)
create mode 100644 apps/amm/qml/Logos/Wallet/qmldir
delete mode 100644 apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml
create mode 100644 apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml
delete mode 100644 apps/amm/qml/components/swap/SwapConfirmationDialog.qml
create mode 100644 apps/amm/qml/components/swap/SwapConfirmationSummary.qml
delete mode 100644 apps/amm/qml/components/wallet/AccountControl.qml
delete mode 100644 apps/amm/qml/components/wallet/AccountDelegate.qml
delete mode 100644 apps/amm/qml/components/wallet/CreateAccountDialog.qml
delete mode 100644 apps/amm/qml/components/wallet/CreateWalletDialog.qml
delete mode 100644 apps/amm/qml/components/wallet/LogosCopyButton.qml
delete mode 100644 apps/amm/qml/components/wallet/WalletIconButton.qml
delete mode 100644 apps/amm/qml/components/wallet/icons/settings.svg
delete mode 100644 apps/amm/src/AccountModel.cpp
delete mode 100644 apps/amm/src/AccountModel.h
create mode 100644 apps/shared/wallet/CMakeLists.txt
create mode 100644 apps/shared/wallet/qml/SubmittedTransaction.qml
create mode 100644 apps/shared/wallet/qml/TransactionConfirmationDialog.qml
create mode 100644 apps/shared/wallet/qml/WalletControl.qml
create mode 100644 apps/shared/wallet/qml/internal/AccountDelegate.qml
create mode 100644 apps/shared/wallet/qml/internal/CopyButton.qml
create mode 100644 apps/shared/wallet/qml/internal/CreateAccountDialog.qml
create mode 100644 apps/shared/wallet/qml/internal/CreateWalletDialog.qml
create mode 100644 apps/shared/wallet/qml/internal/WalletIconButton.qml
create mode 100644 apps/shared/wallet/qml/internal/WalletMessageDialog.qml
rename apps/{amm/qml/components/wallet => shared/wallet/qml/internal}/icons/account.svg (100%)
rename apps/{amm/qml/components/wallet => shared/wallet/qml/internal}/icons/back.svg (100%)
rename apps/{amm/qml/components/wallet => shared/wallet/qml/internal}/icons/checkmark.svg (100%)
rename apps/{amm/qml/components/wallet => shared/wallet/qml/internal}/icons/copy.svg (100%)
rename apps/{amm/qml/components/wallet => shared/wallet/qml/internal}/icons/power.svg (100%)
create mode 100644 apps/shared/wallet/src/LogosWalletProvider.cpp
create mode 100644 apps/shared/wallet/src/LogosWalletProvider.h
create mode 100644 apps/shared/wallet/src/WalletAccountModel.cpp
create mode 100644 apps/shared/wallet/src/WalletAccountModel.h
create mode 100644 apps/shared/wallet/src/WalletController.cpp
create mode 100644 apps/shared/wallet/src/WalletController.h
create mode 100644 apps/shared/wallet/src/WalletProvider.cpp
create mode 100644 apps/shared/wallet/src/WalletProvider.h
create mode 100644 apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp
create mode 100644 apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h
create mode 100644 apps/shared/wallet/tests/qml/main.cpp
create mode 100644 apps/shared/wallet/tests/qml/tst_SubmittedTransaction.qml
create mode 100644 apps/shared/wallet/tests/qml/tst_TransactionConfirmationDialog.qml
create mode 100644 apps/shared/wallet/tests/qml/tst_WalletControl.qml
create mode 100644 apps/shared/wallet/tests/support/FakeWalletProvider.h
diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt
index 40f11ac7..8d66e29f 100644
--- a/apps/amm/CMakeLists.txt
+++ b/apps/amm/CMakeLists.txt
@@ -1,12 +1,25 @@
-cmake_minimum_required(VERSION 3.14)
+cmake_minimum_required(VERSION 3.21)
project(AmmUiPlugin LANGUAGES CXX)
+find_package(Qt6 6.8 REQUIRED COMPONENTS Core Gui Network Qml Quick QuickControls2)
+qt_standard_project_setup(REQUIRES 6.8)
+
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else()
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
endif()
+set(LOGOS_WALLET_SOURCE_DIR
+ "${CMAKE_CURRENT_SOURCE_DIR}/../shared/wallet"
+ CACHE PATH "Path to the shared Logos wallet module"
+)
+set(LOGOS_WALLET_GENERATED_DIR
+ "${CMAKE_CURRENT_SOURCE_DIR}/generated_code"
+ CACHE PATH "Path to generated Logos SDK sources"
+)
+add_subdirectory("${LOGOS_WALLET_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/shared-wallet")
+
# ui_qml module with a hand-written C++ backend (QtRO .rep view contract +
# generated *SimpleSource/*ViewPluginBase). Mirrors the LEZ wallet UI module.
logos_module(
@@ -18,8 +31,6 @@ logos_module(
src/AmmUiPlugin.cpp
src/AmmUiBackend.h
src/AmmUiBackend.cpp
- src/AccountModel.h
- src/AccountModel.cpp
FIND_PACKAGES
Qt6Gui
Qt6Network
@@ -28,4 +39,6 @@ logos_module(
Qt6::Network
EXTERNAL_LIBS
amm_client_ffi
+ LINK_TARGETS
+ logos_wallet_access
)
diff --git a/apps/amm/flake.lock b/apps/amm/flake.lock
index 9bc7175f..8f79e67e 100644
--- a/apps/amm/flake.lock
+++ b/apps/amm/flake.lock
@@ -2238,17 +2238,17 @@
"rust-rapidsnark": "rust-rapidsnark"
},
"locked": {
- "lastModified": 1782310268,
- "narHash": "sha256-tIIIO+V+w/C/KLtKnLIv8O8/GoJ4PzgJSWrlQCXJ2P4=",
+ "lastModified": 1784202021,
+ "narHash": "sha256-BG94Ob5tbJyw7GclNBdnOWqcBLRld8sc+xeOU8LI5RQ=",
"owner": "logos-blockchain",
- "repo": "lssa",
- "rev": "fb8cbac40e0bda4f152415ff4f181cdc6bca6d4a",
+ "repo": "logos-execution-zone",
+ "rev": "a7e06a660940a00093b1760560d37ff84aff5a05",
"type": "github"
},
"original": {
"owner": "logos-blockchain",
- "repo": "lssa",
- "rev": "fb8cbac40e0bda4f152415ff4f181cdc6bca6d4a",
+ "repo": "logos-execution-zone",
+ "rev": "a7e06a660940a00093b1760560d37ff84aff5a05",
"type": "github"
}
},
@@ -14438,11 +14438,11 @@
]
},
"locked": {
- "lastModified": 1782082589,
- "narHash": "sha256-Qeqxp0HYb3oTpmfD5YlFPJzpAJa7Ilb9o4sMeVvmHRI=",
+ "lastModified": 1782920676,
+ "narHash": "sha256-Vb81kiYbi8yYDZbUSW6v7QhV6uO/pj7F+lCAW1coAsY=",
"owner": "logos-co",
"repo": "logos-protocol",
- "rev": "315a3a2e0af61bc47aad5601ee44cd7689975820",
+ "rev": "d7ad26d369c4e464a99f2a357f10c5947c7174e1",
"type": "github"
},
"original": {
@@ -16410,17 +16410,17 @@
"nix-bundle-lgx": "nix-bundle-lgx_28"
},
"locked": {
- "lastModified": 1782313954,
- "narHash": "sha256-psh5EtcIZh6kzFD4pQDT8bae9JS9eVtk5nPWxR2aGSA=",
+ "lastModified": 1782741385,
+ "narHash": "sha256-kN6xb1b/Jvu9Ninaqtmi/C4fx8poisGYbThIBIXwvHw=",
"owner": "logos-blockchain",
"repo": "logos-execution-zone-module",
- "rev": "d2e9400ac06c3cdbfc2405b4f153fff9841a453c",
+ "rev": "d70225ced646934d2294fd9e8f8b03615c104b80",
"type": "github"
},
"original": {
"owner": "logos-blockchain",
"repo": "logos-execution-zone-module",
- "rev": "d2e9400ac06c3cdbfc2405b4f153fff9841a453c",
+ "rev": "d70225ced646934d2294fd9e8f8b03615c104b80",
"type": "github"
}
},
@@ -26763,7 +26763,8 @@
"root": {
"inputs": {
"logos-module-builder": "logos-module-builder",
- "logos_execution_zone": "logos_execution_zone"
+ "logos_execution_zone": "logos_execution_zone",
+ "shared_wallet": "shared_wallet"
}
},
"rust-overlay": {
@@ -26849,6 +26850,18 @@
"rev": "e91187f8ccb5bbfc7bb00dac88169112428da78f",
"type": "github"
}
+ },
+ "shared_wallet": {
+ "flake": false,
+ "locked": {
+ "path": "../shared/wallet",
+ "type": "path"
+ },
+ "original": {
+ "path": "../shared/wallet",
+ "type": "path"
+ },
+ "parent": []
}
},
"root": "root",
diff --git a/apps/amm/flake.nix b/apps/amm/flake.nix
index 1b36c1aa..44121c52 100644
--- a/apps/amm/flake.nix
+++ b/apps/amm/flake.nix
@@ -4,11 +4,28 @@
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
+ # Shared C++ wallet access and Logos.Wallet QML sources.
+ shared_wallet = {
+ url = "path:../shared/wallet";
+ flake = false;
+ };
+
# Core wallet module (the LEZ wallet FFI Qt plugin). The input name must
# match the metadata.json `dependencies` entry so the builder can resolve
- # it as a module dependency. This rev pins LEZ (lssa) at fb8cbac4, which
- # includes the macOS Metal-build fix, so no `--override-input` is needed.
- logos_execution_zone.url = "github:logos-blockchain/logos-execution-zone-module?rev=d2e9400ac06c3cdbfc2405b4f153fff9841a453c";
+ # it as a module dependency. This revision exposes generic transaction
+ # submission by deployed program ID.
+ logos_execution_zone = {
+ url = "github:logos-blockchain/logos-execution-zone-module?rev=d70225ced646934d2294fd9e8f8b03615c104b80";
+
+ # The module pins the monorepo at v0.2.0-rc6 (e37876a), which owns the
+ # xcrun wrapper in its flake.nix. Override that transitive input to the
+ # head of PR #629 (fix(macos): nuke xcrun cache) so the Metal/xcrun build
+ # works on macOS. Note: #629 branches off `dev`, so this also pulls dev's
+ # drift from rc6 — if the wallet_ffi ABI mismatches the module build, fall
+ # back to cherry-picking #629's one line onto e37876a and pin that commit.
+ inputs.logos-execution-zone.url =
+ "github:logos-blockchain/logos-execution-zone?rev=a7e06a660940a00093b1760560d37ff84aff5a05";
+ };
};
# NOTE: this flake is no longer built standalone. The amm_client_ffi crate
@@ -22,10 +39,28 @@
# as a named attribute (there is no bare `default`): run it from the repo root
# with `nix run .#amm-ui`, and build just the FFI crate with
# `nix build .#amm_client_ffi`.
- outputs = inputs@{ logos-module-builder, ... }:
+ outputs = inputs@{ logos-module-builder, shared_wallet, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
+ preConfigure = ''
+ cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${shared_wallet}")
+ '';
+ postInstall = ''
+ # The builder installs the view under lib/qml after this hook. Its
+ # import descriptor points back to this compiled shared QML module.
+ test -f ${./qml}/Logos/Wallet/qmldir
+
+ walletQmlDir="shared-wallet/qml/Logos/Wallet"
+ if [ ! -d "$walletQmlDir" ]; then
+ echo "Built Logos.Wallet QML module not found"
+ exit 1
+ fi
+ walletQmlInstallDir="$out/lib/Logos/Wallet"
+ mkdir -p "$walletQmlInstallDir"
+ cp -r "$walletQmlDir/." "$walletQmlInstallDir/"
+ test -f "$walletQmlInstallDir/qmldir"
+ '';
};
}
diff --git a/apps/amm/qml/Logos/Wallet/qmldir b/apps/amm/qml/Logos/Wallet/qmldir
new file mode 100644
index 00000000..866351b9
--- /dev/null
+++ b/apps/amm/qml/Logos/Wallet/qmldir
@@ -0,0 +1,5 @@
+module Logos.Wallet
+optional plugin logos_wallet_qmlplugin ../../../Logos/Wallet
+classname Logos_WalletPlugin
+prefer :/qt/qml/Logos/Wallet/
+depends QtQuick
diff --git a/apps/amm/qml/NavBar.qml b/apps/amm/qml/NavBar.qml
index f5b78570..3c06f910 100644
--- a/apps/amm/qml/NavBar.qml
+++ b/apps/amm/qml/NavBar.qml
@@ -2,8 +2,7 @@ import QtQuick 2.15
import QtQuick.Layouts 1.15
import Logos.Theme
-
-import "components/wallet"
+import Logos.Wallet
// Self-contained navigation bar — styling is independent of any view's theme.
// Use currentIndex to read the active tab; tabChanged(index) fires on selection.
@@ -94,11 +93,15 @@ Item {
}
// Wallet / account control on the far right.
- AccountControl {
+ WalletControl {
id: accountControl
Layout.leftMargin: 12
- backend: root.backend
+ wallet: root.backend
accountModel: root.accountModel
+ viewportWidth: root.width
+ watchCall: function(result, success, failure) {
+ logos.watch(result, success, failure)
+ }
}
}
}
diff --git a/apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml b/apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml
deleted file mode 100644
index 16b0dc66..00000000
--- a/apps/amm/qml/components/liquidity/LiquidityConfirmationDialog.qml
+++ /dev/null
@@ -1,238 +0,0 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
-
-FocusScope {
- id: root
-
- property var snapshot: ({})
- property bool open: false
- readonly property bool isAdd: root.snapshot.action === "add"
-
- signal canceled
- signal confirmed(var snapshot)
-
- visible: root.open
- z: 20
-
- Keys.onEscapePressed: root.cancel()
-
- function openWithSnapshot(nextSnapshot) {
- root.snapshot = nextSnapshot;
- root.open = true;
- root.forceActiveFocus();
- cancelButton.forceActiveFocus();
- }
-
- function cancel() {
- root.open = false;
- root.canceled();
- }
-
- function confirm() {
- const confirmedSnapshot = root.snapshot;
- root.open = false;
- root.confirmed(confirmedSnapshot);
- }
-
- Rectangle {
- anchors.fill: parent
- color: "#99000000"
-
- MouseArea {
- anchors.fill: parent
- }
- }
-
- Rectangle {
- id: panel
-
- anchors.centerIn: parent
- color: "#1D1D1D"
- implicitHeight: dialogContent.implicitHeight + 24
- radius: 8
- width: Math.max(0, Math.min(360, root.width - 32))
- border.color: "#343434"
- border.width: 1
-
- ColumnLayout {
- id: dialogContent
-
- anchors.fill: parent
- anchors.margins: 12
- spacing: 12
-
- Text {
- color: "#E7E1D8"
- font.bold: true
- font.pixelSize: 16
- text: root.isAdd ? qsTr("Confirm add liquidity") : qsTr("Confirm remove liquidity")
-
- Layout.fillWidth: true
- }
-
- ColumnLayout {
- spacing: 8
- visible: root.isAdd
-
- Layout.fillWidth: true
-
- SummaryRow {
- label: qsTr("Deposit %1").arg(root.snapshot.tokenA || "")
- value: root.snapshot.depositA || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Deposit %1").arg(root.snapshot.tokenB || "")
- value: root.snapshot.depositB || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Receive at least")
- value: root.snapshot.minLpReceived || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Current ratio")
- value: root.snapshot.currentRatio || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Fee tier")
- value: root.snapshot.feeTier || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Slippage tolerance")
- value: root.snapshot.slippageTolerance || ""
-
- Layout.fillWidth: true
- }
- }
-
- ColumnLayout {
- spacing: 8
- visible: !root.isAdd
-
- Layout.fillWidth: true
-
- SummaryRow {
- label: qsTr("Burn LP")
- value: qsTr("%1 (%2)").arg(root.snapshot.burnText || "").arg(root.snapshot.burnPercent || "")
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Receive at least %1").arg(root.snapshot.tokenA || "")
- value: root.snapshot.minTokenAReceived || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Receive at least %1").arg(root.snapshot.tokenB || "")
- value: root.snapshot.minTokenBReceived || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Slippage tolerance")
- value: root.snapshot.slippageTolerance || ""
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Post-removal share")
- value: root.snapshot.postRemovalShare || ""
-
- Layout.fillWidth: true
- }
- }
-
- RowLayout {
- spacing: 8
-
- Layout.fillWidth: true
-
- Button {
- id: cancelButton
-
- activeFocusOnTab: true
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("Cancel")
-
- Accessible.name: cancelButton.text
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onClicked: root.cancel()
-
- contentItem: Text {
- color: cancelButton.hovered || cancelButton.activeFocus ? "#151515" : "#E7E1D8"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 13
- horizontalAlignment: Text.AlignHCenter
- text: cancelButton.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: cancelButton.activeFocus ? "#F26A21" : "#343434"
- border.width: 1
- color: cancelButton.pressed ? "#343434" : cancelButton.hovered || cancelButton.activeFocus ? "#E7E1D8" : "#151515"
- radius: 6
- }
- }
-
- Button {
- id: confirmButton
-
- activeFocusOnTab: true
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("Confirm")
-
- Accessible.name: confirmButton.text
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onClicked: root.confirm()
-
- contentItem: Text {
- color: "#151515"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 13
- horizontalAlignment: Text.AlignHCenter
- text: confirmButton.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: "#F26A21"
- border.width: 1
- color: confirmButton.pressed ? "#D95C1E" : confirmButton.hovered || confirmButton.activeFocus ? "#FF8A3D" : "#F26A21"
- radius: 6
- }
- }
- }
- }
- }
-}
diff --git a/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml
new file mode 100644
index 00000000..68af40da
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml
@@ -0,0 +1,91 @@
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ property var snapshot: ({})
+ readonly property bool isAdd: root.snapshot.action === "add"
+
+ spacing: 8
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 8
+ visible: root.isAdd
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Deposit %1").arg(root.snapshot.tokenA || "")
+ value: root.snapshot.depositA || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Deposit %1").arg(root.snapshot.tokenB || "")
+ value: root.snapshot.depositB || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Receive at least")
+ value: root.snapshot.minLpReceived || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Current ratio")
+ value: root.snapshot.currentRatio || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Fee tier")
+ value: root.snapshot.feeTier || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Slippage tolerance")
+ value: root.snapshot.slippageTolerance || ""
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 8
+ visible: !root.isAdd
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Burn LP")
+ value: qsTr("%1 (%2)")
+ .arg(root.snapshot.burnText || "")
+ .arg(root.snapshot.burnPercent || "")
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Receive at least %1").arg(root.snapshot.tokenA || "")
+ value: root.snapshot.minTokenAReceived || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Receive at least %1").arg(root.snapshot.tokenB || "")
+ value: root.snapshot.minTokenBReceived || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Slippage tolerance")
+ value: root.snapshot.slippageTolerance || ""
+ }
+
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Post-removal share")
+ value: root.snapshot.postRemovalShare || ""
+ }
+ }
+}
diff --git a/apps/amm/qml/components/swap/SwapConfirmationDialog.qml b/apps/amm/qml/components/swap/SwapConfirmationDialog.qml
deleted file mode 100644
index 54f61b44..00000000
--- a/apps/amm/qml/components/swap/SwapConfirmationDialog.qml
+++ /dev/null
@@ -1,225 +0,0 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
-
-FocusScope {
- id: root
-
- property var theme
- property var snapshot: ({})
- property bool open: false
-
- signal canceled
- signal confirmed(var snapshot)
-
- visible: root.open
- z: 20
-
- Keys.onEscapePressed: root.cancel()
-
- function openWithSnapshot(nextSnapshot) {
- root.snapshot = nextSnapshot;
- root.open = true;
- root.forceActiveFocus();
- cancelButton.forceActiveFocus();
- }
-
- function cancel() {
- root.open = false;
- root.canceled();
- }
-
- function confirm() {
- const confirmedSnapshot = root.snapshot;
- root.open = false;
- root.confirmed(confirmedSnapshot);
- }
-
- Rectangle {
- anchors.fill: parent
- color: "#99000000"
-
- MouseArea {
- anchors.fill: parent
- }
- }
-
- Rectangle {
- id: panel
-
- anchors.centerIn: parent
- color: root.theme.colors.cardBg
- implicitHeight: dialogContent.implicitHeight + 32
- radius: 16
- width: Math.max(0, Math.min(380, root.width - 32))
- border.color: root.theme.colors.border
- border.width: 1
-
- MouseArea {
- anchors.fill: parent
- }
-
- ColumnLayout {
- id: dialogContent
-
- anchors.fill: parent
- anchors.margins: 16
- spacing: 14
-
- Text {
- color: root.theme.colors.textPrimary
- font.bold: true
- font.pixelSize: 17
- text: qsTr("Confirm swap")
- Layout.fillWidth: true
- }
-
- ColumnLayout {
- spacing: 10
- Layout.fillWidth: true
-
- Rectangle {
- Layout.fillWidth: true
- color: root.theme.colors.inputBg
- radius: 12
- implicitHeight: payColumn.implicitHeight + 24
-
- ColumnLayout {
- id: payColumn
- anchors.fill: parent
- anchors.margins: 12
- spacing: 4
-
- Text {
- text: qsTr("You pay")
- color: root.theme.colors.textSecondary
- font.pixelSize: 12
- Layout.fillWidth: true
- }
- Text {
- text: qsTr("%1 %2")
- .arg(root.snapshot.sellAmount || "")
- .arg(root.snapshot.sellToken || "")
- color: root.theme.colors.textPrimary
- font.bold: true
- font.pixelSize: 18
- elide: Text.ElideRight
- Layout.fillWidth: true
- }
- }
- }
-
- Rectangle {
- Layout.fillWidth: true
- color: root.theme.colors.inputBg
- radius: 12
- implicitHeight: receiveColumn.implicitHeight + 24
-
- ColumnLayout {
- id: receiveColumn
- anchors.fill: parent
- anchors.margins: 12
- spacing: 4
-
- Text {
- text: qsTr("You receive at least")
- color: root.theme.colors.textSecondary
- font.pixelSize: 12
- Layout.fillWidth: true
- }
- Text {
- text: qsTr("%1 %2")
- .arg(root.snapshot.minReceived || "")
- .arg(root.snapshot.buyToken || "")
- color: root.theme.colors.textPrimary
- font.bold: true
- font.pixelSize: 18
- elide: Text.ElideRight
- Layout.fillWidth: true
- }
- }
- }
- }
-
- SwapSummary {
- Layout.fillWidth: true
- theme: root.theme
- swapModeText: root.snapshot.swapModeText || ""
- feeText: root.snapshot.feeAmount || ""
- priceImpactText: root.snapshot.priceImpactPercent || ""
- priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0
- slippageText: root.snapshot.slippageTolerance || ""
- minReceivedText: qsTr("%1 %2")
- .arg(root.snapshot.minReceived || "")
- .arg(root.snapshot.buyToken || "")
- }
-
- RowLayout {
- spacing: 10
- Layout.fillWidth: true
- Layout.topMargin: 4
-
- Button {
- id: cancelButton
- activeFocusOnTab: true
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("Cancel")
- Layout.fillWidth: true
- Layout.minimumHeight: 48
- onClicked: root.cancel()
-
- contentItem: Text {
- color: root.theme.colors.textPrimary
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 14
- horizontalAlignment: Text.AlignHCenter
- text: cancelButton.text
- verticalAlignment: Text.AlignVCenter
- }
- background: Rectangle {
- border.color: root.theme.colors.borderStrong
- border.width: 1
- color: cancelButton.pressed
- ? root.theme.colors.panelHoverBg
- : cancelButton.hovered || cancelButton.activeFocus
- ? root.theme.colors.panelBg
- : "transparent"
- radius: 14
- }
- }
-
- Button {
- id: confirmButton
- activeFocusOnTab: true
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("Confirm Swap")
- Layout.fillWidth: true
- Layout.minimumHeight: 48
- onClicked: root.confirm()
-
- contentItem: Text {
- color: "#ffffff"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 14
- horizontalAlignment: Text.AlignHCenter
- text: confirmButton.text
- verticalAlignment: Text.AlignVCenter
- }
- background: Rectangle {
- border.width: 0
- color: confirmButton.pressed
- ? "#D95C1E"
- : confirmButton.hovered || confirmButton.activeFocus
- ? root.theme.colors.ctaHoverBg
- : root.theme.colors.ctaBg
- radius: 14
- }
- }
- }
- }
- }
-}
diff --git a/apps/amm/qml/components/swap/SwapConfirmationSummary.qml b/apps/amm/qml/components/swap/SwapConfirmationSummary.qml
new file mode 100644
index 00000000..c3ff22e4
--- /dev/null
+++ b/apps/amm/qml/components/swap/SwapConfirmationSummary.qml
@@ -0,0 +1,88 @@
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ property var theme
+ property var snapshot: ({})
+
+ spacing: 10
+
+ Rectangle {
+ Layout.fillWidth: true
+ color: root.theme.colors.inputBg
+ radius: 8
+ implicitHeight: payColumn.implicitHeight + 24
+
+ ColumnLayout {
+ id: payColumn
+ anchors.fill: parent
+ anchors.margins: 12
+ spacing: 4
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("You pay")
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 12
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("%1 %2")
+ .arg(root.snapshot.sellAmount || "")
+ .arg(root.snapshot.sellToken || "")
+ color: root.theme.colors.textPrimary
+ font.bold: true
+ font.pixelSize: 18
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ color: root.theme.colors.inputBg
+ radius: 8
+ implicitHeight: receiveColumn.implicitHeight + 24
+
+ ColumnLayout {
+ id: receiveColumn
+ anchors.fill: parent
+ anchors.margins: 12
+ spacing: 4
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("You receive at least")
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 12
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("%1 %2")
+ .arg(root.snapshot.minReceived || "")
+ .arg(root.snapshot.buyToken || "")
+ color: root.theme.colors.textPrimary
+ font.bold: true
+ font.pixelSize: 18
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ SwapSummary {
+ Layout.fillWidth: true
+ theme: root.theme
+ swapModeText: root.snapshot.swapModeText || ""
+ feeText: root.snapshot.feeAmount || ""
+ priceImpactText: root.snapshot.priceImpactPercent || ""
+ priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0
+ slippageText: root.snapshot.slippageTolerance || ""
+ minReceivedText: qsTr("%1 %2")
+ .arg(root.snapshot.minReceived || "")
+ .arg(root.snapshot.buyToken || "")
+ }
+}
diff --git a/apps/amm/qml/components/wallet/AccountControl.qml b/apps/amm/qml/components/wallet/AccountControl.qml
deleted file mode 100644
index 9956eebd..00000000
--- a/apps/amm/qml/components/wallet/AccountControl.qml
+++ /dev/null
@@ -1,490 +0,0 @@
-import QtQuick
-import QtQml
-import QtQuick.Controls
-import QtQuick.Layouts
-
-import Logos.Theme
-import Logos.Controls
-
-// Header wallet control (Uniswap-style), with two states:
-// - not connected → a "Connect" button that opens the create-wallet modal
-// - connected → a single button showing the active account address;
-// clicking it opens a popup (top-right, just under the
-// button) holding the account selector, create-account and
-// disconnect actions.
-// The selected account address is exposed via selectedAddress for the
-// trade/liquidity flows to use as the "from" account.
-Item {
- id: root
-
- // Backend replica (logos.module("amm_ui")) and its account model.
- property var backend: null
- property var accountModel: null
-
- readonly property bool connected: backend !== null && backend.isWalletOpen
-
- // Index of the active account. selectedAddress/selectedName are derived from
- // the model mirror below so they stay valid while the popup (and its list)
- // is closed.
- property int selectedIndex: 0
-
- // Non-visual mirror of the account model: realizes every row regardless of
- // popup visibility, so the active account is addressable by index at all
- // times (a ListView only realizes rows while it is shown).
- Instantiator {
- id: accounts
- model: root.accountModel
- delegate: QtObject {
- readonly property string address: model.address ?? ""
- readonly property string name: model.name ?? ""
- readonly property string balance: model.balance ?? ""
- readonly property bool isPublic: model.isPublic ?? false
- }
- }
-
- function entryAt(i) {
- return (i >= 0 && i < accounts.count) ? accounts.objectAt(i) : null
- }
-
- readonly property string selectedAddress: {
- const e = root.entryAt(root.selectedIndex)
- return e ? e.address : ""
- }
- readonly property string selectedName: {
- const e = root.entryAt(root.selectedIndex)
- return e ? e.name : ""
- }
- readonly property string selectedBalance: {
- const e = root.entryAt(root.selectedIndex)
- return e ? e.balance : ""
- }
- readonly property bool selectedIsPublic: {
- const e = root.entryAt(root.selectedIndex)
- return e ? e.isPublic : false
- }
-
- // Keep the selection within bounds as accounts are added/removed.
- function clampSelection() {
- if (accounts.count === 0) { root.selectedIndex = 0; return }
- if (root.selectedIndex < 0) root.selectedIndex = 0
- else if (root.selectedIndex >= accounts.count) root.selectedIndex = accounts.count - 1
- }
- Connections {
- target: root.accountModel
- ignoreUnknownSignals: true
- function onModelReset() { root.clampSelection() }
- function onRowsInserted() { root.clampSelection() }
- function onRowsRemoved() { root.clampSelection() }
- }
-
- // 0x123456…cdef style truncation for the connected button label.
- function truncated(addr) {
- if (!addr) return ""
- return addr.length > 13 ? (addr.substring(0, 6) + "…" + addr.substring(addr.length - 4)) : addr
- }
-
- // Copy on the QML/view side. Routing this through the backend would call
- // QGuiApplication::clipboard() in the (headless) module host process, which
- // has no clipboard — that call tears the backend down, dropping the wallet
- // connection. A hidden TextEdit copies via the GUI process that owns it.
- TextEdit { id: clipboardProxy; visible: false }
- function copyToClipboard(text) {
- if (!text) return
- clipboardProxy.text = text
- clipboardProxy.selectAll()
- clipboardProxy.copy()
- clipboardProxy.deselect()
- clipboardProxy.text = ""
- }
-
- implicitWidth: root.connected ? connectedButton.width : connectButton.width
- implicitHeight: 40
-
- // ── Disconnected: Connect ────────────────────────────────────────────
- LogosButton {
- id: connectButton
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- height: 40
- visible: !root.connected
- enabled: root.backend !== null
- text: qsTr("Connect")
- onClicked: {
- // Re-open an existing wallet; only show the create modal on first run.
- if (root.backend && root.backend.walletExists)
- logos.watch(root.backend.openExisting(),
- function(ok) { if (!ok) console.warn("openExisting failed") },
- function(error) { console.warn("openExisting error:", error) })
- else
- createWalletDialog.open()
- }
- }
-
- // ── Connected: address pill that toggles the wallet menu ─────────────
- Rectangle {
- id: connectedButton
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- visible: root.connected
- implicitHeight: 40
- implicitWidth: connectedRow.implicitWidth + Theme.spacing.medium * 2
- radius: height / 2
- // Keep an opaque dark fill in both states: the navbar is white, and the
- // active "muted" fill is translucent gray, which renders light over white
- // and makes the white label unreadable. Signal "open" with an accent
- // border instead.
- color: Theme.palette.backgroundSecondary
- border.width: 1
- border.color: walletMenu.opened ? Theme.palette.overlayOrange : "transparent"
-
- RowLayout {
- id: connectedRow
- anchors.centerIn: parent
- spacing: Theme.spacing.small
-
- Rectangle {
- Layout.preferredWidth: 8
- Layout.preferredHeight: 8
- radius: 4
- color: "#39c06a"
- }
- LogosText {
- text: root.truncated(root.selectedAddress) || qsTr("Connected")
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.text
- }
- LogosText {
- text: walletMenu.opened ? "▴" : "▾"
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- }
- }
-
- MouseArea {
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- // CloseOnPressOutside already dismisses the popup on this same press
- // (the button is outside it), so `opened` is false by the time this
- // fires. Without the recency guard the dismissing click would just
- // reopen it. If it just closed, leave it closed.
- onClicked: {
- if (walletMenu.opened || (Date.now() - walletMenu.lastClosedMs) < 200)
- walletMenu.close()
- else
- walletMenu.open()
- }
- }
- }
-
- // ── Wallet menu popup (top-right, under the connected button) ─────────
- Popup {
- id: walletMenu
- parent: connectedButton
- y: connectedButton.height + Theme.spacing.small
- x: connectedButton.width - width // right-align under the button
- width: 360
- padding: Theme.spacing.medium
- closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
-
- // Timestamp of the last dismissal, used by the toggle button to tell a
- // genuine "open" click from the press that just closed the popup.
- property real lastClosedMs: 0
- onClosed: {
- walletMenu.lastClosedMs = Date.now()
- // Always reopen on the main (selected-account) view.
- if (viewStack.depth > 1)
- viewStack.pop(null, StackView.Immediate)
- }
-
- background: Rectangle {
- color: Theme.palette.backgroundTertiary
- border.width: 1
- border.color: Theme.palette.backgroundElevated
- radius: Theme.spacing.radiusLarge
- }
-
- // Two stacked views: the main view (active account + actions) and the
- // accounts view (full list + create). The popup height follows the
- // active view's natural height, animated so the resize isn't abrupt.
- contentItem: StackView {
- id: viewStack
- clip: true
- implicitWidth: walletMenu.availableWidth
- implicitHeight: currentItem ? currentItem.implicitHeight : 0
- initialItem: mainView
-
- Behavior on implicitHeight {
- NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
- }
-
- pushEnter: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 120 } }
- pushExit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 120 } }
- popEnter: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 120 } }
- popExit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 120 } }
- }
-
- // ── Main view: account icon + power icon, then the active account ──
- Component {
- id: mainView
-
- ColumnLayout {
- spacing: Theme.spacing.medium
-
- // Top-right actions: open the account list / disconnect.
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacing.small
-
- Item { Layout.fillWidth: true }
-
- WalletIconButton {
- iconSource: Qt.resolvedUrl("icons/account.svg")
- onClicked: viewStack.push(accountsView)
- }
- WalletIconButton {
- iconSource: Qt.resolvedUrl("icons/settings.svg")
- onClicked: viewStack.push(settingsView)
- }
- WalletIconButton {
- iconSource: Qt.resolvedUrl("icons/power.svg")
- onClicked: {
- walletMenu.close()
- if (root.backend) root.backend.disconnectWallet()
- }
- }
- }
-
- // Active account card.
- Rectangle {
- Layout.fillWidth: true
- Layout.preferredHeight: cardColumn.implicitHeight + Theme.spacing.medium * 2
- radius: Theme.spacing.radiusLarge
- color: Theme.palette.backgroundMuted
-
- ColumnLayout {
- id: cardColumn
- anchors.fill: parent
- anchors.margins: Theme.spacing.medium
- spacing: Theme.spacing.small
-
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacing.small
-
- LogosText {
- text: root.selectedName
- font.pixelSize: Theme.typography.secondaryText
- font.bold: true
- }
- Rectangle {
- Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
- Layout.preferredHeight: tagLabel.implicitHeight + 4
- radius: 4
- color: Theme.palette.backgroundSecondary
- LogosText {
- id: tagLabel
- anchors.centerIn: parent
- text: root.selectedIsPublic ? qsTr("Public") : qsTr("Private")
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- }
- }
- Item { Layout.fillWidth: true }
- LogosText {
- text: root.selectedBalance.length > 0 ? root.selectedBalance : "—"
- font.bold: true
- }
- }
-
- RowLayout {
- Layout.fillWidth: true
- spacing: 0
- LogosText {
- Layout.fillWidth: true
- verticalAlignment: Text.AlignVCenter
- text: root.selectedAddress
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textMuted
- elide: Text.ElideMiddle
- }
- LogosCopyButton {
- Layout.preferredHeight: 40
- Layout.preferredWidth: 40
- visible: root.selectedAddress.length > 0
- onCopyText: root.copyToClipboard(root.selectedAddress)
- icon.color: Theme.palette.textMuted
- }
- }
- }
- }
- }
- }
-
- // ── Accounts view: back + full list + create ──────────────────────
- Component {
- id: accountsView
-
- ColumnLayout {
- spacing: Theme.spacing.medium
-
- // Header: back to the main view + title.
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacing.small
-
- WalletIconButton {
- iconSource: Qt.resolvedUrl("icons/back.svg")
- onClicked: viewStack.pop()
- }
- LogosText {
- Layout.fillWidth: true
- text: qsTr("Accounts")
- font.bold: true
- color: Theme.palette.text
- }
- }
-
- // Account list: tap a row to make it the active account, then
- // return to the main view so the selection is reflected.
- ListView {
- Layout.fillWidth: true
- Layout.preferredHeight: Math.min(contentHeight, 260)
- clip: true
- model: root.accountModel
- spacing: Theme.spacing.small
- ScrollIndicator.vertical: ScrollIndicator { }
-
- delegate: AccountDelegate {
- width: ListView.view.width
- highlighted: index === root.selectedIndex
- onClicked: {
- root.selectedIndex = index
- viewStack.pop()
- }
- onCopyRequested: (text) => root.copyToClipboard(text)
- }
- }
-
- LogosButton {
- Layout.fillWidth: true
- height: 40
- text: qsTr("Add")
- // Leave the wallet menu open behind the (modal) dialog.
- onClicked: createAccountDialog.open()
- }
- }
- }
-
- // ── Settings view: back + editable network (sequencer) ────────────
- Component {
- id: settingsView
-
- ColumnLayout {
- spacing: Theme.spacing.medium
-
- // Header: back to the main view + title.
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacing.small
-
- WalletIconButton {
- iconSource: Qt.resolvedUrl("icons/back.svg")
- onClicked: viewStack.pop()
- }
- LogosText {
- Layout.fillWidth: true
- text: qsTr("Settings")
- font.bold: true
- color: Theme.palette.text
- }
- }
-
- LogosText {
- text: qsTr("Network (sequencer URL)")
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- }
-
- LogosTextField {
- id: seqField
- Layout.fillWidth: true
- placeholderText: "http://127.0.0.1:3040"
- // Initialize from the live value without binding, so typing
- // isn't clobbered when sequencerAddr updates after a save.
- Component.onCompleted: text = root.backend ? root.backend.sequencerAddr : ""
- }
-
- LogosText {
- id: seqStatus
- Layout.fillWidth: true
- visible: text.length > 0
- wrapMode: Text.WordWrap
- font.pixelSize: Theme.typography.secondaryText
- property bool ok: false
- color: ok ? Theme.palette.success : Theme.palette.error
- }
-
- LogosButton {
- Layout.fillWidth: true
- height: 40
- text: qsTr("Save")
- onClicked: {
- if (!root.backend) return
- seqStatus.text = ""
- logos.watch(root.backend.changeSequencerAddr(seqField.text),
- function(ok) {
- seqStatus.ok = ok
- seqStatus.text = ok ? qsTr("Network updated.")
- : qsTr("Failed to update network.")
- },
- function(error) {
- seqStatus.ok = false
- seqStatus.text = qsTr("Error: %1").arg(error)
- })
- }
- }
- }
- }
- }
-
- // ── Dialogs ──────────────────────────────────────────────────────────
- CreateWalletDialog {
- id: createWalletDialog
- walletHome: root.backend ? root.backend.walletHome : ""
- onCreateWallet: function(password) {
- if (!root.backend) return
- // createNewDefault returns the new wallet's seed phrase (empty on
- // failure). On success we hand it to the dialog, which switches to
- // its backup page — we do NOT close here, so the user can't skip it.
- logos.watch(root.backend.createNewDefault(password),
- function(mnemonic) {
- if (mnemonic && mnemonic.length > 0)
- createWalletDialog.mnemonic = mnemonic
- else
- createWalletDialog.createError = qsTr("Failed to create wallet. Please try again.")
- },
- function(error) {
- createWalletDialog.createError = qsTr("Error creating wallet: %1").arg(error)
- })
- }
- onCopyRequested: function(text) {
- if (root.backend) root.backend.copyToClipboard(text)
- }
- }
-
- CreateAccountDialog {
- id: createAccountDialog
- onCreatePublicRequested: {
- if (!root.backend) return
- logos.watch(root.backend.createAccountPublic(),
- function(_id) { /* model updates via NOTIFY after refresh */ },
- function(error) { console.warn("createAccountPublic failed:", error) })
- }
- onCreatePrivateRequested: {
- if (!root.backend) return
- logos.watch(root.backend.createAccountPrivate(),
- function(_id) { /* model updates via NOTIFY after refresh */ },
- function(error) { console.warn("createAccountPrivate failed:", error) })
- }
- }
-}
diff --git a/apps/amm/qml/components/wallet/AccountDelegate.qml b/apps/amm/qml/components/wallet/AccountDelegate.qml
deleted file mode 100644
index 14c03ee2..00000000
--- a/apps/amm/qml/components/wallet/AccountDelegate.qml
+++ /dev/null
@@ -1,84 +0,0 @@
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-import Logos.Theme
-import Logos.Controls
-
-// One account row in the account dropdown. Ported from the LEZ wallet UI.
-ItemDelegate {
- id: root
-
- // Emitted when the user clicks the copy icon; the parent view connects this
- // to its QML-side clipboard helper (AccountControl.copyToClipboard).
- signal copyRequested(string text)
-
- leftPadding: Theme.spacing.medium
- rightPadding: Theme.spacing.medium
- topPadding: Theme.spacing.medium
- bottomPadding: Theme.spacing.medium
-
- background: Rectangle {
- color: root.highlighted || root.hovered ?
- Theme.palette.backgroundMuted :
- Theme.palette.backgroundTertiary
- radius: Theme.spacing.radiusLarge
- }
-
- contentItem: ColumnLayout {
- spacing: Theme.spacing.small
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacing.small
-
- LogosText {
- text: model.name ?? ""
- font.pixelSize: Theme.typography.secondaryText
- font.bold: true
- }
-
- Rectangle {
- Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
- Layout.preferredHeight: tagLabel.implicitHeight + 4
- radius: 4
- color: Theme.palette.backgroundSecondary
-
- LogosText {
- id: tagLabel
- anchors.centerIn: parent
- text: model.isPublic ? qsTr("Public") : qsTr("Private")
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- }
- }
-
- Item { Layout.fillWidth: true }
-
- LogosText {
- text: model.balance && model.balance.length > 0 ? model.balance : "—"
- font.bold: true
- }
- }
-
- RowLayout {
- Layout.fillWidth: true
- spacing: 0
- LogosText {
- id: addressLabel
- Layout.fillWidth: true
- verticalAlignment: Text.AlignVCenter
- text: model.address ?? ""
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textMuted
- elide: Text.ElideMiddle
- }
- LogosCopyButton {
- Layout.preferredHeight: 40
- Layout.preferredWidth: 40
- onCopyText: root.copyRequested(model.address)
- visible: addressLabel.text
- icon.color: Theme.palette.textMuted
- }
- }
- }
-}
diff --git a/apps/amm/qml/components/wallet/CreateAccountDialog.qml b/apps/amm/qml/components/wallet/CreateAccountDialog.qml
deleted file mode 100644
index 80707958..00000000
--- a/apps/amm/qml/components/wallet/CreateAccountDialog.qml
+++ /dev/null
@@ -1,102 +0,0 @@
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-import Logos.Theme
-import Logos.Controls
-
-// Public/private account creation dialog. Ported from the LEZ wallet UI.
-Popup {
- id: root
-
- signal createPublicRequested()
- signal createPrivateRequested()
-
- modal: true
- dim: true
- padding: Theme.spacing.large
- closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
-
- // Center on the full-window overlay rather than the small navbar control
- // this popup is declared inside.
- parent: Overlay.overlay
- anchors.centerIn: parent
- width: 360
-
- background: Rectangle {
- color: Theme.palette.backgroundSecondary
- radius: Theme.spacing.radiusXlarge
- border.color: Theme.palette.backgroundElevated
- }
-
- contentItem: ColumnLayout {
- id: contentLayout
- // Pin to the popup's padded width so children stay within the modal.
- width: root.availableWidth
- spacing: Theme.spacing.large
-
- LogosText {
- text: qsTr("Create account")
- font.pixelSize: Theme.typography.titleText
- font.weight: Theme.typography.weightBold
- color: Theme.palette.text
- }
-
- LogosText {
- text: qsTr("Choose account type.")
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- Layout.topMargin: -Theme.spacing.small
- }
-
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacing.medium
-
- ColumnLayout {
- Layout.fillWidth: true
- spacing: 0
-
- LogosText {
- text: qsTr("Private")
- font.pixelSize: Theme.typography.primaryText
- color: Theme.palette.text
- }
- LogosText {
- text: qsTr("Private balance and activity.")
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- wrapMode: Text.WordWrap
- Layout.fillWidth: true
- }
- }
-
- LogosSwitch {
- id: privateSwitch
- checked: false
- }
- }
-
- RowLayout {
- Layout.topMargin: Theme.spacing.medium
- spacing: Theme.spacing.medium
- Layout.fillWidth: true
- LogosButton {
- text: qsTr("Cancel")
- Layout.fillWidth: true
- onClicked: root.close()
- }
- LogosButton {
- text: qsTr("Create")
- Layout.fillWidth: true
- onClicked: {
- if (privateSwitch.checked)
- root.createPrivateRequested()
- else
- root.createPublicRequested()
- root.close()
- }
- }
- }
- }
-}
diff --git a/apps/amm/qml/components/wallet/CreateWalletDialog.qml b/apps/amm/qml/components/wallet/CreateWalletDialog.qml
deleted file mode 100644
index 05c8d6e6..00000000
--- a/apps/amm/qml/components/wallet/CreateWalletDialog.qml
+++ /dev/null
@@ -1,201 +0,0 @@
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-import Logos.Theme
-import Logos.Controls
-
-// Wallet creation modal. Two pages, driven by whether a mnemonic exists yet:
-// 1. Password entry — emits createWallet(password); the parent creates the
-// wallet and, on success, sets `mnemonic` to the returned seed phrase.
-// 2. Seed-phrase backup — shows the mnemonic once and gates dismissal behind
-// an explicit acknowledgement. This is the only time the phrase is shown,
-// so the popup is not auto-dismissable while it is visible.
-// Storage/config live at the per-app default (backend.walletHome) — no path
-// picking. Opened from the navbar "Connect" button.
-Popup {
- id: root
-
- // Where the wallet will be stored, shown for transparency.
- property string walletHome: ""
- property string createError: ""
- // Set by the parent to the BIP39 seed phrase once creation succeeds. A
- // non-empty value flips the dialog to the backup page.
- property string mnemonic: ""
-
- signal createWallet(string password)
- signal copyRequested(string text)
-
- modal: true
- dim: true
- padding: Theme.spacing.large
- // Once the wallet exists we must not let the user dismiss the modal (and
- // lose the only view of their seed phrase) by clicking away or pressing Esc.
- closePolicy: root.mnemonic.length > 0
- ? Popup.NoAutoClose
- : (Popup.CloseOnEscape | Popup.CloseOnPressOutside)
- // Center on the full-window overlay rather than the small navbar control
- // this popup is declared inside.
- parent: Overlay.overlay
- anchors.centerIn: parent
- width: 380
-
- onOpened: {
- passwordField.text = ""
- confirmField.text = ""
- root.createError = ""
- root.mnemonic = ""
- passwordField.forceActiveFocus()
- }
-
- background: Rectangle {
- color: Theme.palette.backgroundSecondary
- radius: Theme.spacing.radiusXlarge
- border.color: Theme.palette.backgroundElevated
- }
-
- contentItem: ColumnLayout {
- // Pin to the popup's padded width so long text wraps and fillWidth
- // children don't push the layout wider than the modal.
- width: root.availableWidth
- spacing: 0
-
- // ── Page 1: password entry ────────────────────────────────────────
- ColumnLayout {
- id: passwordPage
- visible: root.mnemonic.length === 0
- Layout.fillWidth: true
- spacing: Theme.spacing.large
-
- LogosText {
- text: qsTr("Create your wallet")
- font.pixelSize: Theme.typography.titleText
- font.weight: Theme.typography.weightBold
- color: Theme.palette.text
- }
-
- LogosText {
- text: qsTr("Secure your wallet with a password. It will be stored on this device at %1.")
- .arg(root.walletHome || qsTr("the default location"))
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- wrapMode: Text.WordWrap
- Layout.fillWidth: true
- Layout.topMargin: -Theme.spacing.small
- }
-
- LogosTextField {
- id: passwordField
- Layout.fillWidth: true
- placeholderText: qsTr("Password")
- echoMode: TextInput.Password
- Keys.onReturnPressed: createButton.tryCreate()
- }
- LogosTextField {
- id: confirmField
- Layout.fillWidth: true
- placeholderText: qsTr("Confirm password")
- echoMode: TextInput.Password
- Keys.onReturnPressed: createButton.tryCreate()
- }
-
- LogosText {
- Layout.fillWidth: true
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.error
- wrapMode: Text.WordWrap
- visible: text.length > 0
- text: root.createError
- }
-
- RowLayout {
- Layout.topMargin: Theme.spacing.small
- Layout.fillWidth: true
- spacing: Theme.spacing.medium
- LogosButton {
- text: qsTr("Cancel")
- Layout.fillWidth: true
- onClicked: root.close()
- }
- LogosButton {
- id: createButton
- Layout.fillWidth: true
- text: qsTr("Create Wallet")
- function tryCreate() {
- if (passwordField.text.length === 0) {
- root.createError = qsTr("Password cannot be empty.")
- } else if (passwordField.text !== confirmField.text) {
- root.createError = qsTr("Passwords do not match.")
- } else {
- root.createError = ""
- root.createWallet(passwordField.text)
- }
- }
- onClicked: tryCreate()
- }
- }
- }
-
- // ── Page 2: seed-phrase backup ────────────────────────────────────
- ColumnLayout {
- id: backupPage
- visible: root.mnemonic.length > 0
- Layout.fillWidth: true
- spacing: Theme.spacing.large
-
- LogosText {
- text: qsTr("Back up your recovery phrase")
- font.pixelSize: Theme.typography.titleText
- font.weight: Theme.typography.weightBold
- color: Theme.palette.text
- }
-
- LogosText {
- text: qsTr("Write these words down in order and store them somewhere safe. Anyone with this phrase can control your wallet, and it will not be shown again — it is the only way to recover access.")
- font.pixelSize: Theme.typography.secondaryText
- color: Theme.palette.textSecondary
- wrapMode: Text.WordWrap
- Layout.fillWidth: true
- Layout.topMargin: -Theme.spacing.small
- }
-
- Rectangle {
- Layout.fillWidth: true
- radius: Theme.spacing.radiusLarge
- color: Theme.palette.backgroundElevated
- implicitHeight: phraseText.implicitHeight + 2 * Theme.spacing.medium
-
- LogosText {
- id: phraseText
- anchors.fill: parent
- anchors.margins: Theme.spacing.medium
- text: root.mnemonic
- wrapMode: Text.WordWrap
- lineHeight: 1.4
- font.pixelSize: Theme.typography.primaryText
- font.weight: Theme.typography.weightBold
- color: Theme.palette.text
- }
- }
-
- LogosButton {
- Layout.fillWidth: true
- text: qsTr("Copy to clipboard")
- onClicked: root.copyRequested(root.mnemonic)
- }
-
- LogosCheckbox {
- id: ackCheck
- Layout.fillWidth: true
- text: qsTr("I have safely backed up my recovery phrase")
- }
-
- LogosButton {
- Layout.fillWidth: true
- enabled: ackCheck.checked
- text: qsTr("Continue")
- onClicked: root.close()
- }
- }
- }
-}
diff --git a/apps/amm/qml/components/wallet/LogosCopyButton.qml b/apps/amm/qml/components/wallet/LogosCopyButton.qml
deleted file mode 100644
index 7c1d7bdc..00000000
--- a/apps/amm/qml/components/wallet/LogosCopyButton.qml
+++ /dev/null
@@ -1,39 +0,0 @@
-import QtQuick
-import QtQuick.Controls
-
-import Logos.Theme
-
-Button {
- id: root
-
- signal copyText()
-
- implicitWidth: 24
- implicitHeight: 24
- display: AbstractButton.IconOnly
- flat: true
-
- property string iconSource: Qt.resolvedUrl("icons/copy.svg")
-
- icon.source: root.iconSource
- icon.width: 24
- icon.height: 24
- icon.color: Theme.palette.textSecondary
-
- function reset() {
- iconSource = Qt.resolvedUrl("icons/copy.svg")
- }
-
- Timer {
- id: resetTimer
- interval: 1500
- repeat: false
- onTriggered: root.reset()
- }
-
- onClicked: {
- root.copyText()
- root.iconSource = Qt.resolvedUrl("icons/checkmark.svg")
- resetTimer.restart()
- }
-}
diff --git a/apps/amm/qml/components/wallet/WalletIconButton.qml b/apps/amm/qml/components/wallet/WalletIconButton.qml
deleted file mode 100644
index 61545e0d..00000000
--- a/apps/amm/qml/components/wallet/WalletIconButton.qml
+++ /dev/null
@@ -1,25 +0,0 @@
-import QtQuick
-import QtQuick.Controls
-
-import Logos.Theme
-
-// Small icon-only action button for the wallet menu. Uses the same Button +
-// icon.source/icon.color path as LogosCopyButton, which renders reliably here
-// (LogosIconButton's Image + MultiEffect shader path does not).
-Button {
- id: root
-
- property url iconSource
- property color iconColor: Theme.palette.textSecondary
- property int iconSize: 18
-
- implicitWidth: 32
- implicitHeight: 32
- display: AbstractButton.IconOnly
- flat: true
-
- icon.source: root.iconSource
- icon.width: root.iconSize
- icon.height: root.iconSize
- icon.color: root.iconColor
-}
diff --git a/apps/amm/qml/components/wallet/icons/settings.svg b/apps/amm/qml/components/wallet/icons/settings.svg
deleted file mode 100644
index 77f89ae5..00000000
--- a/apps/amm/qml/components/wallet/icons/settings.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml
index f4719521..9dde13a6 100644
--- a/apps/amm/qml/pages/LiquidityPage.qml
+++ b/apps/amm/qml/pages/LiquidityPage.qml
@@ -1,5 +1,6 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
+import Logos.Wallet
import "../components/shared"
import "../components/liquidity"
import "../state"
@@ -162,10 +163,18 @@ Item {
}
}
- LiquidityConfirmationDialog {
- id: confirmationDialog
+ Component {
+ id: liquidityConfirmationSummary
- anchors.fill: parent
+ LiquidityConfirmationSummary { }
+ }
+
+ TransactionConfirmationDialog {
+ id: confirmationDialog
+ title: snapshot.action === "add"
+ ? qsTr("Confirm add liquidity")
+ : qsTr("Confirm remove liquidity")
+ summary: liquidityConfirmationSummary
onConfirmed: function (snapshot) {
root.confirmLiquidityAction(snapshot);
diff --git a/apps/amm/qml/pages/SwapPage.qml b/apps/amm/qml/pages/SwapPage.qml
index 344d47a7..d524e527 100644
--- a/apps/amm/qml/pages/SwapPage.qml
+++ b/apps/amm/qml/pages/SwapPage.qml
@@ -1,9 +1,10 @@
+pragma ComponentBehavior: Bound
+
import QtQuick 2.15
-import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
+import Logos.Wallet
import "../components/shared"
import "../components/swap"
-import "../state"
Item {
id: root
@@ -25,7 +26,7 @@ Item {
}
QtObject {
- id: theme
+ id: pageTheme
property bool isDark: true
property var colors: isDark ? dark : light
@@ -68,7 +69,7 @@ Item {
Rectangle {
anchors.fill: parent
- color: theme.colors.background
+ color: pageTheme.colors.background
Behavior on color { ColorAnimation { duration: 300 } }
// Theme toggle
@@ -77,19 +78,19 @@ Item {
anchors.right: parent.right
anchors.margins: 16
width: 44; height: 24; radius: 12
- color: theme.colors.panelBg
- border.color: theme.colors.border
+ color: pageTheme.colors.panelBg
+ border.color: pageTheme.colors.border
border.width: 1
Text {
anchors.centerIn: parent
- text: theme.isDark ? "☀" : "☾"
+ text: pageTheme.isDark ? "☀" : "☾"
font.pixelSize: 13
- color: theme.colors.textSecondary
+ color: pageTheme.colors.textSecondary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
- onClicked: theme.isDark = !theme.isDark
+ onClicked: pageTheme.isDark = !pageTheme.isDark
}
}
@@ -100,7 +101,7 @@ Item {
SwapCard {
id: swapCard
Layout.alignment: Qt.AlignHCenter
- theme: theme
+ theme: pageTheme
tokens: root.tokens
backend: root.backend
width: Math.min(480, root.width - 32)
@@ -126,9 +127,9 @@ Item {
Text {
Layout.alignment: Qt.AlignHCenter
- text: "Buy and sell crypto on LEZ."
+ text: "Buy and sell crypto on LEZ."
textFormat: Text.RichText
- color: theme.colors.textSecondary
+ color: pageTheme.colors.textSecondary
font.pixelSize: 15
horizontalAlignment: Text.AlignHCenter
}
@@ -138,7 +139,7 @@ Item {
id: tokenModal
anchors.fill: parent
z: 10
- theme: theme
+ theme: pageTheme
tokens: root.tokens
property string targetSide: "sell"
@@ -161,10 +162,19 @@ Item {
}
}
- SwapConfirmationDialog {
+ Component {
+ id: swapConfirmationSummary
+
+ SwapConfirmationSummary {
+ theme: pageTheme
+ }
+ }
+
+ TransactionConfirmationDialog {
id: swapConfirmationDialog
- anchors.fill: parent
- theme: theme
+ title: qsTr("Confirm swap")
+ confirmText: qsTr("Confirm swap")
+ summary: swapConfirmationSummary
onConfirmed: function(snapshot) {
// The dialog only shows a preview snapshot; the actual
diff --git a/apps/amm/src/AccountModel.cpp b/apps/amm/src/AccountModel.cpp
deleted file mode 100644
index 63992f86..00000000
--- a/apps/amm/src/AccountModel.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-#include "AccountModel.h"
-
-#include
-
-AccountModel::AccountModel(QObject* parent)
- : QAbstractListModel(parent)
-{
-}
-
-int AccountModel::rowCount(const QModelIndex& parent) const
-{
- if (parent.isValid())
- return 0;
- return m_entries.size();
-}
-
-QVariant AccountModel::data(const QModelIndex& index, int role) const
-{
- if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size())
- return QVariant();
-
- const AccountEntry& e = m_entries.at(index.row());
- switch (role) {
- case NameRole: return e.name;
- case AddressRole: return e.address;
- case BalanceRole: return e.balance;
- case IsPublicRole: return e.isPublic;
- default: return QVariant();
- }
-}
-
-QHash AccountModel::roleNames() const
-{
- return {
- { NameRole, "name" },
- { AddressRole, "address" },
- { BalanceRole, "balance" },
- { IsPublicRole, "isPublic" }
- };
-}
-
-void AccountModel::replaceFromJsonArray(const QJsonArray& arr)
-{
- beginResetModel();
- const int oldCount = m_entries.size();
- m_entries.clear();
- int idx = 0;
- for (const QJsonValue& v : arr) {
- AccountEntry e;
- e.name = QStringLiteral("Account %1").arg(++idx);
- e.balance = QString();
- if (v.isObject()) {
- const QJsonObject obj = v.toObject();
- e.address = obj.value(QStringLiteral("account_id")).toString();
- e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true);
- } else {
- e.address = v.toString();
- e.isPublic = true;
- }
- m_entries.append(e);
- }
- endResetModel();
- if (oldCount != m_entries.size())
- emit countChanged();
-}
-
-void AccountModel::setBalanceByAddress(const QString& address, const QString& balance)
-{
- for (int i = 0; i < m_entries.size(); ++i) {
- if (m_entries.at(i).address == address) {
- if (m_entries.at(i).balance != balance) {
- m_entries[i].balance = balance;
- const QModelIndex idx = index(i, 0);
- emit dataChanged(idx, idx, { BalanceRole });
- }
- return;
- }
- }
-}
diff --git a/apps/amm/src/AccountModel.h b/apps/amm/src/AccountModel.h
deleted file mode 100644
index 918839c3..00000000
--- a/apps/amm/src/AccountModel.h
+++ /dev/null
@@ -1,47 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-
-// One wallet account row. Mirrors the shape returned by the core wallet
-// module's list_accounts() (account_id + is_public), with a display name and
-// a lazily-fetched balance.
-struct AccountEntry {
- QString name;
- QString address;
- QString balance;
- bool isPublic = true;
-};
-
-// QAbstractListModel of wallet accounts, exposed to QML via
-// logos.model("amm_ui", "accountModel"). Ported from the LEZ wallet UI.
-class AccountModel : public QAbstractListModel {
- Q_OBJECT
- Q_PROPERTY(int count READ count NOTIFY countChanged)
-public:
- enum Role {
- NameRole = Qt::UserRole + 1,
- AddressRole,
- BalanceRole,
- IsPublicRole
- };
- Q_ENUM(Role)
-
- explicit AccountModel(QObject* parent = nullptr);
-
- int rowCount(const QModelIndex& parent = QModelIndex()) const override;
- QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
- QHash roleNames() const override;
-
- void replaceFromJsonArray(const QJsonArray& arr);
- void setBalanceByAddress(const QString& address, const QString& balance);
- int count() const { return m_entries.size(); }
-
-signals:
- void countChanged();
-
-private:
- QVector m_entries;
-};
diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp
index f9eb89da..6b30cd13 100644
--- a/apps/amm/src/AmmUiBackend.cpp
+++ b/apps/amm/src/AmmUiBackend.cpp
@@ -20,6 +20,8 @@
#include
#include
+#include "LogosWalletProvider.h"
+#include "WalletController.h"
#include "logos_api.h"
#include "logos_sdk.h"
@@ -51,17 +53,6 @@ static bool ammDebugEnabled()
if (ammDebugEnabled()) qWarning().noquote()
namespace {
- const char SETTINGS_ORG[] = "Logos";
- const char SETTINGS_APP[] = "AmmUI";
- // Sticky "user pressed Disconnect" flag so the wallet stays locked across
- // relaunches until the user reconnects.
- const char DISCONNECTED_KEY[] = "disconnected";
- const int WALLET_FFI_SUCCESS = 0;
-
- // 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";
-
// Absolute path to the deployed AMM program's RISC Zero program binary
// (amm.bin — the `ProgramBinary` `.bin` from the docker guest build, decoded
// on the Rust side via `ProgramBinary::decode`; NOT a raw ELF — pointing at
@@ -77,13 +68,6 @@ namespace {
// doesn't need a hardcoded/dummy token list.
const char TOKENS_CONFIG_ENV[] = "TOKENS_CONFIG";
- // Normalise file:// URLs and OS paths to a plain local path.
- QString toLocalPath(const QString& path) {
- if (path.startsWith("file://") || path.contains("/"))
- return QUrl::fromUserInput(path).toLocalFile();
- return path;
- }
-
QString bytes32ToHex(const uint8_t (&b)[32]) {
return QString::fromLatin1(QByteArray(reinterpret_cast(b), 32).toHex());
}
@@ -157,367 +141,86 @@ namespace {
}
}
-QString AmmUiBackend::defaultWalletHome()
-{
- const QByteArray override = qgetenv(WALLET_HOME_ENV);
- if (!override.isEmpty())
- return QString::fromLocal8Bit(override);
- // LEZ's canonical wallet home, shared with the wallet UI and other LEZ apps
- // (matches lez/wallet get_home_default_path()).
- return QDir::homePath() + QStringLiteral("/.lee/wallet");
-}
-
-QString AmmUiBackend::defaultConfigPath() const
-{
- return defaultWalletHome() + QStringLiteral("/wallet_config.json");
-}
-
-QString AmmUiBackend::defaultStoragePath() const
-{
- return defaultWalletHome() + QStringLiteral("/storage.json");
-}
-
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
: AmmUiBackendSimpleSource(parent),
- m_accountModel(new AccountModel(this)),
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
- m_logos(new LogosModules(m_logosAPI)),
- m_net(new QNetworkAccessManager(this)),
- m_reachabilityTimer(new QTimer(this))
-{
- // PROP defaults via the generated setters.
- setIsWalletOpen(false);
- setLastSyncedBlock(0);
- setCurrentBlockHeight(0);
- setWalletHome(defaultWalletHome());
- // Assume reachable until a probe proves otherwise (avoids a startup flash).
- setSequencerReachable(true);
-
- // 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
- // QSettings anymore: a previously-persisted per-app path (~/.lee/amm-wallet)
- // would otherwise override the default and pin the app to the old keystore.
-
- // A wallet exists on disk if its storage file is present (drives whether
- // the navbar "Connect" reconnects or offers to create a wallet).
- const QString effectiveStorage = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
- setWalletExists(QFileInfo::exists(effectiveStorage));
-
- // ui-host runs our constructor inside initLogos(), synchronously, BEFORE
- // it enables remoting and emits READY. Any blocking RPC here would stall
- // ui-host startup past its ready watchdog. Defer the open+refresh chain to
- // the first event-loop tick so ui-host finishes wiring itself up first.
- QTimer::singleShot(0, this, [this]() { openOrAdoptWallet(); });
-
- // Save wallet on quit; host may not call destructors so this is best-effort.
- connect(qApp, &QCoreApplication::aboutToQuit, this,
- [this]() { saveWallet(); }, Qt::DirectConnection);
-}
-
-AmmUiBackend::~AmmUiBackend()
-{
- saveWallet();
- delete m_logos;
-}
-
-void AmmUiBackend::openOrAdoptWallet()
+ m_logos(std::make_unique(m_logosAPI)),
+ m_wallet(std::make_unique(m_logosAPI)),
+ m_walletController(std::make_unique(
+ *m_wallet, QStringLiteral("AmmUI")))
{
- // Respect an explicit user disconnect: stay locked, show "Connect".
- if (QSettings(SETTINGS_ORG, SETTINGS_APP).value(DISCONNECTED_KEY, false).toBool())
- return;
-
- // In Basecamp the logos_execution_zone module is a single shared instance,
- // so the wallet may already be open (e.g. opened by the dedicated wallet
- // app). Adopt that wallet instead of fighting over it: mirror its state
- // rather than re-opening from disk, which could clobber unsaved in-memory
- // accounts the other app holds. A freshly-created shared wallet can be open
- // with zero accounts, so we can't key off list_accounts() alone (see
- // sharedWalletIsOpen).
- if (sharedWalletIsOpen()) {
- const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
- qDebug() << "AmmUiBackend: adopting already-open shared wallet"
- << existing.size() << "accounts";
- setIsWalletOpen(true);
- m_accountModel->replaceFromJsonArray(existing);
- refreshBalances();
- refreshSequencerAddr();
- return;
- }
-
- // Standalone (own core instance): auto-open a previously-created wallet.
- // Use persisted paths if the user picked custom ones, else the per-app
- // default. Only open if the storage actually exists, otherwise stay closed
- // so QML shows the "Connect" entry point (no noisy FFI errors on first run).
- const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
- const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
- if (!QFileInfo::exists(stg))
- return; // No wallet yet — QML shows "Connect".
-
- qDebug() << "AmmUiBackend: opening wallet with config" << cfg << "storage" << stg;
- const int err = m_logos->logos_execution_zone.open(cfg, stg);
- if (err == WALLET_FFI_SUCCESS) {
- persistConfigPath(cfg);
- persistStoragePath(stg);
- setIsWalletOpen(true);
- refreshAccounts();
- refreshBlockHeights();
- refreshSequencerAddr();
- } else {
- qWarning() << "AmmUiBackend: wallet open failed, code" << err;
- }
+ connect(m_walletController.get(), &WalletController::stateChanged,
+ this, &AmmUiBackend::syncWalletState);
+ syncWalletState();
+ m_walletController->start();
}
-bool AmmUiBackend::sharedWalletIsOpen()
-{
- // Treat the shared core as "already open" ONLY when it actually holds
- // accounts. We used to also treat a non-empty sequencer address as proof of
- // an open wallet ("a closed core returns an empty string"), but the wallet
- // module now reports a DEFAULT sequencer (e.g. http://localhost:…) even on a
- // CLOSED core. That made standalone launches wrongly take the adopt path —
- // mirroring an empty account list and returning early — instead of opening
- // the real on-disk wallet, so the user saw no accounts. Keying off accounts
- // alone means a genuinely-open shared wallet (Basecamp) is still adopted,
- // while a closed standalone core correctly falls through to open-from-disk.
- // Tradeoff: a shared wallet that is open but holds ZERO accounts is treated
- // as closed here, so it won't be adopted — an accepted edge, preferable to
- // the default-sequencer false-positive that keying off the address caused.
- return !QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()).isEmpty();
-}
+AmmUiBackend::~AmmUiBackend() = default;
-QString AmmUiBackend::createNewDefault(QString password)
+WalletAccountModel* AmmUiBackend::accountModel() const
{
- QDir().mkpath(defaultWalletHome());
- return createNew(defaultConfigPath(), defaultStoragePath(), password);
-}
-
-QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
-{
- const QString localConfig = toLocalPath(configPath);
- const QString localStorage = toLocalPath(storagePath);
- // create_new returns the new wallet's BIP39 mnemonic (empty on failure). We
- // hand it back to the caller instead of discarding it: wallet creation is
- // the only moment the seed phrase is recoverable, so the UI must force a
- // backup step before the user can proceed.
- const QString mnemonic = m_logos->logos_execution_zone.create_new(localConfig, localStorage, password);
- if (mnemonic.isEmpty()) {
- qWarning() << "AmmUiBackend: create_new failed (empty mnemonic)";
- return QString();
- }
-
- persistConfigPath(localConfig);
- persistStoragePath(localStorage);
- setWalletExists(true);
- QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
- setIsWalletOpen(true);
- refreshAccounts();
- refreshBlockHeights();
- refreshSequencerAddr();
- return mnemonic;
-}
-
-bool AmmUiBackend::openExisting()
-{
- // Adopt a shared open wallet (Basecamp), else open our own from disk. A
- // freshly-created shared wallet can be open with zero accounts, so probe
- // open-ness rather than keying off list_accounts() alone.
- if (sharedWalletIsOpen()) {
- const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
- setIsWalletOpen(true);
- m_accountModel->replaceFromJsonArray(existing);
- refreshBalances();
- refreshSequencerAddr();
- QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
- return true;
- }
-
- const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
- const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
- if (!QFileInfo::exists(stg))
- return false;
-
- const int err = m_logos->logos_execution_zone.open(cfg, stg);
- if (err != WALLET_FFI_SUCCESS) {
- qWarning() << "AmmUiBackend: openExisting failed, code" << err;
- return false;
- }
- persistConfigPath(cfg);
- persistStoragePath(stg);
- setIsWalletOpen(true);
- QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
- refreshAccounts();
- refreshBlockHeights();
- refreshSequencerAddr();
- return true;
-}
-
-void AmmUiBackend::disconnectWallet()
-{
- // UI-local lock: persist wallet state, drop our view of it, and remember
- // the choice. We do NOT close the core module's wallet handle — in Basecamp
- // that instance is shared with other apps.
- saveWallet();
- setIsWalletOpen(false);
- m_accountModel->replaceFromJsonArray(QJsonArray());
- QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, true);
+ return m_walletController->accountModel();
}
QString AmmUiBackend::createAccountPublic()
{
- const QString result = m_logos->logos_execution_zone.create_account_public();
- if (!result.isEmpty())
- refreshAccounts();
- return result;
+ return m_walletController->createAccount(true);
}
QString AmmUiBackend::createAccountPrivate()
{
- const QString result = m_logos->logos_execution_zone.create_account_private();
- if (!result.isEmpty())
- refreshAccounts();
- return result;
+ return m_walletController->createAccount(false);
}
void AmmUiBackend::refreshAccounts()
{
- const QJsonArray arr = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
- m_accountModel->replaceFromJsonArray(arr);
- refreshBalances();
+ m_walletController->refresh();
}
void AmmUiBackend::refreshBalances()
{
- refreshBlockHeights();
- if (currentBlockHeight() > 0)
- m_logos->logos_execution_zone.sync_to_block(static_cast(currentBlockHeight()));
-
- for (int i = 0; i < m_accountModel->count(); ++i) {
- const QModelIndex idx = m_accountModel->index(i, 0);
- const QString addr = m_accountModel->data(idx, AccountModel::AddressRole).toString();
- const bool isPub = m_accountModel->data(idx, AccountModel::IsPublicRole).toBool();
- m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub));
- }
- saveWallet();
+ m_walletController->refresh();
}
QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
{
- return m_logos->logos_execution_zone.get_balance(accountIdHex, isPublic);
-}
-
-void AmmUiBackend::refreshBlockHeights()
-{
- const int lastVal = m_logos->logos_execution_zone.get_last_synced_block();
- const int currentVal = m_logos->logos_execution_zone.get_current_block_height();
- if (lastSyncedBlock() != lastVal)
- setLastSyncedBlock(lastVal);
- if (currentBlockHeight() != currentVal)
- setCurrentBlockHeight(currentVal);
+ return m_walletController->balance(accountIdHex, isPublic);
}
-void AmmUiBackend::refreshSequencerAddr()
-{
- const QString addr = m_logos->logos_execution_zone.get_sequencer_addr();
- if (sequencerAddr() != addr)
- setSequencerAddr(addr);
- // Probe right away so the banner reflects the (possibly new) endpoint
- // without waiting for the next periodic tick.
- checkReachability();
-}
-
-void AmmUiBackend::checkReachability()
-{
- const QString addr = sequencerAddr();
- if (addr.isEmpty())
- return;
-
- QNetworkRequest req{QUrl(addr)};
- req.setTransferTimeout(4000);
- QNetworkReply* reply = m_net->get(req);
- connect(reply, &QNetworkReply::finished, this, [this, reply]() {
- // Any HTTP response (even a 404) means the node is up; only a transport
- // failure (connection refused, host not found, timeout) counts as down.
- const bool gotHttpStatus =
- reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
- const bool reachable = gotHttpStatus || reply->error() == QNetworkReply::NoError;
- if (sequencerReachable() != reachable)
- setSequencerReachable(reachable);
- reply->deleteLater();
- });
-}
-
-void AmmUiBackend::saveWallet()
+QString AmmUiBackend::createNewDefault(QString password)
{
- if (isWalletOpen())
- m_logos->logos_execution_zone.save();
+ return m_walletController->createDefaultWallet(password);
}
-// These only update the in-session PROPs (so subsequent open/refresh calls
-// reuse the same path). They are no longer written to QSettings: the app
-// always resolves against the canonical wallet home, so there's nothing to
-// remember across launches.
-void AmmUiBackend::persistConfigPath(const QString& path)
+QString AmmUiBackend::createNew(QString configPath,
+ QString storagePath,
+ QString password)
{
- setConfigPath(toLocalPath(path));
+ return m_walletController->createWallet(configPath, storagePath, password);
}
-void AmmUiBackend::persistStoragePath(const QString& path)
+bool AmmUiBackend::openExisting()
{
- setStoragePath(toLocalPath(path));
+ return m_walletController->open();
}
-bool AmmUiBackend::changeSequencerAddr(QString url)
+void AmmUiBackend::disconnectWallet()
{
- const QString trimmed = url.trimmed();
- if (trimmed.isEmpty()) {
- qWarning() << "AmmUiBackend: refusing to set empty sequencer_addr";
- return false;
- }
-
- const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
-
- // Preserve the other config fields (poll timeouts, retries) — only swap the
- // endpoint. The wallet reads this file on open via from_path_or_initialize_default.
- QJsonObject obj;
- QFile in(cfg);
- if (in.open(QIODevice::ReadOnly)) {
- obj = QJsonDocument::fromJson(in.readAll()).object();
- in.close();
- }
- obj.insert(QStringLiteral("sequencer_addr"), trimmed);
-
- QFile out(cfg);
- if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
- qWarning() << "AmmUiBackend: cannot write wallet config" << cfg;
- return false;
- }
- out.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
- out.close();
-
- // Re-open so the live wallet uses the new endpoint right away.
- if (isWalletOpen()) {
- const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
- const int err = m_logos->logos_execution_zone.open(cfg, stg);
- if (err != WALLET_FFI_SUCCESS) {
- qWarning() << "AmmUiBackend: reopen after sequencer change failed, code" << err;
- return false;
- }
- refreshSequencerAddr();
- refreshAccounts();
- }
- return true;
+ m_walletController->disconnect();
}
-void AmmUiBackend::copyToClipboard(QString text)
+void AmmUiBackend::syncWalletState()
{
- if (QGuiApplication::clipboard())
- QGuiApplication::clipboard()->setText(text);
+ const WalletUiState& state = m_walletController->state();
+ setIsWalletOpen(state.isWalletOpen);
+ setWalletExists(state.walletExists);
+ setConfigPath(state.configPath);
+ setStoragePath(state.storagePath);
+ setWalletHome(state.walletHome);
+ setLastSyncedBlock(state.lastSyncedBlock);
+ setCurrentBlockHeight(state.currentBlockHeight);
+ setSequencerAddr(state.sequencerAddress);
+ setSequencerReachable(state.sequencerReachable);
}
QString AmmUiBackend::normalizeAccountId(const QString& id)
diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h
index 0fd09f09..244ccc6d 100644
--- a/apps/amm/src/AmmUiBackend.h
+++ b/apps/amm/src/AmmUiBackend.h
@@ -1,14 +1,15 @@
#ifndef AMM_UI_BACKEND_H
#define AMM_UI_BACKEND_H
-#include
+#include
+
#include
#include
#include
#include "rep_AmmUiBackend_source.h"
-#include "AccountModel.h"
+#include "WalletAccountModel.h"
extern "C" {
#include "amm_client_ffi.h"
@@ -16,38 +17,29 @@ extern "C" {
class LogosAPI;
struct LogosModules;
-class QNetworkAccessManager;
-class QTimer;
+class LogosWalletProvider;
+class WalletController;
-// Source-side implementation of the AmmUiBackend .rep interface.
-// Inheriting from AmmUiBackendSimpleSource gives us the generated PROPs and
-// SLOTs from AmmUiBackend.rep — all the simple ones flow over QtRO. Talks to
-// the core logos_execution_zone wallet module via LogosModules.
class AmmUiBackend : public AmmUiBackendSimpleSource {
Q_OBJECT
- Q_PROPERTY(AccountModel* accountModel READ accountModel CONSTANT)
+ Q_PROPERTY(WalletAccountModel* accountModel READ accountModel CONSTANT)
public:
explicit AmmUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
~AmmUiBackend() override;
- AccountModel* accountModel() const { return m_accountModel; }
+ WalletAccountModel* accountModel() const;
public slots:
- // Overrides of the pure-virtual slots generated from the .rep.
QString createAccountPublic() override;
QString createAccountPrivate() override;
void refreshAccounts() override;
void refreshBalances() override;
QString getBalance(QString accountIdHex, bool isPublic) override;
- // Return the new wallet's BIP39 mnemonic (empty string on failure) so the
- // UI can force a one-time seed-phrase backup step.
QString createNewDefault(QString password) override;
QString createNew(QString configPath, QString storagePath, QString password) override;
bool openExisting() override;
void disconnectWallet() override;
- bool changeSequencerAddr(QString url) override;
- void copyToClipboard(QString text) override;
// AMM
QVariantMap resolvePool(QString defAHex, QString defBHex) override;
@@ -59,43 +51,27 @@ public slots:
QVariantList tokenList() override;
private:
- // Per-app wallet home (kept distinct from the wallet's canonical
- // ~/.lee/wallet so standalone instances stay isolated; Basecamp sharing
- // is handled by adopting an already-open shared wallet on startup).
- static QString defaultWalletHome();
- QString defaultConfigPath() const;
- QString defaultStoragePath() const;
-
- void persistConfigPath(const QString& path);
- void persistStoragePath(const QString& path);
+ void syncWalletState();
+
// Normalizes an account id given as either 64-char lowercase/uppercase hex
// or base58 to lowercase hex. Returns an empty QString if `id` is neither
// (or the base58 decode fails), so callers can detect and skip it.
QString normalizeAccountId(const QString& id);
- void openOrAdoptWallet();
- // True when the shared core already has a wallet open — including a freshly
- // created one with zero accounts. See the definition for why list_accounts()
- // alone is insufficient.
- bool sharedWalletIsOpen();
- void refreshBlockHeights();
- void refreshSequencerAddr();
- void saveWallet();
// Returns the deployed AMM program-binary bytes (a RISC Zero ProgramBinary
// .bin, not a raw ELF) from $AMM_PROGRAM_BIN, or an empty QByteArray (with a
// qWarning) if the env var is unset/unreadable/empty.
QByteArray loadAmmElf();
- // Probe the configured sequencer over HTTP and update sequencerReachable.
- void checkReachability();
-
- AccountModel* m_accountModel;
-
LogosAPI* m_logosAPI;
- LogosModules* m_logos;
-
- QNetworkAccessManager* m_net;
- QTimer* m_reachabilityTimer;
+ // Direct module handle for the AMM/swap path (resolvePool/swapExactInput/
+ // tokenList). The shared wallet provider exposes only wallet-level ops, not
+ // the raw account-id / get_account_public / send_generic_public_transaction
+ // calls the AMM path needs, so keep a thin LogosModules over the same
+ // LogosAPI as the wallet provider.
+ std::unique_ptr m_logos;
+ std::unique_ptr m_wallet;
+ std::unique_ptr m_walletController;
};
#endif // AMM_UI_BACKEND_H
diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep
index 4827e077..f389b9dc 100644
--- a/apps/amm/src/AmmUiBackend.rep
+++ b/apps/amm/src/AmmUiBackend.rep
@@ -37,13 +37,6 @@ class AmmUiBackend
// Basecamp, does not close the wallet other apps share.
SLOT(void disconnectWallet())
- // Settings. Rewrites the wallet config's sequencer_addr and re-opens the
- // wallet so the new network takes effect immediately.
- SLOT(bool changeSequencerAddr(QString url))
-
- // Misc
- SLOT(void copyToClipboard(QString text))
-
// AMM
// Derives the AMM pool's PDAs (config/pool/vaults/current-tick) from the
// deployed AMM program binary (see AMM_PROGRAM_BIN — a RISC Zero
diff --git a/apps/shared/wallet/CMakeLists.txt b/apps/shared/wallet/CMakeLists.txt
new file mode 100644
index 00000000..37f16630
--- /dev/null
+++ b/apps/shared/wallet/CMakeLists.txt
@@ -0,0 +1,168 @@
+cmake_minimum_required(VERSION 3.21)
+
+if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
+ project(LogosWallet LANGUAGES CXX)
+ include(CTest)
+endif()
+
+option(LOGOS_WALLET_BUILD_QML "Build the Logos.Wallet QML module" ON)
+option(LOGOS_WALLET_BUILD_ACCESS "Build the generated-SDK wallet adapter" ON)
+set(LOGOS_WALLET_GENERATED_DIR "" CACHE PATH "Path to generated Logos SDK sources")
+
+if(LOGOS_WALLET_BUILD_ACCESS
+ AND NOT EXISTS "${LOGOS_WALLET_GENERATED_DIR}/logos_sdk.h"
+ AND NOT EXISTS "${LOGOS_WALLET_GENERATED_DIR}/include/logos_sdk.h")
+ message(FATAL_ERROR
+ "logos_wallet_access requires logos_sdk.h; set LOGOS_WALLET_GENERATED_DIR"
+ )
+endif()
+
+find_package(Qt6 6.8 REQUIRED COMPONENTS Core)
+if(LOGOS_WALLET_BUILD_ACCESS OR BUILD_TESTING)
+ find_package(Qt6 6.8 REQUIRED COMPONENTS Network)
+endif()
+set(CMAKE_AUTOMOC ON)
+
+if(LOGOS_WALLET_BUILD_ACCESS)
+ add_library(logos_wallet_access STATIC
+ src/WalletProvider.h
+ src/WalletProvider.cpp
+ src/LogosWalletProvider.h
+ src/LogosWalletProvider.cpp
+ src/WalletAccountModel.h
+ src/WalletAccountModel.cpp
+ src/WalletController.h
+ src/WalletController.cpp
+ )
+ set_target_properties(logos_wallet_access PROPERTIES
+ AUTOMOC ON
+ POSITION_INDEPENDENT_CODE ON
+ )
+ target_compile_features(logos_wallet_access PUBLIC cxx_std_17)
+ target_include_directories(logos_wallet_access
+ PUBLIC
+ "$"
+ PRIVATE
+ "${LOGOS_WALLET_GENERATED_DIR}"
+ "${LOGOS_WALLET_GENERATED_DIR}/include"
+ )
+ target_link_libraries(logos_wallet_access
+ PUBLIC Qt6::Core
+ PRIVATE Qt6::Network
+ )
+endif()
+
+if(LOGOS_WALLET_BUILD_QML)
+ find_package(Qt6 6.8 REQUIRED COMPONENTS Qml Quick QuickControls2)
+ qt_policy(SET QTP0004 NEW)
+
+ set(wallet_qml_output_dir "${CMAKE_CURRENT_BINARY_DIR}/qml/Logos/Wallet")
+ set(wallet_internal_qml
+ qml/internal/WalletIconButton.qml
+ qml/internal/CopyButton.qml
+ qml/internal/AccountDelegate.qml
+ qml/internal/CreateAccountDialog.qml
+ qml/internal/CreateWalletDialog.qml
+ qml/internal/WalletMessageDialog.qml
+ )
+ set(wallet_public_qml
+ qml/WalletControl.qml
+ qml/TransactionConfirmationDialog.qml
+ qml/SubmittedTransaction.qml
+ )
+ set(wallet_icons
+ qml/internal/icons/account.svg
+ qml/internal/icons/back.svg
+ qml/internal/icons/checkmark.svg
+ qml/internal/icons/copy.svg
+ qml/internal/icons/power.svg
+ )
+ foreach(qml_file IN LISTS wallet_public_qml wallet_internal_qml)
+ get_filename_component(qml_name "${qml_file}" NAME)
+ set_source_files_properties("${qml_file}" PROPERTIES QT_RESOURCE_ALIAS "${qml_name}")
+ endforeach()
+ foreach(icon IN LISTS wallet_icons)
+ get_filename_component(icon_name "${icon}" NAME)
+ set_source_files_properties("${icon}" PROPERTIES QT_RESOURCE_ALIAS "icons/${icon_name}")
+ endforeach()
+ set_source_files_properties(${wallet_internal_qml} PROPERTIES QT_QML_INTERNAL_TYPE TRUE)
+
+ qt_add_library(logos_wallet_qml SHARED)
+ qt_add_qml_module(logos_wallet_qml
+ URI Logos.Wallet
+ VERSION 1.0
+ RESOURCE_PREFIX /qt/qml
+ OUTPUT_DIRECTORY "${wallet_qml_output_dir}"
+ TYPEINFO plugins.qmltypes
+ QML_FILES
+ ${wallet_public_qml}
+ ${wallet_internal_qml}
+ RESOURCES
+ ${wallet_icons}
+ )
+ target_link_libraries(logos_wallet_qml PRIVATE
+ Qt6::Core
+ Qt6::Qml
+ Qt6::Quick
+ Qt6::QuickControls2
+ )
+ set_target_properties(logos_wallet_qml logos_wallet_qmlplugin PROPERTIES
+ LIBRARY_OUTPUT_DIRECTORY "${wallet_qml_output_dir}"
+ RUNTIME_OUTPUT_DIRECTORY "${wallet_qml_output_dir}"
+ BUILD_WITH_INSTALL_RPATH ON
+ INSTALL_RPATH "$ORIGIN"
+ )
+
+ set(wallet_qml_install_dir "lib/qml/Logos/Wallet")
+ install(TARGETS logos_wallet_qml logos_wallet_qmlplugin
+ LIBRARY DESTINATION "${wallet_qml_install_dir}"
+ RUNTIME DESTINATION "${wallet_qml_install_dir}"
+ )
+ install(FILES
+ "${wallet_qml_output_dir}/qmldir"
+ "${wallet_qml_output_dir}/plugins.qmltypes"
+ DESTINATION "${wallet_qml_install_dir}"
+ OPTIONAL
+ )
+endif()
+
+if(BUILD_TESTING)
+ find_package(Qt6 6.8 REQUIRED COMPONENTS Test)
+
+ add_executable(logos_wallet_access_test
+ tests/cpp/LogosWalletProviderTest.cpp
+ src/WalletProvider.cpp
+ src/LogosWalletProvider.cpp
+ src/WalletAccountModel.cpp
+ src/WalletAccountModel.h
+ src/WalletController.cpp
+ src/WalletController.h
+ )
+ set_target_properties(logos_wallet_access_test PROPERTIES AUTOMOC ON)
+ target_compile_features(logos_wallet_access_test PRIVATE cxx_std_17)
+ target_include_directories(logos_wallet_access_test PRIVATE
+ tests/cpp/fixtures
+ tests/support
+ src
+ )
+ target_link_libraries(logos_wallet_access_test PRIVATE
+ Qt6::Core
+ Qt6::Network
+ Qt6::Test
+ )
+ add_test(NAME logos_wallet_access COMMAND logos_wallet_access_test)
+
+ if(LOGOS_WALLET_BUILD_QML)
+ find_package(Qt6 6.8 REQUIRED COMPONENTS QuickTest)
+ add_executable(logos_wallet_qml_test tests/qml/main.cpp)
+ target_compile_definitions(logos_wallet_qml_test PRIVATE
+ QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/tests/qml"
+ )
+ target_link_libraries(logos_wallet_qml_test PRIVATE Qt6::QuickTest)
+ add_dependencies(logos_wallet_qml_test logos_wallet_qmlplugin)
+ add_test(NAME logos_wallet_qml COMMAND logos_wallet_qml_test)
+ set_tests_properties(logos_wallet_qml PROPERTIES ENVIRONMENT
+ "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software;QML2_IMPORT_PATH=${CMAKE_CURRENT_BINARY_DIR}/qml;QML_IMPORT_PATH=${CMAKE_CURRENT_BINARY_DIR}/qml"
+ )
+ endif()
+endif()
diff --git a/apps/shared/wallet/qml/SubmittedTransaction.qml b/apps/shared/wallet/qml/SubmittedTransaction.qml
new file mode 100644
index 00000000..00cc3ba8
--- /dev/null
+++ b/apps/shared/wallet/qml/SubmittedTransaction.qml
@@ -0,0 +1,86 @@
+import QtQuick
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ property string title: qsTr("Transaction submitted")
+ property string transactionId: ""
+ readonly property bool base58Shape: root.transactionId.length > 0
+ && /^[1-9A-HJ-NP-Za-km-z]+$/.test(root.transactionId)
+
+ implicitWidth: 420
+ implicitHeight: resultCard.implicitHeight
+
+ TextEdit {
+ id: clipboardProxy
+ visible: false
+ }
+
+ function copyTransactionId() {
+ if (!root.transactionId)
+ return
+ clipboardProxy.text = root.transactionId
+ clipboardProxy.selectAll()
+ clipboardProxy.copy()
+ clipboardProxy.deselect()
+ clipboardProxy.text = ""
+ }
+
+ Rectangle {
+ id: resultCard
+ anchors.left: parent.left
+ anchors.right: parent.right
+ implicitHeight: resultContent.implicitHeight + 32
+ color: "#18181b"
+ border.color: "#3f3f46"
+ border.width: 1
+ radius: 8
+
+ ColumnLayout {
+ id: resultContent
+ anchors.fill: parent
+ anchors.margins: 16
+ spacing: 12
+
+ Label {
+ Layout.fillWidth: true
+ text: root.title
+ color: "#f4f4f5"
+ font.bold: true
+ font.pixelSize: 17
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Transaction ID")
+ color: "#a1a1aa"
+ font.pixelSize: 12
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+
+ Label {
+ id: transactionLabel
+ objectName: "submittedTransactionId"
+ Layout.fillWidth: true
+ text: root.transactionId
+ color: "#f4f4f5"
+ font.family: "monospace"
+ wrapMode: Text.WrapAnywhere
+ Accessible.name: qsTr("Transaction ID %1").arg(root.transactionId)
+ }
+
+ CopyButton {
+ objectName: "copySubmittedTransactionButton"
+ enabled: root.transactionId.length > 0
+ onCopyRequested: root.copyTransactionId()
+ }
+ }
+ }
+ }
+}
diff --git a/apps/shared/wallet/qml/TransactionConfirmationDialog.qml b/apps/shared/wallet/qml/TransactionConfirmationDialog.qml
new file mode 100644
index 00000000..141a8db8
--- /dev/null
+++ b/apps/shared/wallet/qml/TransactionConfirmationDialog.qml
@@ -0,0 +1,169 @@
+import QtQuick
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+
+Popup {
+ id: root
+
+ property string title: qsTr("Confirm transaction")
+ property string cancelText: qsTr("Cancel")
+ property string confirmText: qsTr("Confirm")
+ property bool busy: false
+ property var snapshot: ({})
+ property Component summary: null
+ property bool confirmationPending: false
+
+ signal canceled
+ signal confirmed(var snapshot)
+
+ modal: true
+ dim: true
+ padding: 20
+ width: Math.max(0, Math.min(420, parent ? parent.width - 32 : 420))
+ height: Math.max(0, Math.min(implicitHeight, parent ? parent.height - 32 : implicitHeight))
+ x: parent ? Math.max(0, Math.round((parent.width - width) / 2)) : 0
+ y: parent ? Math.max(0, Math.round((parent.height - height) / 2)) : 0
+ closePolicy: root.busy ? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ focus: true
+
+ function cloneSnapshot(value) {
+ if (value === undefined || value === null)
+ return ({})
+ try {
+ return JSON.parse(JSON.stringify(value))
+ } catch (_error) {
+ return value
+ }
+ }
+
+ function openWithSnapshot(nextSnapshot) {
+ root.snapshot = root.cloneSnapshot(nextSnapshot)
+ root.confirmationPending = false
+ root.open()
+ cancelButton.forceActiveFocus()
+ }
+
+ function cancel() {
+ if (root.busy)
+ return
+ root.confirmationPending = false
+ root.close()
+ root.canceled()
+ }
+
+ function confirm() {
+ if (root.busy)
+ return
+ root.confirmationPending = true
+ root.confirmed(root.snapshot)
+ if (!root.busy) {
+ root.confirmationPending = false
+ root.close()
+ }
+ }
+
+ onBusyChanged: {
+ if (!root.busy && root.confirmationPending) {
+ root.confirmationPending = false
+ root.close()
+ }
+ }
+
+ onSnapshotChanged: {
+ if (summaryLoader.item && summaryLoader.item.hasOwnProperty("snapshot"))
+ summaryLoader.item.snapshot = root.snapshot
+ }
+
+ Overlay.modal: Rectangle { color: "#99000000" }
+
+ background: Rectangle {
+ color: "#18181b"
+ border.color: "#3f3f46"
+ border.width: 1
+ radius: 8
+ }
+
+ contentItem: ColumnLayout {
+ spacing: 16
+
+ Label {
+ Layout.fillWidth: true
+ text: root.title
+ color: "#f4f4f5"
+ font.bold: true
+ font.pixelSize: 17
+ wrapMode: Text.WordWrap
+ }
+
+ ScrollView {
+ id: summaryScroll
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.minimumHeight: 0
+ Layout.preferredHeight: summaryLoader.implicitHeight
+ clip: true
+ contentWidth: availableWidth
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+ Loader {
+ id: summaryLoader
+ objectName: "transactionSummaryLoader"
+ width: summaryScroll.availableWidth
+ sourceComponent: root.summary
+ onLoaded: {
+ if (item && item.hasOwnProperty("snapshot"))
+ item.snapshot = root.snapshot
+ }
+ }
+ }
+
+ BusyIndicator {
+ Layout.alignment: Qt.AlignHCenter
+ visible: root.busy
+ running: root.busy
+ Accessible.name: qsTr("Submitting transaction")
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 10
+
+ Button {
+ id: cancelButton
+ objectName: "transactionCancelButton"
+ Layout.fillWidth: true
+ implicitHeight: 44
+ text: root.cancelText
+ enabled: !root.busy
+ Accessible.name: text
+ onClicked: root.cancel()
+ }
+
+ Button {
+ id: confirmButton
+ objectName: "transactionConfirmButton"
+ Layout.fillWidth: true
+ implicitHeight: 44
+ text: root.busy ? qsTr("Submitting...") : root.confirmText
+ enabled: !root.busy
+ Accessible.name: text
+ onClicked: root.confirm()
+
+ background: Rectangle {
+ color: confirmButton.enabled
+ ? confirmButton.pressed ? "#d95c1e" : "#f26a21"
+ : "#52525b"
+ radius: 6
+ }
+
+ contentItem: Label {
+ text: confirmButton.text
+ color: "#ffffff"
+ font.bold: true
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ }
+ }
+}
diff --git a/apps/shared/wallet/qml/WalletControl.qml b/apps/shared/wallet/qml/WalletControl.qml
new file mode 100644
index 00000000..28d32a13
--- /dev/null
+++ b/apps/shared/wallet/qml/WalletControl.qml
@@ -0,0 +1,503 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ property var wallet: null
+ property var accountModel: null
+ property var watchCall: null
+ property bool compact: false
+ property real viewportWidth: width
+ property int selectedIndex: 0
+ property bool busy: false
+
+ readonly property bool connected: root.wallet !== null && root.wallet.isWalletOpen
+ readonly property bool compactLayout: root.compact || root.viewportWidth < 680
+ readonly property string selectedAddress: root.accountAt(root.selectedIndex, "address")
+ readonly property string selectedName: root.accountAt(root.selectedIndex, "name")
+ readonly property string selectedBalance: root.accountAt(root.selectedIndex, "balance")
+ readonly property bool selectedIsPublic: root.accountAt(root.selectedIndex, "isPublic") === true
+
+ implicitWidth: root.connected ? connectedButton.implicitWidth : connectButton.implicitWidth
+ implicitHeight: 40
+
+ Instantiator {
+ id: accounts
+ model: root.accountModel
+ delegate: QtObject {
+ required property string address
+ required property string name
+ required property string balance
+ required property bool isPublic
+ }
+ onCountChanged: root.clampSelection()
+ }
+
+ function accountAt(index, field) {
+ const entry = index >= 0 && index < accounts.count ? accounts.objectAt(index) : null
+ return entry ? entry[field] : (field === "isPublic" ? false : "")
+ }
+
+ function clampSelection() {
+ if (accounts.count === 0) {
+ root.selectedIndex = 0
+ } else {
+ root.selectedIndex = Math.max(0, Math.min(root.selectedIndex, accounts.count - 1))
+ }
+ }
+
+ function shortAddress(address) {
+ return address && address.length > 13
+ ? address.substring(0, 6) + "..." + address.substring(address.length - 4)
+ : address || ""
+ }
+
+ function watchResult(result, success, failure) {
+ if (root.watchCall) {
+ root.watchCall(result, success, failure)
+ } else {
+ success(result)
+ }
+ }
+
+ function showError(message) {
+ messageDialog.message = message
+ messageDialog.open()
+ }
+
+ function openWallet() {
+ if (!root.wallet || root.busy)
+ return
+ root.busy = true
+ try {
+ root.watchResult(root.wallet.openExisting(), function(ok) {
+ root.busy = false
+ if (!ok)
+ root.showError(qsTr("Wallet could not be opened."))
+ }, function(error) {
+ root.busy = false
+ root.showError(qsTr("Wallet could not be opened: %1").arg(error))
+ })
+ } catch (error) {
+ root.busy = false
+ root.showError(qsTr("Wallet could not be opened: %1").arg(error))
+ }
+ }
+
+ TextEdit {
+ id: clipboardProxy
+ visible: false
+ }
+
+ function copyToClipboard(text) {
+ if (!text)
+ return
+ clipboardProxy.text = text
+ clipboardProxy.selectAll()
+ clipboardProxy.copy()
+ clipboardProxy.deselect()
+ clipboardProxy.text = ""
+ }
+
+ Connections {
+ target: root.accountModel
+ ignoreUnknownSignals: true
+ function onModelReset() { root.clampSelection() }
+ function onRowsInserted() { root.clampSelection() }
+ function onRowsRemoved() { root.clampSelection() }
+ }
+
+ onConnectedChanged: {
+ if (!root.connected) {
+ root.selectedIndex = 0
+ walletMenu.close()
+ }
+ }
+
+ onViewportWidthChanged: {
+ if (walletMenu.opened)
+ Qt.callLater(walletMenu.updateAnchor)
+ }
+
+ Button {
+ id: connectButton
+ objectName: "walletConnectButton"
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !root.connected
+ enabled: root.wallet !== null && !root.busy
+ implicitHeight: 40
+ implicitWidth: root.compactLayout ? 40 : 108
+ text: root.compactLayout ? "" : root.busy ? qsTr("Connecting...") : qsTr("Connect")
+ display: root.compactLayout ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
+ icon.source: Qt.resolvedUrl("icons/account.svg")
+ icon.color: "#ffffff"
+ icon.width: 18
+ icon.height: 18
+ Accessible.name: qsTr("Connect wallet")
+ ToolTip.text: Accessible.name
+ ToolTip.visible: hovered && root.compactLayout
+
+ background: Rectangle {
+ color: connectButton.pressed ? "#d95c1e" : "#f26a21"
+ radius: 6
+ }
+
+ contentItem: RowLayout {
+ spacing: 6
+ Image {
+ visible: root.compactLayout
+ source: connectButton.icon.source
+ sourceSize.width: 18
+ sourceSize.height: 18
+ }
+ Label {
+ Layout.fillWidth: true
+ visible: !root.compactLayout
+ text: connectButton.text
+ color: "#ffffff"
+ font.bold: true
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ onClicked: {
+ if (root.wallet && root.wallet.walletExists)
+ root.openWallet()
+ else
+ createWalletDialog.open()
+ }
+ }
+
+ Button {
+ id: connectedButton
+ objectName: "walletAccountButton"
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ visible: root.connected
+ enabled: !root.busy
+ implicitHeight: 40
+ implicitWidth: root.compactLayout ? 44 : Math.max(140, accountButtonLabel.implicitWidth + 58)
+ Accessible.name: qsTr("Wallet account %1").arg(root.selectedAddress)
+
+ background: Rectangle {
+ color: connectedButton.pressed ? "#3f3f46" : "#27272a"
+ border.width: walletMenu.opened || connectedButton.activeFocus ? 1 : 0
+ border.color: "#f26a21"
+ radius: 6
+ }
+
+ contentItem: RowLayout {
+ spacing: 8
+
+ Rectangle {
+ Layout.preferredWidth: 8
+ Layout.preferredHeight: 8
+ radius: 4
+ color: "#22c55e"
+ }
+
+ Label {
+ id: accountButtonLabel
+ Layout.fillWidth: true
+ visible: !root.compactLayout
+ text: root.shortAddress(root.selectedAddress) || qsTr("Connected")
+ color: "#f4f4f5"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Label {
+ visible: !root.compactLayout
+ text: walletMenu.opened ? "\u25b4" : "\u25be"
+ color: "#a1a1aa"
+ }
+ }
+
+ onClicked: {
+ if (walletMenu.opened || Date.now() - walletMenu.lastClosedMs < 200)
+ walletMenu.close()
+ else
+ walletMenu.open()
+ }
+ }
+
+ Popup {
+ id: walletMenu
+ objectName: "walletMenu"
+ property real lastClosedMs: 0
+ property point anchorPosition: Qt.point(0, 0)
+ readonly property var viewport: Overlay.overlay
+ readonly property real spaceAbove: Math.max(0, anchorPosition.y - 20)
+ readonly property real spaceBelow: viewport
+ ? Math.max(0, viewport.height - anchorPosition.y - connectedButton.height - 20)
+ : implicitHeight
+ readonly property bool opensAbove: spaceBelow < implicitHeight && spaceAbove > spaceBelow
+ readonly property real availableMenuHeight: opensAbove ? spaceAbove : spaceBelow
+ parent: connectedButton
+ x: viewport
+ ? Math.max(12 - anchorPosition.x,
+ Math.min(connectedButton.width - width,
+ viewport.width - width - 12 - anchorPosition.x))
+ : connectedButton.width - width
+ y: opensAbove
+ ? -height - 8
+ : connectedButton.height + 8
+ width: Math.min(360, Math.max(0, Math.min(root.viewportWidth,
+ viewport ? viewport.width : root.viewportWidth) - 24))
+ height: Math.min(implicitHeight, availableMenuHeight)
+ margins: 12
+ padding: 12
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ function updateAnchor() {
+ if (viewport)
+ anchorPosition = connectedButton.mapToItem(viewport, 0, 0)
+ }
+
+ onAboutToShow: updateAnchor()
+
+ onClosed: {
+ walletMenu.lastClosedMs = Date.now()
+ if (walletStack.depth > 1)
+ walletStack.pop(null, StackView.Immediate)
+ }
+
+ Connections {
+ target: walletMenu.opened ? walletMenu.viewport : null
+
+ function onWidthChanged() { Qt.callLater(walletMenu.updateAnchor) }
+ function onHeightChanged() { Qt.callLater(walletMenu.updateAnchor) }
+ }
+
+ background: Rectangle {
+ color: "#18181b"
+ border.color: "#3f3f46"
+ border.width: 1
+ radius: 8
+ }
+
+ contentItem: StackView {
+ id: walletStack
+ width: walletMenu.availableWidth
+ height: walletMenu.availableHeight
+ implicitWidth: walletMenu.availableWidth
+ implicitHeight: currentItem ? currentItem.implicitHeight : 0
+ initialItem: walletOverview
+ }
+
+ Component {
+ id: walletOverview
+
+ ColumnLayout {
+ spacing: 12
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Item { Layout.fillWidth: true }
+
+ WalletIconButton {
+ objectName: "walletAccountsButton"
+ iconSource: Qt.resolvedUrl("icons/account.svg")
+ accessibleName: qsTr("Accounts")
+ onClicked: walletStack.push(accountList)
+ }
+
+ WalletIconButton {
+ objectName: "walletDisconnectButton"
+ iconSource: Qt.resolvedUrl("icons/power.svg")
+ accessibleName: qsTr("Disconnect")
+ onClicked: {
+ walletMenu.close()
+ if (root.wallet)
+ root.wallet.disconnectWallet()
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ implicitHeight: accountCard.implicitHeight + 24
+ color: "#27272a"
+ radius: 6
+
+ ColumnLayout {
+ id: accountCard
+ anchors.fill: parent
+ anchors.margins: 12
+ spacing: 8
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Label {
+ text: root.selectedName || qsTr("Account")
+ color: "#f4f4f5"
+ font.bold: true
+ }
+
+ Label {
+ text: root.selectedIsPublic ? qsTr("Public") : qsTr("Private")
+ color: "#a1a1aa"
+ font.pixelSize: 11
+ }
+
+ Item { Layout.fillWidth: true }
+
+ Label {
+ text: root.selectedBalance || "-"
+ color: "#f4f4f5"
+ font.bold: true
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 4
+
+ Label {
+ Layout.fillWidth: true
+ text: root.selectedAddress
+ color: "#a1a1aa"
+ font.family: "monospace"
+ font.pixelSize: 11
+ elide: Text.ElideMiddle
+ }
+
+ CopyButton {
+ visible: root.selectedAddress.length > 0
+ onCopyRequested: root.copyToClipboard(root.selectedAddress)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: accountList
+
+ ColumnLayout {
+ spacing: 12
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ WalletIconButton {
+ iconSource: Qt.resolvedUrl("icons/back.svg")
+ accessibleName: qsTr("Back")
+ onClicked: walletStack.pop()
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Accounts")
+ color: "#f4f4f5"
+ font.bold: true
+ }
+ }
+
+ ListView {
+ id: accountListView
+ objectName: "walletAccountList"
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.minimumHeight: 0
+ Layout.preferredHeight: Math.min(contentHeight, 260)
+ clip: true
+ spacing: 6
+ model: root.accountModel
+ ScrollIndicator.vertical: ScrollIndicator { }
+
+ delegate: AccountDelegate {
+ width: ListView.view.width
+ highlighted: index === root.selectedIndex
+ onClicked: {
+ root.selectedIndex = index
+ walletStack.pop()
+ }
+ onCopyRequested: function(text) { root.copyToClipboard(text) }
+ }
+ }
+
+ Button {
+ objectName: "walletAddAccountButton"
+ Layout.fillWidth: true
+ text: qsTr("Add account")
+ enabled: !root.busy
+ onClicked: createAccountDialog.open()
+ }
+ }
+ }
+ }
+
+ CreateWalletDialog {
+ id: createWalletDialog
+ objectName: "createWalletDialog"
+ walletHome: root.wallet ? root.wallet.walletHome || "" : ""
+ busy: root.busy
+
+ onCreateRequested: function(password) {
+ if (!root.wallet || root.busy)
+ return
+ root.busy = true
+ try {
+ root.watchResult(root.wallet.createNewDefault(password), function(mnemonic) {
+ root.busy = false
+ if (mnemonic && mnemonic.length > 0)
+ createWalletDialog.mnemonic = mnemonic
+ else
+ createWalletDialog.errorText = qsTr("Wallet could not be created.")
+ }, function(error) {
+ root.busy = false
+ createWalletDialog.errorText = qsTr("Wallet could not be created: %1").arg(error)
+ })
+ } catch (error) {
+ root.busy = false
+ createWalletDialog.errorText = qsTr("Wallet could not be created: %1").arg(error)
+ }
+ }
+
+ onCopyRequested: function(text) { root.copyToClipboard(text) }
+ }
+
+ CreateAccountDialog {
+ id: createAccountDialog
+ objectName: "createAccountDialog"
+ busy: root.busy
+
+ onCreateRequested: function(isPublic) {
+ if (!root.wallet || root.busy)
+ return
+ root.busy = true
+ try {
+ const request = isPublic
+ ? root.wallet.createAccountPublic()
+ : root.wallet.createAccountPrivate()
+ root.watchResult(request, function(accountId) {
+ root.busy = false
+ if (accountId && accountId.length > 0) {
+ createAccountDialog.close()
+ } else {
+ root.showError(qsTr("Account could not be created."))
+ }
+ }, function(error) {
+ root.busy = false
+ root.showError(qsTr("Account could not be created: %1").arg(error))
+ })
+ } catch (error) {
+ root.busy = false
+ root.showError(qsTr("Account could not be created: %1").arg(error))
+ }
+ }
+ }
+
+ WalletMessageDialog {
+ id: messageDialog
+ objectName: "walletMessageDialog"
+ }
+}
diff --git a/apps/shared/wallet/qml/internal/AccountDelegate.qml b/apps/shared/wallet/qml/internal/AccountDelegate.qml
new file mode 100644
index 00000000..b0ff529f
--- /dev/null
+++ b/apps/shared/wallet/qml/internal/AccountDelegate.qml
@@ -0,0 +1,77 @@
+import QtQuick
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+
+ItemDelegate {
+ id: root
+
+ required property int index
+ required property string name
+ required property string address
+ required property string balance
+ required property bool isPublic
+
+ signal copyRequested(string text)
+
+ leftPadding: 12
+ rightPadding: 8
+ topPadding: 10
+ bottomPadding: 10
+
+ Accessible.name: qsTr("%1, balance %2").arg(root.name).arg(root.balance || "0")
+
+ background: Rectangle {
+ color: root.highlighted || root.hovered ? "#27272a" : "#18181b"
+ radius: 6
+ border.width: root.activeFocus ? 1 : 0
+ border.color: "#f26a21"
+ }
+
+ contentItem: ColumnLayout {
+ spacing: 6
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+
+ Label {
+ text: root.name
+ color: "#f4f4f5"
+ font.bold: true
+ }
+
+ Label {
+ text: root.isPublic ? qsTr("Public") : qsTr("Private")
+ color: "#a1a1aa"
+ font.pixelSize: 11
+ }
+
+ Item { Layout.fillWidth: true }
+
+ Label {
+ text: root.balance.length > 0 ? root.balance : "-"
+ color: "#f4f4f5"
+ font.bold: true
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 4
+
+ Label {
+ Layout.fillWidth: true
+ text: root.address
+ color: "#a1a1aa"
+ font.family: "monospace"
+ font.pixelSize: 11
+ elide: Text.ElideMiddle
+ }
+
+ CopyButton {
+ visible: root.address.length > 0
+ onCopyRequested: root.copyRequested(root.address)
+ }
+ }
+ }
+}
diff --git a/apps/shared/wallet/qml/internal/CopyButton.qml b/apps/shared/wallet/qml/internal/CopyButton.qml
new file mode 100644
index 00000000..1964da3c
--- /dev/null
+++ b/apps/shared/wallet/qml/internal/CopyButton.qml
@@ -0,0 +1,26 @@
+import QtQuick
+
+WalletIconButton {
+ id: root
+
+ signal copyRequested
+
+ property bool copied: false
+
+ accessibleName: root.copied ? qsTr("Copied") : qsTr("Copy")
+ iconSource: root.copied
+ ? Qt.resolvedUrl("icons/checkmark.svg")
+ : Qt.resolvedUrl("icons/copy.svg")
+
+ Timer {
+ id: resetTimer
+ interval: 1500
+ onTriggered: root.copied = false
+ }
+
+ onClicked: {
+ root.copyRequested()
+ root.copied = true
+ resetTimer.restart()
+ }
+}
diff --git a/apps/shared/wallet/qml/internal/CreateAccountDialog.qml b/apps/shared/wallet/qml/internal/CreateAccountDialog.qml
new file mode 100644
index 00000000..51d5f31f
--- /dev/null
+++ b/apps/shared/wallet/qml/internal/CreateAccountDialog.qml
@@ -0,0 +1,94 @@
+import QtQuick
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+
+Popup {
+ id: root
+
+ property bool busy: false
+
+ signal createRequested(bool isPublic)
+
+ modal: true
+ dim: true
+ parent: Overlay.overlay
+ width: Math.max(0, Math.min(380, parent ? parent.width - 32 : 380))
+ x: parent ? Math.max(0, Math.round((parent.width - width) / 2)) : 0
+ y: parent ? Math.max(0, Math.round((parent.height - height) / 2)) : 0
+ padding: 20
+ closePolicy: root.busy ? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ onOpened: privateSwitch.checked = false
+
+ background: Rectangle {
+ color: "#18181b"
+ border.color: "#3f3f46"
+ border.width: 1
+ radius: 8
+ }
+
+ contentItem: ColumnLayout {
+ spacing: 16
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Create account")
+ color: "#f4f4f5"
+ font.bold: true
+ font.pixelSize: 17
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 12
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Label {
+ text: privateSwitch.checked ? qsTr("Private") : qsTr("Public")
+ color: "#f4f4f5"
+ font.bold: true
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: privateSwitch.checked
+ ? qsTr("Private balance and activity")
+ : qsTr("Public balance and activity")
+ color: "#a1a1aa"
+ wrapMode: Text.WordWrap
+ }
+ }
+
+ Switch {
+ id: privateSwitch
+ objectName: "privateAccountSwitch"
+ Accessible.name: qsTr("Create private account")
+ enabled: !root.busy
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 10
+
+ Button {
+ Layout.fillWidth: true
+ text: qsTr("Cancel")
+ enabled: !root.busy
+ onClicked: root.close()
+ }
+
+ Button {
+ id: createButton
+ objectName: "createAccountButton"
+ Layout.fillWidth: true
+ text: root.busy ? qsTr("Creating...") : qsTr("Create")
+ enabled: !root.busy
+ onClicked: root.createRequested(!privateSwitch.checked)
+ }
+ }
+ }
+}
diff --git a/apps/shared/wallet/qml/internal/CreateWalletDialog.qml b/apps/shared/wallet/qml/internal/CreateWalletDialog.qml
new file mode 100644
index 00000000..ac422c1f
--- /dev/null
+++ b/apps/shared/wallet/qml/internal/CreateWalletDialog.qml
@@ -0,0 +1,190 @@
+import QtQuick
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+
+Popup {
+ id: root
+
+ property string walletHome: ""
+ property string mnemonic: ""
+ property string errorText: ""
+ property bool busy: false
+
+ signal createRequested(string password)
+ signal copyRequested(string text)
+
+ modal: true
+ dim: true
+ parent: Overlay.overlay
+ width: Math.max(0, Math.min(420, parent ? parent.width - 32 : 420))
+ x: parent ? Math.max(0, Math.round((parent.width - width) / 2)) : 0
+ y: parent ? Math.max(0, Math.round((parent.height - height) / 2)) : 0
+ padding: 20
+ closePolicy: root.busy || root.mnemonic.length > 0
+ ? Popup.NoAutoClose
+ : Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ onOpened: {
+ passwordField.text = ""
+ confirmField.text = ""
+ acknowledgement.checked = false
+ root.errorText = ""
+ root.mnemonic = ""
+ passwordField.forceActiveFocus()
+ }
+
+ background: Rectangle {
+ color: "#18181b"
+ border.color: "#3f3f46"
+ border.width: 1
+ radius: 8
+ }
+
+ contentItem: ColumnLayout {
+ spacing: 16
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 14
+ visible: root.mnemonic.length === 0
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Create wallet")
+ color: "#f4f4f5"
+ font.bold: true
+ font.pixelSize: 17
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: root.walletHome.length > 0
+ ? qsTr("Wallet will be stored at %1").arg(root.walletHome)
+ : qsTr("Wallet will use default storage")
+ color: "#a1a1aa"
+ wrapMode: Text.WordWrap
+ }
+
+ TextField {
+ id: passwordField
+ objectName: "walletPasswordField"
+ Layout.fillWidth: true
+ placeholderText: qsTr("Password")
+ echoMode: TextInput.Password
+ enabled: !root.busy
+ Keys.onReturnPressed: createButton.tryCreate()
+ }
+
+ TextField {
+ id: confirmField
+ objectName: "walletConfirmPasswordField"
+ Layout.fillWidth: true
+ placeholderText: qsTr("Confirm password")
+ echoMode: TextInput.Password
+ enabled: !root.busy
+ Keys.onReturnPressed: createButton.tryCreate()
+ }
+
+ Label {
+ Layout.fillWidth: true
+ visible: root.errorText.length > 0
+ text: root.errorText
+ color: "#f87171"
+ wrapMode: Text.WordWrap
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 10
+
+ Button {
+ Layout.fillWidth: true
+ text: qsTr("Cancel")
+ enabled: !root.busy
+ onClicked: root.close()
+ }
+
+ Button {
+ id: createButton
+ objectName: "createWalletButton"
+ Layout.fillWidth: true
+ text: root.busy ? qsTr("Creating...") : qsTr("Create wallet")
+ enabled: !root.busy
+
+ function tryCreate() {
+ if (passwordField.text.length === 0) {
+ root.errorText = qsTr("Password cannot be empty.")
+ } else if (passwordField.text !== confirmField.text) {
+ root.errorText = qsTr("Passwords do not match.")
+ } else {
+ root.errorText = ""
+ root.createRequested(passwordField.text)
+ }
+ }
+
+ onClicked: tryCreate()
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 14
+ visible: root.mnemonic.length > 0
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Back up recovery phrase")
+ color: "#f4f4f5"
+ font.bold: true
+ font.pixelSize: 17
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Write these words down in order. Anyone with this phrase can control this wallet.")
+ color: "#d4d4d8"
+ wrapMode: Text.WordWrap
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ implicitHeight: phraseLabel.implicitHeight + 24
+ color: "#27272a"
+ radius: 6
+
+ Label {
+ id: phraseLabel
+ objectName: "walletMnemonic"
+ anchors.fill: parent
+ anchors.margins: 12
+ text: root.mnemonic
+ color: "#f4f4f5"
+ font.bold: true
+ wrapMode: Text.WordWrap
+ }
+ }
+
+ Button {
+ Layout.fillWidth: true
+ text: qsTr("Copy recovery phrase")
+ onClicked: root.copyRequested(root.mnemonic)
+ }
+
+ CheckBox {
+ id: acknowledgement
+ objectName: "walletBackupAcknowledgement"
+ Layout.fillWidth: true
+ text: qsTr("I have safely backed up my recovery phrase")
+ }
+
+ Button {
+ objectName: "walletContinueButton"
+ Layout.fillWidth: true
+ text: qsTr("Continue")
+ enabled: acknowledgement.checked
+ onClicked: root.close()
+ }
+ }
+ }
+}
diff --git a/apps/shared/wallet/qml/internal/WalletIconButton.qml b/apps/shared/wallet/qml/internal/WalletIconButton.qml
new file mode 100644
index 00000000..159d5e10
--- /dev/null
+++ b/apps/shared/wallet/qml/internal/WalletIconButton.qml
@@ -0,0 +1,31 @@
+import QtQuick
+import QtQuick.Controls.Basic
+
+Button {
+ id: root
+
+ property url iconSource
+ property string accessibleName: ""
+ property color iconColor: "#d4d4d8"
+
+ implicitWidth: 36
+ implicitHeight: 36
+ display: AbstractButton.IconOnly
+ hoverEnabled: true
+
+ Accessible.name: root.accessibleName
+ ToolTip.text: root.accessibleName
+ ToolTip.visible: root.hovered && root.accessibleName.length > 0
+
+ icon.source: root.iconSource
+ icon.width: 18
+ icon.height: 18
+ icon.color: root.iconColor
+
+ background: Rectangle {
+ color: root.pressed ? "#3f3f46" : root.hovered || root.activeFocus ? "#27272a" : "transparent"
+ radius: 6
+ border.width: root.activeFocus ? 1 : 0
+ border.color: "#f26a21"
+ }
+}
diff --git a/apps/shared/wallet/qml/internal/WalletMessageDialog.qml b/apps/shared/wallet/qml/internal/WalletMessageDialog.qml
new file mode 100644
index 00000000..7f7a3467
--- /dev/null
+++ b/apps/shared/wallet/qml/internal/WalletMessageDialog.qml
@@ -0,0 +1,52 @@
+import QtQuick
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+
+Popup {
+ id: root
+
+ property string title: qsTr("Wallet error")
+ property string message: ""
+
+ modal: true
+ dim: true
+ parent: Overlay.overlay
+ width: Math.max(0, Math.min(380, parent ? parent.width - 32 : 380))
+ x: parent ? Math.max(0, Math.round((parent.width - width) / 2)) : 0
+ y: parent ? Math.max(0, Math.round((parent.height - height) / 2)) : 0
+ padding: 20
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ background: Rectangle {
+ color: "#18181b"
+ border.color: "#3f3f46"
+ border.width: 1
+ radius: 8
+ }
+
+ contentItem: ColumnLayout {
+ spacing: 16
+
+ Label {
+ Layout.fillWidth: true
+ text: root.title
+ color: "#f4f4f5"
+ font.bold: true
+ font.pixelSize: 17
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: root.message
+ color: "#d4d4d8"
+ wrapMode: Text.WordWrap
+ }
+
+ Button {
+ Layout.alignment: Qt.AlignRight
+ text: qsTr("Close")
+ onClicked: root.close()
+ }
+ }
+}
diff --git a/apps/amm/qml/components/wallet/icons/account.svg b/apps/shared/wallet/qml/internal/icons/account.svg
similarity index 100%
rename from apps/amm/qml/components/wallet/icons/account.svg
rename to apps/shared/wallet/qml/internal/icons/account.svg
diff --git a/apps/amm/qml/components/wallet/icons/back.svg b/apps/shared/wallet/qml/internal/icons/back.svg
similarity index 100%
rename from apps/amm/qml/components/wallet/icons/back.svg
rename to apps/shared/wallet/qml/internal/icons/back.svg
diff --git a/apps/amm/qml/components/wallet/icons/checkmark.svg b/apps/shared/wallet/qml/internal/icons/checkmark.svg
similarity index 100%
rename from apps/amm/qml/components/wallet/icons/checkmark.svg
rename to apps/shared/wallet/qml/internal/icons/checkmark.svg
diff --git a/apps/amm/qml/components/wallet/icons/copy.svg b/apps/shared/wallet/qml/internal/icons/copy.svg
similarity index 100%
rename from apps/amm/qml/components/wallet/icons/copy.svg
rename to apps/shared/wallet/qml/internal/icons/copy.svg
diff --git a/apps/amm/qml/components/wallet/icons/power.svg b/apps/shared/wallet/qml/internal/icons/power.svg
similarity index 100%
rename from apps/amm/qml/components/wallet/icons/power.svg
rename to apps/shared/wallet/qml/internal/icons/power.svg
diff --git a/apps/shared/wallet/src/LogosWalletProvider.cpp b/apps/shared/wallet/src/LogosWalletProvider.cpp
new file mode 100644
index 00000000..3e569525
--- /dev/null
+++ b/apps/shared/wallet/src/LogosWalletProvider.cpp
@@ -0,0 +1,382 @@
+#include "LogosWalletProvider.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "logos_sdk.h"
+
+namespace {
+constexpr int WALLET_FFI_SUCCESS = 0;
+
+bool isHex(const QString& value, qsizetype size, bool lowercaseOnly = true)
+{
+ if (value.size() != size)
+ return false;
+
+ for (const QChar character : value) {
+ const bool digit = character >= QLatin1Char('0')
+ && character <= QLatin1Char('9');
+ const bool lowercase = character >= QLatin1Char('a')
+ && character <= QLatin1Char('f');
+ const bool uppercase = character >= QLatin1Char('A')
+ && character <= QLatin1Char('F');
+ if (!digit && !lowercase && (!uppercase || lowercaseOnly))
+ return false;
+ }
+ return true;
+}
+
+QString littleEndianU128ToDecimal(const QString& value)
+{
+ if (!isHex(value, 32))
+ return {};
+
+ QString decimal = QStringLiteral("0");
+ for (int byteIndex = 15; byteIndex >= 0; --byteIndex) {
+ bool parsed = false;
+ int carry = value.mid(byteIndex * 2, 2).toInt(&parsed, 16);
+ if (!parsed)
+ return {};
+
+ for (qsizetype digit = decimal.size(); digit-- > 0;) {
+ const int next = (decimal.at(digit).unicode() - QLatin1Char('0').unicode())
+ * 256 + carry;
+ decimal[digit] = QLatin1Char(static_cast('0' + next % 10));
+ carry = next / 10;
+ }
+ while (carry > 0) {
+ decimal.prepend(QLatin1Char(static_cast('0' + carry % 10)));
+ carry /= 10;
+ }
+ }
+
+ return decimal;
+}
+
+WalletSession failedSession(WalletFailure failure)
+{
+ WalletSession session;
+ session.failure = failure;
+ session.snapshot.failure = failure;
+ return session;
+}
+
+WalletCreation failedCreation(WalletFailure failure)
+{
+ WalletCreation creation;
+ creation.failure = failure;
+ creation.snapshot.failure = failure;
+ return creation;
+}
+}
+
+struct LogosWalletProvider::Impl {
+ explicit Impl(LogosAPI* api)
+ : ownedLogos(std::make_unique(api)), logos(ownedLogos.get())
+ {
+ }
+
+ explicit Impl(LogosModules* value)
+ : logos(value)
+ {
+ }
+
+ std::unique_ptr ownedLogos;
+ LogosModules* logos = nullptr;
+};
+
+LogosWalletProvider::LogosWalletProvider(LogosAPI* api)
+ : m_impl(std::make_unique(api))
+{
+}
+
+LogosWalletProvider::LogosWalletProvider(LogosModules* logos)
+ : m_impl(std::make_unique(logos))
+{
+}
+
+LogosWalletProvider::~LogosWalletProvider()
+{
+ if (m_connected)
+ save();
+}
+
+WalletSession LogosWalletProvider::connect(const WalletPaths& paths)
+{
+ clearSnapshot();
+ if (!m_impl->logos)
+ return failedSession(WalletFailure::WalletUnavailable);
+
+ WalletSession session;
+ if (sharedWalletIsOpen()) {
+ session.adopted = true;
+ } else {
+ if (!QFileInfo::exists(paths.storage))
+ return failedSession(WalletFailure::WalletMissing);
+ if (m_impl->logos->logos_execution_zone.open(paths.config, paths.storage)
+ != WALLET_FFI_SUCCESS) {
+ return failedSession(WalletFailure::OpenFailed);
+ }
+ }
+
+ m_connected = true;
+ session.snapshot = snapshot(true);
+ session.failure = session.snapshot.failure;
+ return session;
+}
+
+WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths,
+ const QString& password)
+{
+ clearSnapshot();
+ if (!m_impl->logos)
+ return failedCreation(WalletFailure::WalletUnavailable);
+
+ const QFileInfo configInfo(paths.config);
+ const QFileInfo storageInfo(paths.storage);
+ if (!QDir().mkpath(configInfo.absolutePath())
+ || !QDir().mkpath(storageInfo.absolutePath())) {
+ return failedCreation(WalletFailure::CreateFailed);
+ }
+
+ WalletCreation creation;
+ creation.mnemonic = m_impl->logos->logos_execution_zone.create_new(
+ paths.config, paths.storage, password);
+ if (creation.mnemonic.isEmpty())
+ return failedCreation(WalletFailure::CreateFailed);
+
+ m_connected = true;
+ if (!save()) {
+ creation.failure = WalletFailure::SaveFailed;
+ creation.snapshot.failure = creation.failure;
+ return creation;
+ }
+
+ creation.snapshot = snapshot(true);
+ creation.failure = creation.snapshot.failure;
+ return creation;
+}
+
+WalletSnapshot LogosWalletProvider::snapshot(bool forceRefresh)
+{
+ if (m_snapshotReady && !forceRefresh)
+ return m_snapshot;
+ if (!m_connected) {
+ WalletSnapshot result;
+ result.failure = WalletFailure::WalletUnavailable;
+ return result;
+ }
+
+ WalletSnapshot result = loadSnapshot();
+ if (result.ok()) {
+ m_snapshot = result;
+ m_snapshotReady = true;
+ }
+ return result;
+}
+
+void LogosWalletProvider::clearSnapshot()
+{
+ m_snapshot = {};
+ m_snapshotReady = false;
+}
+
+WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic)
+{
+ WalletAccountCreation creation;
+ if (!m_connected || !m_impl->logos) {
+ creation.failure = WalletFailure::WalletUnavailable;
+ return creation;
+ }
+
+ creation.accountId = isPublic
+ ? m_impl->logos->logos_execution_zone.create_account_public()
+ : m_impl->logos->logos_execution_zone.create_account_private();
+ if (!isHex(creation.accountId, 64)) {
+ creation.failure = WalletFailure::CreateFailed;
+ return creation;
+ }
+ if (!save()) {
+ creation.failure = WalletFailure::SaveFailed;
+ return creation;
+ }
+
+ if (isPublic)
+ creation.publicAccount = readPublicAccount(creation.accountId);
+
+ clearSnapshot();
+ creation.snapshot = snapshot(true);
+ return creation;
+}
+
+WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const
+{
+ WalletAccountRead read;
+ read.accountId = accountId;
+ if (!m_impl->logos || !isHex(accountId, 64))
+ return read;
+
+ QJsonParseError parseError;
+ const QJsonDocument document = QJsonDocument::fromJson(
+ m_impl->logos->logos_execution_zone.get_account_public(accountId).toUtf8(),
+ &parseError);
+ if (parseError.error != QJsonParseError::NoError || !document.isObject())
+ return read;
+
+ const QJsonObject account = document.object();
+ const QString owner = account.value(QStringLiteral("program_owner")).toString();
+ const QString balance = account.value(QStringLiteral("balance")).toString();
+ const QString nonce = account.value(QStringLiteral("nonce")).toString();
+ const QString data = account.value(QStringLiteral("data")).toString();
+ if (!isHex(owner, 64)
+ || !isHex(balance, 32)
+ || !isHex(nonce, 32)
+ || data.size() % 2 != 0
+ || !isHex(data, data.size())) {
+ return read;
+ }
+
+ read.status = QStringLiteral("ok");
+ read.programOwner = owner;
+ read.balanceHex = balance;
+ read.nonceHex = nonce;
+ read.dataHex = data;
+ return read;
+}
+
+WalletSubmission LogosWalletProvider::submitPublicTransaction(
+ const WalletTransaction& transaction)
+{
+ WalletSubmission submission;
+ if (!m_connected || !m_impl->logos) {
+ submission.failure = WalletFailure::WalletUnavailable;
+ return submission;
+ }
+ if (!isHex(transaction.programId, 64)
+ || transaction.accountIds.size() != transaction.signingRequirements.size()) {
+ submission.failure = WalletFailure::InvalidRequest;
+ return submission;
+ }
+ for (const QString& accountId : transaction.accountIds) {
+ if (!isHex(accountId, 64)) {
+ submission.failure = WalletFailure::InvalidRequest;
+ return submission;
+ }
+ }
+
+ QVariantList signingRequirements;
+ signingRequirements.reserve(transaction.signingRequirements.size());
+ for (bool required : transaction.signingRequirements)
+ signingRequirements.append(required);
+
+ QVariantList instruction;
+ instruction.reserve(transaction.instruction.size());
+ for (quint32 word : transaction.instruction)
+ instruction.append(word);
+
+ const QString response =
+ m_impl->logos->logos_execution_zone.send_generic_public_transaction(
+ transaction.accountIds,
+ signingRequirements,
+ QVariant::fromValue(instruction),
+ transaction.programId);
+
+ QJsonParseError parseError;
+ const QJsonDocument document = QJsonDocument::fromJson(response.toUtf8(), &parseError);
+ if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
+ submission.failure = WalletFailure::SubmissionFailed;
+ return submission;
+ }
+
+ const QJsonObject result = document.object();
+ const QJsonValue success = result.value(QStringLiteral("success"));
+ const QJsonValue error = result.value(QStringLiteral("error"));
+ const QString hash = result.value(QStringLiteral("tx_hash")).toString();
+ const bool emptyError = error.isUndefined()
+ || error.isNull()
+ || (error.isString() && error.toString().isEmpty());
+ if (!success.isBool()
+ || !success.toBool()
+ || !emptyError
+ || !isHex(hash, 64, false)) {
+ submission.failure = WalletFailure::SubmissionFailed;
+ return submission;
+ }
+
+ submission.nativeHash = hash.toLower();
+ return submission;
+}
+
+void LogosWalletProvider::disconnect()
+{
+ if (m_connected)
+ save();
+ clearSnapshot();
+ m_connected = false;
+}
+
+bool LogosWalletProvider::sharedWalletIsOpen() const
+{
+ if (!m_impl->logos)
+ return false;
+ if (!m_impl->logos->logos_execution_zone.get_sequencer_addr().isEmpty())
+ return true;
+ return !m_impl->logos->logos_execution_zone.list_accounts().isEmpty();
+}
+
+WalletSnapshot LogosWalletProvider::loadSnapshot()
+{
+ WalletSnapshot result;
+ result.currentBlockHeight = static_cast(
+ qMax(0, m_impl->logos->logos_execution_zone.get_current_block_height()));
+ if (result.currentBlockHeight > 0
+ && m_impl->logos->logos_execution_zone.sync_to_block(result.currentBlockHeight)
+ != WALLET_FFI_SUCCESS) {
+ result.failure = WalletFailure::ReadFailed;
+ return result;
+ }
+ result.lastSyncedBlock = static_cast(
+ qMax(0, m_impl->logos->logos_execution_zone.get_last_synced_block()));
+ result.sequencerAddress = m_impl->logos->logos_execution_zone.get_sequencer_addr();
+
+ const QVariantList entries = m_impl->logos->logos_execution_zone.list_accounts();
+ result.accounts.reserve(entries.size());
+ result.publicAccountReads.reserve(entries.size());
+ for (const QVariant& value : entries) {
+ const QVariantMap entry = value.toMap();
+ const QString address = entry.value(QStringLiteral("account_id")).toString();
+ if (entry.isEmpty() || !isHex(address, 64)) {
+ result.failure = WalletFailure::ReadFailed;
+ return result;
+ }
+
+ WalletAccount account;
+ account.address = address;
+ account.isPublic = entry.value(QStringLiteral("is_public"), true).toBool();
+ if (account.isPublic) {
+ const WalletAccountRead read = readPublicAccount(address);
+ result.publicAccountReads.append(read);
+ account.balance = read.ok()
+ ? littleEndianU128ToDecimal(read.balanceHex)
+ : m_impl->logos->logos_execution_zone.get_balance(address, true);
+ } else {
+ account.balance = m_impl->logos->logos_execution_zone.get_balance(address, false);
+ }
+ result.accounts.append(account);
+ }
+
+ if (!save())
+ result.failure = WalletFailure::SaveFailed;
+ return result;
+}
+
+bool LogosWalletProvider::save() const
+{
+ return m_impl->logos
+ && m_impl->logos->logos_execution_zone.save() == WALLET_FFI_SUCCESS;
+}
diff --git a/apps/shared/wallet/src/LogosWalletProvider.h b/apps/shared/wallet/src/LogosWalletProvider.h
new file mode 100644
index 00000000..d0462906
--- /dev/null
+++ b/apps/shared/wallet/src/LogosWalletProvider.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include
+
+#include "WalletProvider.h"
+
+class LogosAPI;
+struct LogosModules;
+
+class LogosWalletProvider final : public WalletProvider {
+public:
+ explicit LogosWalletProvider(LogosAPI* api);
+ explicit LogosWalletProvider(LogosModules* logos);
+ ~LogosWalletProvider() override;
+
+ WalletSession connect(const WalletPaths& paths) override;
+ WalletCreation createWallet(const WalletPaths& paths,
+ const QString& password) override;
+ WalletSnapshot snapshot(bool forceRefresh = false) override;
+ void clearSnapshot() override;
+ WalletAccountCreation createAccount(bool isPublic) override;
+ WalletAccountRead readPublicAccount(const QString& accountId) const override;
+ WalletSubmission submitPublicTransaction(
+ const WalletTransaction& transaction) override;
+ void disconnect() override;
+
+private:
+ bool sharedWalletIsOpen() const;
+ WalletSnapshot loadSnapshot();
+ bool save() const;
+
+ struct Impl;
+ std::unique_ptr m_impl;
+ WalletSnapshot m_snapshot;
+ bool m_snapshotReady = false;
+ bool m_connected = false;
+};
diff --git a/apps/shared/wallet/src/WalletAccountModel.cpp b/apps/shared/wallet/src/WalletAccountModel.cpp
new file mode 100644
index 00000000..06e94bfd
--- /dev/null
+++ b/apps/shared/wallet/src/WalletAccountModel.cpp
@@ -0,0 +1,61 @@
+#include "WalletAccountModel.h"
+
+WalletAccountModel::WalletAccountModel(QObject* parent)
+ : QAbstractListModel(parent)
+{
+}
+
+int WalletAccountModel::rowCount(const QModelIndex& parent) const
+{
+ return parent.isValid() ? 0 : m_accounts.size();
+}
+
+QVariant WalletAccountModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_accounts.size())
+ return {};
+
+ const Entry& account = m_accounts.at(index.row());
+ switch (role) {
+ case NameRole:
+ return account.name;
+ case AddressRole:
+ return account.address;
+ case BalanceRole:
+ return account.balance;
+ case IsPublicRole:
+ return account.isPublic;
+ default:
+ return {};
+ }
+}
+
+QHash WalletAccountModel::roleNames() const
+{
+ return {
+ { NameRole, "name" },
+ { AddressRole, "address" },
+ { BalanceRole, "balance" },
+ { IsPublicRole, "isPublic" },
+ };
+}
+
+void WalletAccountModel::replaceAccounts(const QVector& accounts)
+{
+ beginResetModel();
+ const qsizetype oldCount = m_accounts.size();
+ m_accounts.clear();
+ m_accounts.reserve(accounts.size());
+ for (qsizetype index = 0; index < accounts.size(); ++index) {
+ const WalletAccount& account = accounts.at(index);
+ m_accounts.append({
+ QStringLiteral("Account %1").arg(index + 1),
+ account.address,
+ account.balance,
+ account.isPublic,
+ });
+ }
+ endResetModel();
+ if (oldCount != m_accounts.size())
+ emit countChanged();
+}
diff --git a/apps/shared/wallet/src/WalletAccountModel.h b/apps/shared/wallet/src/WalletAccountModel.h
new file mode 100644
index 00000000..c00b0d65
--- /dev/null
+++ b/apps/shared/wallet/src/WalletAccountModel.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+#include
+
+#include "WalletProvider.h"
+
+class WalletAccountModel final : public QAbstractListModel {
+ Q_OBJECT
+ Q_PROPERTY(int count READ count NOTIFY countChanged)
+
+public:
+ enum Role {
+ NameRole = Qt::UserRole + 1,
+ AddressRole,
+ BalanceRole,
+ IsPublicRole,
+ };
+ Q_ENUM(Role)
+
+ explicit WalletAccountModel(QObject* parent = nullptr);
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QHash roleNames() const override;
+
+ void replaceAccounts(const QVector& accounts);
+ int count() const { return m_accounts.size(); }
+
+signals:
+ void countChanged();
+
+private:
+ struct Entry {
+ QString name;
+ QString address;
+ QString balance;
+ bool isPublic = true;
+ };
+
+ QVector m_accounts;
+};
diff --git a/apps/shared/wallet/src/WalletController.cpp b/apps/shared/wallet/src/WalletController.cpp
new file mode 100644
index 00000000..5cffdfd5
--- /dev/null
+++ b/apps/shared/wallet/src/WalletController.cpp
@@ -0,0 +1,238 @@
+#include "WalletController.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "WalletAccountModel.h"
+
+namespace {
+const char SETTINGS_ORG[] = "Logos";
+const char DISCONNECTED_KEY[] = "disconnected";
+const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
+
+QString toLocalPath(const QString& path)
+{
+ if (path.startsWith(QStringLiteral("file://")) || path.contains(QLatin1Char('/')))
+ return QUrl::fromUserInput(path).toLocalFile();
+ return path;
+}
+}
+
+WalletController::WalletController(WalletProvider& wallet,
+ QString settingsApplication,
+ QObject* parent)
+ : QObject(parent),
+ m_wallet(wallet),
+ m_settingsApplication(std::move(settingsApplication)),
+ m_accountModel(new WalletAccountModel(this)),
+ m_network(new QNetworkAccessManager(this)),
+ m_reachabilityTimer(new QTimer(this))
+{
+ m_state.walletHome = defaultWalletHome();
+ m_state.walletExists = QFileInfo::exists(defaultStoragePath());
+
+ m_reachabilityTimer->setInterval(10000);
+ connect(m_reachabilityTimer, &QTimer::timeout,
+ this, &WalletController::checkReachability);
+}
+
+WalletController::~WalletController() = default;
+
+QString WalletController::defaultWalletHome()
+{
+ const QByteArray override = qgetenv(WALLET_HOME_ENV);
+ if (!override.isEmpty())
+ return QString::fromLocal8Bit(override);
+ return QDir::homePath() + QStringLiteral("/.lee/wallet");
+}
+
+QString WalletController::defaultConfigPath() const
+{
+ return m_state.walletHome + QStringLiteral("/wallet_config.json");
+}
+
+QString WalletController::defaultStoragePath() const
+{
+ return m_state.walletHome + QStringLiteral("/storage.json");
+}
+
+void WalletController::start()
+{
+ if (m_started)
+ return;
+ m_started = true;
+ m_reachabilityTimer->start();
+ QTimer::singleShot(0, this, &WalletController::openOnStartup);
+}
+
+void WalletController::openOnStartup()
+{
+ if (QSettings(SETTINGS_ORG, m_settingsApplication)
+ .value(DISCONNECTED_KEY, false).toBool()) {
+ return;
+ }
+
+ const QString config = defaultConfigPath();
+ const QString storage = defaultStoragePath();
+ const WalletSession session = m_wallet.connect({ config, storage });
+ if (session.failure == WalletFailure::WalletMissing)
+ return;
+ if (!session.ok()) {
+ qWarning() << "WalletController: wallet connection failed"
+ << walletFailureCode(session.failure);
+ return;
+ }
+
+ m_state.configPath = config;
+ m_state.storagePath = storage;
+ m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
+ m_state.isWalletOpen = true;
+ applySnapshot(session.snapshot);
+}
+
+QString WalletController::createDefaultWallet(const QString& password)
+{
+ return createWallet(defaultConfigPath(), defaultStoragePath(), password);
+}
+
+QString WalletController::createWallet(const QString& configPath,
+ const QString& storagePath,
+ const QString& password)
+{
+ const QString config = toLocalPath(configPath);
+ const QString storage = toLocalPath(storagePath);
+ const WalletCreation creation = m_wallet.createWallet(
+ { config, storage }, password);
+ if (creation.mnemonic.isEmpty()) {
+ qWarning() << "WalletController: wallet creation failed"
+ << walletFailureCode(creation.failure);
+ return {};
+ }
+
+ m_state.configPath = config;
+ m_state.storagePath = storage;
+ m_state.walletExists = true;
+ QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
+ if (!creation.ok()) {
+ qWarning() << "WalletController: wallet creation failed"
+ << walletFailureCode(creation.failure);
+ emit stateChanged();
+ return creation.mnemonic;
+ }
+
+ m_state.isWalletOpen = true;
+ applySnapshot(creation.snapshot);
+ return creation.mnemonic;
+}
+
+bool WalletController::open()
+{
+ const QString config = m_state.configPath.isEmpty()
+ ? defaultConfigPath() : m_state.configPath;
+ const QString storage = m_state.storagePath.isEmpty()
+ ? defaultStoragePath() : m_state.storagePath;
+ const WalletSession session = m_wallet.connect({ config, storage });
+ if (!session.ok()) {
+ qWarning() << "WalletController: wallet open failed"
+ << walletFailureCode(session.failure);
+ return false;
+ }
+
+ m_state.configPath = config;
+ m_state.storagePath = storage;
+ m_state.walletExists = true;
+ m_state.isWalletOpen = true;
+ QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
+ applySnapshot(session.snapshot);
+ return true;
+}
+
+void WalletController::disconnect()
+{
+ m_wallet.disconnect();
+ m_state.isWalletOpen = false;
+ m_accountModel->replaceAccounts({});
+ QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, true);
+ emit stateChanged();
+}
+
+QString WalletController::createAccount(bool isPublic)
+{
+ const WalletAccountCreation creation = m_wallet.createAccount(isPublic);
+ if (!creation.ok()) {
+ qWarning() << "WalletController: account creation failed"
+ << walletFailureCode(creation.failure);
+ return {};
+ }
+ if (creation.snapshot.ok()) {
+ applySnapshot(creation.snapshot);
+ } else {
+ qWarning() << "WalletController: account refresh failed"
+ << walletFailureCode(creation.snapshot.failure);
+ }
+ return creation.accountId;
+}
+
+void WalletController::refresh()
+{
+ const WalletSnapshot next = m_wallet.snapshot(true);
+ if (next.ok()) {
+ applySnapshot(next);
+ } else {
+ qWarning() << "WalletController: wallet refresh failed"
+ << walletFailureCode(next.failure);
+ }
+}
+
+QString WalletController::balance(const QString& accountId, bool isPublic)
+{
+ const WalletSnapshot current = m_wallet.snapshot();
+ for (const WalletAccount& account : current.accounts) {
+ if (account.address == accountId && account.isPublic == isPublic)
+ return account.balance;
+ }
+ return {};
+}
+
+void WalletController::applySnapshot(const WalletSnapshot& snapshot)
+{
+ m_accountModel->replaceAccounts(snapshot.accounts);
+ m_state.lastSyncedBlock = static_cast(snapshot.lastSyncedBlock);
+ m_state.currentBlockHeight = static_cast(snapshot.currentBlockHeight);
+ m_state.sequencerAddress = snapshot.sequencerAddress;
+ emit stateChanged();
+ checkReachability();
+}
+
+void WalletController::checkReachability()
+{
+ if (!m_state.isWalletOpen || m_state.sequencerAddress.isEmpty())
+ return;
+
+ QNetworkRequest request{QUrl(m_state.sequencerAddress)};
+ request.setTransferTimeout(4000);
+ QNetworkReply* reply = m_network->get(request);
+ connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+ if (!m_state.isWalletOpen) {
+ reply->deleteLater();
+ return;
+ }
+ const bool receivedHttp =
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
+ const bool reachable = receivedHttp || reply->error() == QNetworkReply::NoError;
+ if (m_state.sequencerReachable != reachable) {
+ m_state.sequencerReachable = reachable;
+ emit stateChanged();
+ }
+ reply->deleteLater();
+ });
+}
diff --git a/apps/shared/wallet/src/WalletController.h b/apps/shared/wallet/src/WalletController.h
new file mode 100644
index 00000000..c27f9cef
--- /dev/null
+++ b/apps/shared/wallet/src/WalletController.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include
+#include
+
+#include "WalletProvider.h"
+
+class QNetworkAccessManager;
+class QTimer;
+class WalletAccountModel;
+
+struct WalletUiState {
+ bool isWalletOpen = false;
+ bool walletExists = false;
+ QString configPath;
+ QString storagePath;
+ QString walletHome;
+ int lastSyncedBlock = 0;
+ int currentBlockHeight = 0;
+ QString sequencerAddress;
+ bool sequencerReachable = true;
+};
+
+class WalletController final : public QObject {
+ Q_OBJECT
+
+public:
+ // The provider must outlive the controller.
+ explicit WalletController(WalletProvider& wallet,
+ QString settingsApplication,
+ QObject* parent = nullptr);
+ ~WalletController() override;
+
+ WalletAccountModel* accountModel() const { return m_accountModel; }
+ const WalletUiState& state() const { return m_state; }
+
+ void start();
+ QString createAccount(bool isPublic);
+ void refresh();
+ QString balance(const QString& accountId, bool isPublic);
+ QString createDefaultWallet(const QString& password);
+ QString createWallet(const QString& configPath,
+ const QString& storagePath,
+ const QString& password);
+ bool open();
+ void disconnect();
+
+signals:
+ void stateChanged();
+
+private:
+ static QString defaultWalletHome();
+ QString defaultConfigPath() const;
+ QString defaultStoragePath() const;
+
+ void openOnStartup();
+ void applySnapshot(const WalletSnapshot& snapshot);
+ void checkReachability();
+
+ WalletProvider& m_wallet;
+ QString m_settingsApplication;
+ WalletUiState m_state;
+ WalletAccountModel* m_accountModel;
+ QNetworkAccessManager* m_network;
+ QTimer* m_reachabilityTimer;
+ bool m_started = false;
+};
diff --git a/apps/shared/wallet/src/WalletProvider.cpp b/apps/shared/wallet/src/WalletProvider.cpp
new file mode 100644
index 00000000..9cb259d4
--- /dev/null
+++ b/apps/shared/wallet/src/WalletProvider.cpp
@@ -0,0 +1,26 @@
+#include "WalletProvider.h"
+
+QString walletFailureCode(WalletFailure failure)
+{
+ switch (failure) {
+ case WalletFailure::None:
+ return {};
+ case WalletFailure::WalletMissing:
+ return QStringLiteral("wallet_missing");
+ case WalletFailure::WalletUnavailable:
+ return QStringLiteral("wallet_unavailable");
+ case WalletFailure::OpenFailed:
+ return QStringLiteral("open_failed");
+ case WalletFailure::CreateFailed:
+ return QStringLiteral("create_failed");
+ case WalletFailure::SaveFailed:
+ return QStringLiteral("save_failed");
+ case WalletFailure::ReadFailed:
+ return QStringLiteral("read_failed");
+ case WalletFailure::InvalidRequest:
+ return QStringLiteral("invalid_request");
+ case WalletFailure::SubmissionFailed:
+ return QStringLiteral("submission_failed");
+ }
+ return QStringLiteral("wallet_unavailable");
+}
diff --git a/apps/shared/wallet/src/WalletProvider.h b/apps/shared/wallet/src/WalletProvider.h
new file mode 100644
index 00000000..a7814caa
--- /dev/null
+++ b/apps/shared/wallet/src/WalletProvider.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include
+#include
+#include
+
+enum class WalletFailure {
+ None,
+ WalletMissing,
+ WalletUnavailable,
+ OpenFailed,
+ CreateFailed,
+ SaveFailed,
+ ReadFailed,
+ InvalidRequest,
+ SubmissionFailed,
+};
+
+QString walletFailureCode(WalletFailure failure);
+
+struct WalletPaths {
+ QString config;
+ QString storage;
+};
+
+struct WalletAccountRead {
+ QString accountId;
+ QString status = QStringLiteral("read_failed");
+ QString programOwner;
+ QString balanceHex;
+ QString nonceHex;
+ QString dataHex;
+
+ bool ok() const { return status == QStringLiteral("ok"); }
+};
+
+struct WalletAccount {
+ QString address;
+ QString balance;
+ bool isPublic = true;
+};
+
+struct WalletSnapshot {
+ WalletFailure failure = WalletFailure::None;
+ QVector accounts;
+ QVector publicAccountReads;
+ quint64 lastSyncedBlock = 0;
+ quint64 currentBlockHeight = 0;
+ QString sequencerAddress;
+
+ bool ok() const { return failure == WalletFailure::None; }
+};
+
+struct WalletSession {
+ WalletFailure failure = WalletFailure::None;
+ WalletSnapshot snapshot;
+ bool adopted = false;
+
+ bool ok() const { return failure == WalletFailure::None; }
+};
+
+struct WalletCreation {
+ WalletFailure failure = WalletFailure::None;
+ QString mnemonic;
+ WalletSnapshot snapshot;
+
+ bool ok() const { return failure == WalletFailure::None; }
+};
+
+struct WalletAccountCreation {
+ WalletFailure failure = WalletFailure::None;
+ QString accountId;
+ WalletAccountRead publicAccount;
+ WalletSnapshot snapshot;
+
+ bool ok() const { return failure == WalletFailure::None; }
+};
+
+struct WalletTransaction {
+ QString programId;
+ QStringList accountIds;
+ QVector signingRequirements;
+ QVector instruction;
+};
+
+struct WalletSubmission {
+ WalletFailure failure = WalletFailure::None;
+ QString nativeHash;
+
+ bool accepted() const { return failure == WalletFailure::None && !nativeHash.isEmpty(); }
+};
+
+class WalletProvider {
+public:
+ virtual ~WalletProvider() = default;
+
+ virtual WalletSession connect(const WalletPaths& paths) = 0;
+ virtual WalletCreation createWallet(const WalletPaths& paths,
+ const QString& password) = 0;
+ virtual WalletSnapshot snapshot(bool forceRefresh = false) = 0;
+ virtual void clearSnapshot() = 0;
+ virtual WalletAccountCreation createAccount(bool isPublic) = 0;
+ virtual WalletAccountRead readPublicAccount(const QString& accountId) const = 0;
+ virtual WalletSubmission submitPublicTransaction(
+ const WalletTransaction& transaction) = 0;
+ virtual void disconnect() = 0;
+};
diff --git a/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp b/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp
new file mode 100644
index 00000000..9701d6b5
--- /dev/null
+++ b/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp
@@ -0,0 +1,452 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "FakeWalletProvider.h"
+#include "LogosWalletProvider.h"
+#include "WalletAccountModel.h"
+#include "WalletController.h"
+#include "logos_sdk.h"
+
+namespace {
+const QString ACCOUNT_A(64, QLatin1Char('a'));
+const QString ACCOUNT_B(64, QLatin1Char('b'));
+const QString PROGRAM_ID(64, QLatin1Char('c'));
+
+QString publicAccountJson(const QString& owner = PROGRAM_ID,
+ const QString& balance = QStringLiteral("01000000000000000000000000000000"),
+ const QString& nonce = QString(32, QLatin1Char('0')),
+ const QString& data = QStringLiteral("00ff"))
+{
+ return QString::fromUtf8(QJsonDocument(QJsonObject {
+ { QStringLiteral("program_owner"), owner },
+ { QStringLiteral("balance"), balance },
+ { QStringLiteral("nonce"), nonce },
+ { QStringLiteral("data"), data },
+ }).toJson(QJsonDocument::Compact));
+}
+
+QVariantMap accountEntry(const QString& id, bool isPublic)
+{
+ return {
+ { QStringLiteral("account_id"), id },
+ { QStringLiteral("is_public"), isPublic },
+ };
+}
+}
+
+class LogosWalletProviderTest : public QObject {
+ Q_OBJECT
+
+private slots:
+ void adoptsOpenWalletAndCachesSnapshots();
+ void opensConfiguredWalletWhenNoSharedSessionExists();
+ void createsAndPersistsWallet();
+ void validatesCompletePublicAccountPayloads();
+ void fallsBackToBalanceWhenPublicReadFails();
+ void createsAndPersistsAccounts();
+ void preservesCreatedAccountWhenPublicReadFails();
+ void preservesCreatedAccountWhenSnapshotRefreshFails();
+ void dispatchesExactGenericTransaction();
+ void rejectsInvalidSubmissionResponses();
+ void exposesStableAccountModelRoles();
+ void fakeProviderImplementsConsumerContract();
+ void controllerOwnsUiWalletFlow();
+ void controllerStopsReachabilityChecksAfterDisconnect();
+};
+
+void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
+ modules.logos_execution_zone.currentBlockHeight = 12;
+ modules.logos_execution_zone.lastSyncedBlock = 11;
+ modules.logos_execution_zone.accounts = {
+ accountEntry(ACCOUNT_A, true),
+ accountEntry(ACCOUNT_B, false),
+ };
+ modules.logos_execution_zone.publicAccounts.insert(
+ ACCOUNT_A, publicAccountJson());
+ modules.logos_execution_zone.balances.insert(ACCOUNT_B, QStringLiteral("42"));
+
+ LogosWalletProvider provider(&modules);
+ const WalletSession session = provider.connect({ QStringLiteral("unused"), QStringLiteral("unused") });
+
+ QVERIFY(session.ok());
+ QVERIFY(session.adopted);
+ QCOMPARE(session.snapshot.accounts.size(), 2);
+ QCOMPARE(session.snapshot.accounts.at(0).balance, QStringLiteral("1"));
+ QCOMPARE(session.snapshot.accounts.at(1).balance, QStringLiteral("42"));
+ QCOMPARE(session.snapshot.publicAccountReads.size(), 1);
+ QCOMPARE(session.snapshot.currentBlockHeight, quint64(12));
+ QCOMPARE(session.snapshot.lastSyncedBlock, quint64(11));
+
+ const int listCalls = modules.logos_execution_zone.listCalls;
+ const int readCalls = modules.logos_execution_zone.publicReadCalls;
+ QVERIFY(provider.snapshot().ok());
+ QCOMPARE(modules.logos_execution_zone.listCalls, listCalls);
+ QCOMPARE(modules.logos_execution_zone.publicReadCalls, readCalls);
+
+ QVERIFY(provider.snapshot(true).ok());
+ QVERIFY(modules.logos_execution_zone.listCalls > listCalls);
+ QVERIFY(modules.logos_execution_zone.publicReadCalls > readCalls);
+
+ modules.logos_execution_zone.publicAccounts[ACCOUNT_A] = publicAccountJson(
+ PROGRAM_ID, QString(32, QLatin1Char('f')));
+ QCOMPARE(provider.snapshot(true).accounts.at(0).balance,
+ QStringLiteral("340282366920938463463374607431768211455"));
+
+ provider.clearSnapshot();
+ const int afterRefresh = modules.logos_execution_zone.listCalls;
+ QVERIFY(provider.snapshot().ok());
+ QVERIFY(modules.logos_execution_zone.listCalls > afterRefresh);
+
+ provider.disconnect();
+ QCOMPARE(provider.snapshot().failure, WalletFailure::WalletUnavailable);
+}
+
+void LogosWalletProviderTest::opensConfiguredWalletWhenNoSharedSessionExists()
+{
+ QTemporaryDir directory;
+ QVERIFY(directory.isValid());
+ const QString storage = directory.filePath(QStringLiteral("storage.json"));
+ QFile file(storage);
+ QVERIFY(file.open(QIODevice::WriteOnly));
+ file.close();
+
+ LogosModules modules;
+ LogosWalletProvider provider(&modules);
+ const WalletSession session = provider.connect({
+ directory.filePath(QStringLiteral("wallet.json")),
+ storage,
+ });
+
+ QVERIFY(session.ok());
+ QVERIFY(!session.adopted);
+ QCOMPARE(modules.logos_execution_zone.openCalls, 1);
+ QCOMPARE(modules.logos_execution_zone.openedStorage, storage);
+
+ LogosModules missingModules;
+ LogosWalletProvider missingProvider(&missingModules);
+ QCOMPARE(missingProvider.connect({ QStringLiteral("config"), QStringLiteral("missing") }).failure,
+ WalletFailure::WalletMissing);
+}
+
+void LogosWalletProviderTest::createsAndPersistsWallet()
+{
+ QTemporaryDir directory;
+ QVERIFY(directory.isValid());
+
+ LogosModules modules;
+ LogosWalletProvider provider(&modules);
+ const WalletPaths paths {
+ directory.filePath(QStringLiteral("config/wallet.json")),
+ directory.filePath(QStringLiteral("state/storage.json")),
+ };
+ const WalletCreation creation = provider.createWallet(paths, QStringLiteral("secret"));
+
+ QVERIFY(creation.ok());
+ QCOMPARE(creation.mnemonic, modules.logos_execution_zone.mnemonic);
+ QCOMPARE(modules.logos_execution_zone.createdConfig, paths.config);
+ QCOMPARE(modules.logos_execution_zone.createdStorage, paths.storage);
+ QCOMPARE(modules.logos_execution_zone.createdPassword, QStringLiteral("secret"));
+ QVERIFY(modules.logos_execution_zone.saveCalls >= 1);
+
+ LogosModules rejectedModules;
+ rejectedModules.logos_execution_zone.mnemonic.clear();
+ LogosWalletProvider rejectedProvider(&rejectedModules);
+ QCOMPARE(rejectedProvider.createWallet(paths, QStringLiteral("secret")).failure,
+ WalletFailure::CreateFailed);
+
+ LogosModules unsavedModules;
+ unsavedModules.logos_execution_zone.saveResult = 1;
+ LogosWalletProvider unsavedProvider(&unsavedModules);
+ const WalletCreation unsaved = unsavedProvider.createWallet(paths, QStringLiteral("secret"));
+ QCOMPARE(unsaved.failure, WalletFailure::SaveFailed);
+ QCOMPARE(unsaved.mnemonic, unsavedModules.logos_execution_zone.mnemonic);
+}
+
+void LogosWalletProviderTest::validatesCompletePublicAccountPayloads()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
+ LogosWalletProvider provider(&modules);
+
+ const WalletAccountRead valid = provider.readPublicAccount(ACCOUNT_A);
+ QVERIFY(valid.ok());
+ QCOMPARE(valid.accountId, ACCOUNT_A);
+ QCOMPARE(valid.programOwner, PROGRAM_ID);
+ QCOMPARE(valid.balanceHex, QStringLiteral("01000000000000000000000000000000"));
+ QCOMPARE(valid.dataHex, QStringLiteral("00ff"));
+
+ modules.logos_execution_zone.publicAccounts[ACCOUNT_A] = publicAccountJson(PROGRAM_ID.toUpper());
+ QVERIFY(!provider.readPublicAccount(ACCOUNT_A).ok());
+ modules.logos_execution_zone.publicAccounts[ACCOUNT_A] = publicAccountJson(
+ PROGRAM_ID, QStringLiteral("01"));
+ QVERIFY(!provider.readPublicAccount(ACCOUNT_A).ok());
+ modules.logos_execution_zone.publicAccounts[ACCOUNT_A] = publicAccountJson(
+ PROGRAM_ID, QStringLiteral("01000000000000000000000000000000"),
+ QString(32, QLatin1Char('0')), QStringLiteral("abc"));
+ QVERIFY(!provider.readPublicAccount(ACCOUNT_A).ok());
+ modules.logos_execution_zone.publicAccounts[ACCOUNT_A] = QStringLiteral("[]");
+ QVERIFY(!provider.readPublicAccount(ACCOUNT_A).ok());
+ QVERIFY(!provider.readPublicAccount(QStringLiteral("invalid")).ok());
+}
+
+void LogosWalletProviderTest::fallsBackToBalanceWhenPublicReadFails()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
+ modules.logos_execution_zone.accounts = { accountEntry(ACCOUNT_A, true) };
+ modules.logos_execution_zone.balances.insert(ACCOUNT_A, QStringLiteral("42"));
+
+ LogosWalletProvider provider(&modules);
+ const WalletSession session = provider.connect({});
+
+ QVERIFY(session.ok());
+ QCOMPARE(session.snapshot.accounts.size(), 1);
+ QCOMPARE(session.snapshot.accounts.at(0).balance, QStringLiteral("42"));
+ QCOMPARE(session.snapshot.publicAccountReads.size(), 1);
+ QVERIFY(!session.snapshot.publicAccountReads.at(0).ok());
+}
+
+void LogosWalletProviderTest::createsAndPersistsAccounts()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
+ modules.logos_execution_zone.publicAccountId = ACCOUNT_A;
+ modules.logos_execution_zone.accounts = { accountEntry(ACCOUNT_A, true) };
+ modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
+ LogosWalletProvider provider(&modules);
+ QVERIFY(provider.connect({}).ok());
+
+ const int savesBeforeCreate = modules.logos_execution_zone.saveCalls;
+ const WalletAccountCreation creation = provider.createAccount(true);
+ QVERIFY(creation.ok());
+ QCOMPARE(creation.accountId, ACCOUNT_A);
+ QVERIFY(creation.publicAccount.ok());
+ QCOMPARE(creation.snapshot.accounts.size(), 1);
+ QVERIFY(modules.logos_execution_zone.saveCalls > savesBeforeCreate);
+
+ modules.logos_execution_zone.saveResult = 1;
+ QCOMPARE(provider.createAccount(true).failure, WalletFailure::SaveFailed);
+}
+
+void LogosWalletProviderTest::preservesCreatedAccountWhenPublicReadFails()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
+ modules.logos_execution_zone.publicAccountId = ACCOUNT_A;
+ modules.logos_execution_zone.accounts = { accountEntry(ACCOUNT_A, true) };
+ modules.logos_execution_zone.balances.insert(ACCOUNT_A, QStringLiteral("7"));
+ LogosWalletProvider provider(&modules);
+ QVERIFY(provider.connect({}).ok());
+
+ const WalletAccountCreation creation = provider.createAccount(true);
+
+ QVERIFY(creation.ok());
+ QCOMPARE(creation.accountId, ACCOUNT_A);
+ QVERIFY(!creation.publicAccount.ok());
+ QCOMPARE(creation.snapshot.accounts.size(), 1);
+ QCOMPARE(creation.snapshot.accounts.at(0).balance, QStringLiteral("7"));
+}
+
+void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
+ modules.logos_execution_zone.publicAccountId = ACCOUNT_A;
+ modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
+ LogosWalletProvider provider(&modules);
+ QVERIFY(provider.connect({}).ok());
+
+ modules.logos_execution_zone.currentBlockHeight = 1;
+ modules.logos_execution_zone.syncResult = 1;
+ const WalletAccountCreation creation = provider.createAccount(true);
+
+ QVERIFY(creation.ok());
+ QCOMPARE(creation.accountId, ACCOUNT_A);
+ QCOMPARE(creation.snapshot.failure, WalletFailure::ReadFailed);
+}
+
+void LogosWalletProviderTest::dispatchesExactGenericTransaction()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
+ modules.logos_execution_zone.transactionResponse = QStringLiteral(
+ R"({"success":true,"tx_hash":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"})");
+ LogosWalletProvider provider(&modules);
+ QVERIFY(provider.connect({}).ok());
+
+ WalletTransaction transaction;
+ transaction.programId = PROGRAM_ID;
+ transaction.accountIds = { ACCOUNT_A, ACCOUNT_B };
+ transaction.signingRequirements = { true, false };
+ transaction.instruction = { 7, 0, 4294967295U };
+
+ const WalletSubmission submission = provider.submitPublicTransaction(transaction);
+ QVERIFY(submission.accepted());
+ QCOMPARE(submission.nativeHash, QString(64, QLatin1Char('a')));
+ QCOMPARE(modules.logos_execution_zone.submitCalls, 1);
+ QCOMPARE(modules.logos_execution_zone.submittedProgramId, PROGRAM_ID);
+ QCOMPARE(modules.logos_execution_zone.submittedAccountIds, transaction.accountIds);
+ QCOMPARE(modules.logos_execution_zone.submittedSigningRequirements,
+ QVariantList({ true, false }));
+ QCOMPARE(modules.logos_execution_zone.submittedInstruction.toList(),
+ QVariantList({ 7U, 0U, 4294967295U }));
+}
+
+void LogosWalletProviderTest::rejectsInvalidSubmissionResponses()
+{
+ LogosModules modules;
+ modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
+ LogosWalletProvider provider(&modules);
+ QVERIFY(provider.connect({}).ok());
+
+ WalletTransaction transaction {
+ PROGRAM_ID,
+ { ACCOUNT_A },
+ { true },
+ { 1 },
+ };
+
+ const QStringList invalidResponses {
+ QStringLiteral("not-json"),
+ QStringLiteral(R"({"success":false,"tx_hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})"),
+ QStringLiteral(R"({"success":true,"error":"rejected","tx_hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})"),
+ QStringLiteral(R"({"success":true,"tx_hash":"short"})"),
+ };
+ for (const QString& response : invalidResponses) {
+ modules.logos_execution_zone.transactionResponse = response;
+ QCOMPARE(provider.submitPublicTransaction(transaction).failure,
+ WalletFailure::SubmissionFailed);
+ }
+
+ transaction.signingRequirements.clear();
+ QCOMPARE(provider.submitPublicTransaction(transaction).failure,
+ WalletFailure::InvalidRequest);
+}
+
+void LogosWalletProviderTest::exposesStableAccountModelRoles()
+{
+ WalletAccountModel model;
+ QSignalSpy countChanged(&model, &WalletAccountModel::countChanged);
+ model.replaceAccounts({
+ { ACCOUNT_A, QStringLiteral("10"), true },
+ { ACCOUNT_B, QStringLiteral("20"), false },
+ });
+
+ QCOMPARE(model.count(), 2);
+ QCOMPARE(countChanged.count(), 1);
+ QCOMPARE(model.roleNames().value(WalletAccountModel::NameRole), QByteArray("name"));
+ QCOMPARE(model.data(model.index(0), WalletAccountModel::NameRole).toString(),
+ QStringLiteral("Account 1"));
+ QCOMPARE(model.data(model.index(1), WalletAccountModel::AddressRole).toString(), ACCOUNT_B);
+ QCOMPARE(model.data(model.index(1), WalletAccountModel::BalanceRole).toString(),
+ QStringLiteral("20"));
+ QVERIFY(!model.data(model.index(1), WalletAccountModel::IsPublicRole).toBool());
+}
+
+void LogosWalletProviderTest::fakeProviderImplementsConsumerContract()
+{
+ FakeWalletProvider provider;
+ provider.snapshotResult.accounts = { { ACCOUNT_A, QStringLiteral("5"), true } };
+ provider.submissionResult.nativeHash = QString(64, QLatin1Char('d'));
+
+ QCOMPARE(provider.snapshot(true).accounts.size(), 1);
+ QVERIFY(provider.lastForceRefresh);
+ QCOMPARE(provider.readPublicAccount(ACCOUNT_B).accountId, ACCOUNT_B);
+ QCOMPARE(provider.readCalls, 1);
+ WalletTransaction transaction { PROGRAM_ID, { ACCOUNT_A }, { true }, { 9 } };
+ QVERIFY(provider.submitPublicTransaction(transaction).accepted());
+ QCOMPARE(provider.lastTransaction.instruction, transaction.instruction);
+ provider.disconnect();
+ QCOMPARE(provider.disconnectCalls, 1);
+}
+
+void LogosWalletProviderTest::controllerOwnsUiWalletFlow()
+{
+ const QString settingsApplication = QStringLiteral("WalletControllerTest");
+ QSettings settings(QStringLiteral("Logos"), settingsApplication);
+ settings.clear();
+
+ FakeWalletProvider provider;
+ provider.connectResult.snapshot.accounts = {
+ { ACCOUNT_A, QStringLiteral("5"), true },
+ };
+ provider.connectResult.snapshot.lastSyncedBlock = 7;
+ provider.connectResult.snapshot.currentBlockHeight = 8;
+
+ WalletController controller(provider, settingsApplication);
+ QSignalSpy stateChanged(&controller, &WalletController::stateChanged);
+
+ QVERIFY(controller.open());
+ QCOMPARE(provider.connectCalls, 1);
+ QVERIFY(controller.state().isWalletOpen);
+ QVERIFY(controller.state().walletExists);
+ QCOMPARE(controller.state().lastSyncedBlock, 7);
+ QCOMPARE(controller.state().currentBlockHeight, 8);
+ QCOMPARE(controller.accountModel()->count(), 1);
+
+ provider.snapshotResult.accounts = {
+ { ACCOUNT_A, QStringLiteral("9"), true },
+ };
+ controller.refresh();
+ QVERIFY(provider.lastForceRefresh);
+ QCOMPARE(controller.balance(ACCOUNT_A, true), QStringLiteral("9"));
+
+ provider.createAccountResult.accountId = ACCOUNT_B;
+ provider.createAccountResult.snapshot.accounts = {
+ { ACCOUNT_A, QStringLiteral("9"), true },
+ { ACCOUNT_B, QStringLiteral("3"), false },
+ };
+ QCOMPARE(controller.createAccount(false), ACCOUNT_B);
+ QVERIFY(!provider.lastAccountWasPublic);
+ QCOMPARE(controller.accountModel()->count(), 2);
+
+ controller.disconnect();
+ QCOMPARE(provider.disconnectCalls, 1);
+ QVERIFY(!controller.state().isWalletOpen);
+ QCOMPARE(controller.accountModel()->count(), 0);
+ QVERIFY(stateChanged.count() >= 4);
+
+ settings.clear();
+}
+
+void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect()
+{
+ const QString settingsApplication = QStringLiteral("WalletReachabilityTest");
+ QSettings settings(QStringLiteral("Logos"), settingsApplication);
+ settings.clear();
+
+ FakeWalletProvider provider;
+ provider.connectResult.snapshot.sequencerAddress = QStringLiteral("http://127.0.0.1:1");
+ WalletController controller(provider, settingsApplication);
+ auto* network = controller.findChild();
+ QVERIFY(network);
+ QSignalSpy finished(network, &QNetworkAccessManager::finished);
+
+ QVERIFY(controller.open());
+ QTRY_VERIFY_WITH_TIMEOUT(!finished.isEmpty(), 1000);
+ controller.disconnect();
+ finished.clear();
+
+ auto* timer = controller.findChild();
+ QVERIFY(timer);
+ timer->setInterval(1);
+ controller.start();
+ QTest::qWait(50);
+ QCOMPARE(finished.count(), 0);
+
+ settings.clear();
+}
+
+QTEST_GUILESS_MAIN(LogosWalletProviderTest)
+
+#include "LogosWalletProviderTest.moc"
diff --git a/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h b/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h
new file mode 100644
index 00000000..c1927f43
--- /dev/null
+++ b/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+class LogosAPI;
+
+class FakeExecutionZone {
+public:
+ int openResult = 0;
+ int saveResult = 0;
+ int syncResult = 0;
+ int lastSyncedBlock = 0;
+ int currentBlockHeight = 0;
+ QString sequencerAddress;
+ QString mnemonic = QStringLiteral("one two three");
+ QString publicAccountId;
+ QString privateAccountId;
+ QString transactionResponse;
+ QVariantList accounts;
+ QHash publicAccounts;
+ QHash balances;
+
+ int openCalls = 0;
+ int saveCalls = 0;
+ int syncCalls = 0;
+ int listCalls = 0;
+ int publicReadCalls = 0;
+ int submitCalls = 0;
+ QString openedConfig;
+ QString openedStorage;
+ QString createdConfig;
+ QString createdStorage;
+ QString createdPassword;
+ QStringList submittedAccountIds;
+ QVariantList submittedSigningRequirements;
+ QVariant submittedInstruction;
+ QString submittedProgramId;
+
+ int open(const QString& config, const QString& storage)
+ {
+ ++openCalls;
+ openedConfig = config;
+ openedStorage = storage;
+ return openResult;
+ }
+
+ QString create_new(const QString& config,
+ const QString& storage,
+ const QString& password)
+ {
+ createdConfig = config;
+ createdStorage = storage;
+ createdPassword = password;
+ return mnemonic;
+ }
+
+ int save()
+ {
+ ++saveCalls;
+ return saveResult;
+ }
+
+ QString create_account_public() { return publicAccountId; }
+ QString create_account_private() { return privateAccountId; }
+
+ int get_last_synced_block() const { return lastSyncedBlock; }
+ int get_current_block_height() const { return currentBlockHeight; }
+
+ int sync_to_block(quint64)
+ {
+ ++syncCalls;
+ return syncResult;
+ }
+
+ QString get_sequencer_addr() const { return sequencerAddress; }
+
+ QVariantList list_accounts()
+ {
+ ++listCalls;
+ return accounts;
+ }
+
+ QString get_account_public(const QString& accountId)
+ {
+ ++publicReadCalls;
+ return publicAccounts.value(accountId);
+ }
+
+ QString get_balance(const QString& accountId, bool) const
+ {
+ return balances.value(accountId);
+ }
+
+ QString send_generic_public_transaction(
+ const QStringList& accountIds,
+ const QVariantList& signingRequirements,
+ const QVariant& instruction,
+ const QString& programId)
+ {
+ ++submitCalls;
+ submittedAccountIds = accountIds;
+ submittedSigningRequirements = signingRequirements;
+ submittedInstruction = instruction;
+ submittedProgramId = programId;
+ return transactionResponse;
+ }
+};
+
+struct LogosModules {
+ LogosModules() = default;
+ explicit LogosModules(LogosAPI*) { }
+
+ FakeExecutionZone logos_execution_zone;
+};
diff --git a/apps/shared/wallet/tests/qml/main.cpp b/apps/shared/wallet/tests/qml/main.cpp
new file mode 100644
index 00000000..42c698d3
--- /dev/null
+++ b/apps/shared/wallet/tests/qml/main.cpp
@@ -0,0 +1,3 @@
+#include
+
+QUICK_TEST_MAIN(logos_wallet_qml)
diff --git a/apps/shared/wallet/tests/qml/tst_SubmittedTransaction.qml b/apps/shared/wallet/tests/qml/tst_SubmittedTransaction.qml
new file mode 100644
index 00000000..44e57c41
--- /dev/null
+++ b/apps/shared/wallet/tests/qml/tst_SubmittedTransaction.qml
@@ -0,0 +1,43 @@
+import QtQuick
+import QtTest
+import Logos.Wallet as Wallet
+
+Item {
+ id: root
+ width: 360
+ height: 400
+
+ Component {
+ id: resultComponent
+
+ Wallet.SubmittedTransaction {
+ width: 280
+ }
+ }
+
+ TestCase {
+ name: "SubmittedTransaction"
+ when: windowShown
+
+ function test_presentsBase58AndCopyFeedback() {
+ const result = createTemporaryObject(resultComponent, root, {
+ transactionId: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
+ })
+ verify(result, "Submitted result exists")
+ verify(result.base58Shape)
+
+ const label = findChild(result, "submittedTransactionId")
+ verify(label, "Transaction label exists")
+ compare(label.text, result.transactionId)
+ verify(label.implicitHeight > 0)
+
+ const copyButton = findChild(result, "copySubmittedTransactionButton")
+ verify(copyButton, "Copy button exists")
+ mouseClick(copyButton)
+ verify(copyButton.copied)
+
+ result.transactionId = "0OIl"
+ verify(!result.base58Shape)
+ }
+ }
+}
diff --git a/apps/shared/wallet/tests/qml/tst_TransactionConfirmationDialog.qml b/apps/shared/wallet/tests/qml/tst_TransactionConfirmationDialog.qml
new file mode 100644
index 00000000..898a2450
--- /dev/null
+++ b/apps/shared/wallet/tests/qml/tst_TransactionConfirmationDialog.qml
@@ -0,0 +1,125 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtTest
+import Logos.Wallet as Wallet
+
+Item {
+ id: root
+ width: 800
+ height: 600
+
+ Component {
+ id: summaryComponent
+
+ Item {
+ property var snapshot: ({})
+ implicitHeight: summaryText.implicitHeight
+
+ Label {
+ id: summaryText
+ objectName: "customSummaryText"
+ text: parent.snapshot.amount || ""
+ }
+ }
+ }
+
+ Component {
+ id: dialogComponent
+
+ Wallet.TransactionConfirmationDialog {
+ summary: summaryComponent
+ }
+ }
+
+ Component {
+ id: viewportComponent
+
+ Item {
+ width: 360
+ height: 240
+ }
+ }
+
+ Component {
+ id: tallSummaryComponent
+
+ Item {
+ property var snapshot: ({})
+ implicitHeight: 360
+ }
+ }
+
+ Component {
+ id: constrainedDialogComponent
+
+ Wallet.TransactionConfirmationDialog {
+ summary: tallSummaryComponent
+ }
+ }
+
+ TestCase {
+ name: "TransactionConfirmationDialog"
+ when: windowShown
+
+ function test_capturesSnapshotAndLoadsProgramSummary() {
+ const dialog = createTemporaryObject(dialogComponent, root)
+ verify(dialog, "Dialog exists")
+ const source = { amount: "10" }
+ dialog.openWithSnapshot(source)
+ tryCompare(dialog, "opened", true)
+ source.amount = "20"
+ compare(dialog.snapshot.amount, "10")
+
+ const summary = findChild(dialog, "customSummaryText")
+ verify(summary, "Custom summary exists")
+ compare(summary.text, "10")
+
+ confirmedSpy.target = dialog
+ confirmedSpy.signalName = "confirmed"
+ confirmedSpy.clear()
+ mouseClick(findChild(dialog, "transactionConfirmButton"))
+ compare(confirmedSpy.count, 1)
+ compare(confirmedSpy.signalArguments[0][0].amount, "10")
+ tryCompare(dialog, "opened", false)
+ }
+
+ function test_busyStateCannotBeDismissed() {
+ const dialog = createTemporaryObject(dialogComponent, root)
+ verify(dialog, "Dialog exists")
+ dialog.openWithSnapshot({ amount: "5" })
+ dialog.busy = true
+ const cancelButton = findChild(dialog, "transactionCancelButton")
+ const confirmButton = findChild(dialog, "transactionConfirmButton")
+ verify(!cancelButton.enabled)
+ verify(!confirmButton.enabled)
+ keyClick(Qt.Key_Escape)
+ verify(dialog.opened)
+ dialog.cancel()
+ verify(dialog.opened)
+ dialog.busy = false
+ dialog.cancel()
+ tryCompare(dialog, "opened", false)
+ }
+
+ function test_keepsActionsInsideShortViewport() {
+ const viewport = createTemporaryObject(viewportComponent, root)
+ verify(viewport, "Short viewport exists")
+ const dialog = createTemporaryObject(constrainedDialogComponent, viewport)
+ verify(dialog, "Dialog exists")
+
+ dialog.openWithSnapshot({ amount: "5" })
+ tryCompare(dialog, "opened", true)
+ verify(dialog.height <= viewport.height - 32)
+ verify(dialog.y >= 0)
+ verify(dialog.y + dialog.height <= viewport.height)
+
+ const confirmButton = findChild(dialog, "transactionConfirmButton")
+ verify(confirmButton, "Confirm button exists")
+ verify(confirmButton.visible)
+ }
+ }
+
+ SignalSpy { id: confirmedSpy }
+}
diff --git a/apps/shared/wallet/tests/qml/tst_WalletControl.qml b/apps/shared/wallet/tests/qml/tst_WalletControl.qml
new file mode 100644
index 00000000..157a0bcd
--- /dev/null
+++ b/apps/shared/wallet/tests/qml/tst_WalletControl.qml
@@ -0,0 +1,339 @@
+import QtQuick
+import QtTest
+import Logos.Wallet as Wallet
+
+Item {
+ id: root
+ width: 800
+ height: 600
+
+ Component {
+ id: backendComponent
+
+ QtObject {
+ property bool isWalletOpen: false
+ property bool walletExists: true
+ property string walletHome: "/wallet"
+ property int openCalls: 0
+ property int createCalls: 0
+ property int publicAccountCalls: 0
+ property int privateAccountCalls: 0
+ property int disconnectCalls: 0
+
+ function openExisting() {
+ openCalls++
+ isWalletOpen = true
+ return true
+ }
+
+ function createNewDefault(_password) {
+ createCalls++
+ isWalletOpen = true
+ return "alpha beta gamma"
+ }
+
+ function createAccountPublic() {
+ publicAccountCalls++
+ return "a".repeat(64)
+ }
+
+ function createAccountPrivate() {
+ privateAccountCalls++
+ return "b".repeat(64)
+ }
+
+ function disconnectWallet() {
+ disconnectCalls++
+ isWalletOpen = false
+ }
+ }
+ }
+
+ Component {
+ id: modelComponent
+ ListModel { }
+ }
+
+ Component {
+ id: controlComponent
+ Wallet.WalletControl {
+ width: 320
+ height: implicitHeight
+ viewportWidth: 800
+ }
+ }
+
+ Component {
+ id: compactWindowComponent
+
+ Window {
+ width: 360
+ height: 240
+ visible: true
+
+ property alias control: walletControl
+
+ Wallet.WalletControl {
+ id: walletControl
+ x: 20
+ y: 8
+ width: 320
+ height: implicitHeight
+ viewportWidth: parent.width
+ }
+ }
+ }
+
+ Component {
+ id: compactDialogWindowComponent
+
+ Window {
+ width: 360
+ height: 600
+ visible: true
+
+ property alias control: walletControl
+
+ Wallet.WalletControl {
+ id: walletControl
+ x: parent.width - width - 12
+ y: 8
+ width: 40
+ height: implicitHeight
+ viewportWidth: parent.width
+ }
+ }
+ }
+
+ TestCase {
+ name: "WalletControl"
+ when: windowShown
+
+ function createControl(walletProperties, accounts) {
+ const backend = createTemporaryObject(backendComponent, root, walletProperties || {})
+ verify(backend, "Backend exists")
+ const model = createTemporaryObject(modelComponent, root)
+ verify(model, "Account model exists")
+ for (const account of accounts || [])
+ model.append(account)
+ const control = createTemporaryObject(controlComponent, root, {
+ wallet: backend,
+ accountModel: model
+ })
+ verify(control, "Wallet control exists")
+ return { backend, model, control }
+ }
+
+ function test_opensExistingWallet() {
+ const fixture = createControl({ walletExists: true }, [])
+ const connectButton = findChild(fixture.control, "walletConnectButton")
+ verify(connectButton, "Connect button exists")
+ mouseClick(connectButton)
+ compare(fixture.backend.openCalls, 1)
+ tryCompare(fixture.control, "connected", true)
+ }
+
+ function test_requiresSeedBackupAcknowledgement() {
+ const fixture = createControl({ walletExists: false }, [])
+ mouseClick(findChild(fixture.control, "walletConnectButton"))
+
+ const dialog = findChild(fixture.control, "createWalletDialog")
+ tryCompare(dialog, "opened", true)
+ const password = findChild(dialog, "walletPasswordField")
+ const confirmation = findChild(dialog, "walletConfirmPasswordField")
+ const createButton = findChild(dialog, "createWalletButton")
+ verify(password && confirmation && createButton, "Wallet fields exist")
+
+ password.text = "secret"
+ confirmation.text = "different"
+ mouseClick(createButton)
+ compare(fixture.backend.createCalls, 0)
+ verify(dialog.errorText.length > 0)
+
+ confirmation.text = "secret"
+ mouseClick(createButton)
+ compare(fixture.backend.createCalls, 1)
+ compare(dialog.mnemonic, "alpha beta gamma")
+
+ const acknowledgement = findChild(dialog, "walletBackupAcknowledgement")
+ const continueButton = findChild(dialog, "walletContinueButton")
+ verify(acknowledgement && continueButton, "Backup controls exist")
+ verify(!continueButton.enabled)
+ mouseClick(acknowledgement)
+ verify(continueButton.enabled)
+ mouseClick(continueButton)
+ tryCompare(dialog, "opened", false)
+ }
+
+ function test_clampsSelectionAndDisconnectsLocally() {
+ const fixture = createControl({ isWalletOpen: true }, [
+ { name: "One", address: "a".repeat(64), balance: "10", isPublic: true },
+ { name: "Two", address: "b".repeat(64), balance: "20", isPublic: false }
+ ])
+ fixture.control.selectedIndex = 1
+ compare(fixture.control.selectedAddress, "b".repeat(64))
+ fixture.model.clear()
+ tryCompare(fixture.control, "selectedIndex", 0)
+ compare(fixture.control.selectedAddress, "")
+
+ fixture.model.append({
+ name: "One", address: "a".repeat(64), balance: "10", isPublic: true
+ })
+ mouseClick(findChild(fixture.control, "walletAccountButton"))
+ const disconnectButton = findChild(fixture.control, "walletDisconnectButton")
+ tryVerify(function() { return disconnectButton.visible })
+ mouseClick(disconnectButton)
+ compare(fixture.backend.disconnectCalls, 1)
+ tryCompare(fixture.control, "connected", false)
+ }
+
+ function test_connectedButtonClosesOpenMenu() {
+ const fixture = createControl({ isWalletOpen: true }, [
+ { name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
+ ])
+ const accountButton = findChild(fixture.control, "walletAccountButton")
+ const menu = findChild(fixture.control, "walletMenu")
+
+ mouseClick(accountButton)
+ tryCompare(menu, "opened", true)
+ mouseClick(accountButton)
+ tryCompare(menu, "opened", false)
+ }
+
+ function test_openMenuTracksControlMovement() {
+ const fixture = createControl({ isWalletOpen: true }, [
+ { name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
+ ])
+ fixture.control.x = 20
+ fixture.control.y = 20
+ const accountButton = findChild(fixture.control, "walletAccountButton")
+ const menu = findChild(fixture.control, "walletMenu")
+
+ mouseClick(accountButton)
+ tryCompare(menu, "opened", true)
+ const initialMenu = menu.contentItem.mapToItem(root, 0, 0)
+ const initialButton = accountButton.mapToItem(root, 0, 0)
+
+ fixture.control.x += 300
+ fixture.control.y += 30
+ wait(0)
+
+ const movedMenu = menu.contentItem.mapToItem(root, 0, 0)
+ const movedButton = accountButton.mapToItem(root, 0, 0)
+ compare(movedMenu.x - movedButton.x, initialMenu.x - initialButton.x)
+ compare(movedMenu.y - movedButton.y, initialMenu.y - initialButton.y)
+ }
+
+ function test_selectsAccount() {
+ const fixture = createControl({ isWalletOpen: true }, [
+ { name: "One", address: "a".repeat(64), balance: "10", isPublic: true },
+ { name: "Two", address: "b".repeat(64), balance: "20", isPublic: false }
+ ])
+ mouseClick(findChild(fixture.control, "walletAccountButton"))
+ const accountsButton = findChild(fixture.control, "walletAccountsButton")
+ tryVerify(function() { return accountsButton.visible })
+ mouseClick(accountsButton)
+
+ const accountList = findChild(fixture.control, "walletAccountList")
+ tryCompare(accountList, "count", 2)
+ tryVerify(function() { return accountList.itemAtIndex(1) !== null })
+ const secondAccount = accountList.itemAtIndex(1)
+ secondAccount.clicked()
+ tryCompare(fixture.control, "selectedIndex", 1)
+ compare(fixture.control.selectedAddress, "b".repeat(64))
+ }
+
+ function test_createsAccount() {
+ const fixture = createControl({ isWalletOpen: true }, [
+ { name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
+ ])
+ mouseClick(findChild(fixture.control, "walletAccountButton"))
+ const accountsButton = findChild(fixture.control, "walletAccountsButton")
+ tryVerify(function() { return accountsButton.visible })
+ mouseClick(accountsButton)
+
+ const addButton = findChild(fixture.control, "walletAddAccountButton")
+ tryVerify(function() { return addButton.visible })
+ addButton.clicked()
+ const dialog = findChild(fixture.control, "createAccountDialog")
+ tryCompare(dialog, "opened", true)
+ findChild(dialog, "createAccountButton").clicked()
+ compare(fixture.backend.publicAccountCalls, 1)
+ tryCompare(dialog, "opened", false)
+ }
+
+ function test_compactLayoutHasStableWidth() {
+ const fixture = createControl({ isWalletOpen: false }, [])
+ fixture.control.viewportWidth = 480
+ verify(fixture.control.compactLayout)
+ compare(fixture.control.implicitWidth, 40)
+ fixture.control.viewportWidth = 900
+ verify(!fixture.control.compactLayout)
+ compare(fixture.control.implicitWidth, 108)
+ }
+
+ function test_walletDialogsUseWindowViewport() {
+ const window = createTemporaryObject(compactDialogWindowComponent, root)
+ verify(window, "Compact window exists")
+ waitForRendering(window.contentItem)
+
+ const dialogNames = [
+ "createWalletDialog",
+ "createAccountDialog",
+ "walletMessageDialog"
+ ]
+ for (const name of dialogNames) {
+ const dialog = findChild(window.control, name)
+ verify(dialog, name + " exists")
+ dialog.open()
+ tryCompare(dialog, "opened", true)
+ compare(dialog.width, window.width - 32)
+ verify(dialog.x >= 0)
+ verify(dialog.x + dialog.width <= window.width)
+ dialog.close()
+ tryCompare(dialog, "opened", false)
+ }
+ }
+
+ function test_accountsRemainReachableInShortWindow() {
+ const backend = createTemporaryObject(backendComponent, root, { isWalletOpen: true })
+ const model = createTemporaryObject(modelComponent, root)
+ verify(backend && model, "Wallet fixture exists")
+ for (let index = 0; index < 10; ++index) {
+ model.append({
+ name: "Account " + index,
+ address: String(index).repeat(64),
+ balance: String(index),
+ isPublic: true
+ })
+ }
+
+ const window = createTemporaryObject(compactWindowComponent, root)
+ verify(window, "Short window exists")
+ window.control.wallet = backend
+ window.control.accountModel = model
+ waitForRendering(window.contentItem)
+
+ mouseClick(findChild(window.control, "walletAccountButton"))
+ const menu = findChild(window.control, "walletMenu")
+ tryCompare(menu, "opened", true)
+ mouseClick(findChild(window.control, "walletAccountsButton"))
+
+ const accountList = findChild(window.control, "walletAccountList")
+ const addButton = findChild(window.control, "walletAddAccountButton")
+ tryCompare(accountList, "count", 10)
+ verify(menu.y >= 12, "Menu top: " + menu.y)
+ verify(menu.y + menu.height <= window.height - 12,
+ "Menu bottom: " + (menu.y + menu.height)
+ + ", window: " + window.height)
+ verify(accountList.height > 0)
+ verify(accountList.contentHeight > accountList.height)
+ verify(addButton && addButton.visible, "Add account button is visible")
+ const addPosition = addButton.mapToItem(window.contentItem, 0, 0)
+ verify(addPosition.y >= menu.y)
+ verify(addPosition.y + addButton.height <= menu.y + menu.height,
+ "Add account bottom: " + (addPosition.y + addButton.height)
+ + ", menu bottom: " + (menu.y + menu.height))
+ }
+ }
+}
diff --git a/apps/shared/wallet/tests/support/FakeWalletProvider.h b/apps/shared/wallet/tests/support/FakeWalletProvider.h
new file mode 100644
index 00000000..d5dea0ad
--- /dev/null
+++ b/apps/shared/wallet/tests/support/FakeWalletProvider.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include "WalletProvider.h"
+
+class FakeWalletProvider final : public WalletProvider {
+public:
+ WalletSession connectResult;
+ WalletCreation createWalletResult;
+ WalletSnapshot snapshotResult;
+ WalletAccountCreation createAccountResult;
+ WalletAccountRead readResult;
+ WalletSubmission submissionResult;
+
+ int connectCalls = 0;
+ int createWalletCalls = 0;
+ int snapshotCalls = 0;
+ int clearCalls = 0;
+ int createAccountCalls = 0;
+ mutable int readCalls = 0;
+ int submitCalls = 0;
+ int disconnectCalls = 0;
+ bool lastForceRefresh = false;
+ bool lastAccountWasPublic = false;
+ WalletPaths lastPaths;
+ WalletTransaction lastTransaction;
+
+ WalletSession connect(const WalletPaths& paths) override
+ {
+ ++connectCalls;
+ lastPaths = paths;
+ return connectResult;
+ }
+
+ WalletCreation createWallet(const WalletPaths& paths,
+ const QString&) override
+ {
+ ++createWalletCalls;
+ lastPaths = paths;
+ return createWalletResult;
+ }
+
+ WalletSnapshot snapshot(bool forceRefresh) override
+ {
+ ++snapshotCalls;
+ lastForceRefresh = forceRefresh;
+ return snapshotResult;
+ }
+
+ void clearSnapshot() override { ++clearCalls; }
+
+ WalletAccountCreation createAccount(bool isPublic) override
+ {
+ ++createAccountCalls;
+ lastAccountWasPublic = isPublic;
+ return createAccountResult;
+ }
+
+ WalletAccountRead readPublicAccount(const QString& accountId) const override
+ {
+ ++readCalls;
+ WalletAccountRead result = readResult;
+ result.accountId = accountId;
+ return result;
+ }
+
+ WalletSubmission submitPublicTransaction(
+ const WalletTransaction& transaction) override
+ {
+ ++submitCalls;
+ lastTransaction = transaction;
+ return submissionResult;
+ }
+
+ void disconnect() override { ++disconnectCalls; }
+};
diff --git a/flake.nix b/flake.nix
index a0a443c0..15fc2816 100644
--- a/flake.nix
+++ b/flake.nix
@@ -118,6 +118,27 @@
externalLibInputs = {
amm_client_ffi = { input = self; packages.default = "amm_client_ffi"; };
};
+ # The AMM UI links the shared C++ wallet access lib and bundles the
+ # Logos.Wallet QML module (apps/shared/wallet). apps/amm/flake.nix wires
+ # these via its `shared_wallet` input; when built from this root flake
+ # the source lives in-tree, so point CMake straight at it and stage the
+ # built QML module the same way. Keep in sync with apps/amm/flake.nix.
+ preConfigure = ''
+ cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${./apps/shared/wallet}")
+ '';
+ postInstall = ''
+ test -f ${./apps/amm/qml}/Logos/Wallet/qmldir
+
+ walletQmlDir="shared-wallet/qml/Logos/Wallet"
+ if [ ! -d "$walletQmlDir" ]; then
+ echo "Built Logos.Wallet QML module not found"
+ exit 1
+ fi
+ walletQmlInstallDir="$out/lib/Logos/Wallet"
+ mkdir -p "$walletQmlInstallDir"
+ cp -r "$walletQmlDir/." "$walletQmlInstallDir/"
+ test -f "$walletQmlInstallDir/qmldir"
+ '';
};
# Expose the AMM QML UI as a NAMED app/package (`amm-ui`) rather than
From 46f50a3f81751a8e1876b1aba956a3e4aa1b2826 Mon Sep 17 00:00:00 2001
From: r4bbit <445106+0x-r4bbit@users.noreply.github.com>
Date: Thu, 16 Jul 2026 14:44:20 +0200
Subject: [PATCH 02/10] fix(apps/wallet): use @loader_path rpath for the QML
plugin on macOS
The Logos.Wallet QML plugin was installed with an ELF "$ORIGIN" rpath.
macOS dyld does not expand "$ORIGIN", so loading the plugin failed with
Cannot load library liblogos_wallet_qmlplugin.dylib:
Library not loaded: @rpath/liblogos_wallet_qml.dylib
even though liblogos_wallet_qml.dylib sits in the same directory. Select
the rpath per platform: @loader_path on Apple, $ORIGIN elsewhere, so
@rpath/liblogos_wallet_qml.dylib resolves to the sibling library.
---
apps/shared/wallet/CMakeLists.txt | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/apps/shared/wallet/CMakeLists.txt b/apps/shared/wallet/CMakeLists.txt
index 37f16630..896716b8 100644
--- a/apps/shared/wallet/CMakeLists.txt
+++ b/apps/shared/wallet/CMakeLists.txt
@@ -106,11 +106,18 @@ if(LOGOS_WALLET_BUILD_QML)
Qt6::Quick
Qt6::QuickControls2
)
+ # Resolve the sibling liblogos_wallet_qml.dylib/.so next to the plugin.
+ # macOS dyld does not expand the ELF "$ORIGIN" token — it uses @loader_path.
+ if(APPLE)
+ set(wallet_qml_rpath "@loader_path")
+ else()
+ set(wallet_qml_rpath "$ORIGIN")
+ endif()
set_target_properties(logos_wallet_qml logos_wallet_qmlplugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${wallet_qml_output_dir}"
RUNTIME_OUTPUT_DIRECTORY "${wallet_qml_output_dir}"
BUILD_WITH_INSTALL_RPATH ON
- INSTALL_RPATH "$ORIGIN"
+ INSTALL_RPATH "${wallet_qml_rpath}"
)
set(wallet_qml_install_dir "lib/qml/Logos/Wallet")
From 64b7e8a1973e3876c71b0538fd1708a6fb96eb77 Mon Sep 17 00:00:00 2001
From: r4bbit <445106+0x-r4bbit@users.noreply.github.com>
Date: Fri, 26 Jun 2026 13:00:02 +0200
Subject: [PATCH 03/10] fix(apps/amm): ensure changing endpoint works
---
apps/amm/qml/Main.qml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/amm/qml/Main.qml b/apps/amm/qml/Main.qml
index 6e55e3e1..1703e33d 100644
--- a/apps/amm/qml/Main.qml
+++ b/apps/amm/qml/Main.qml
@@ -45,7 +45,7 @@ Item {
height: show ? 32 : 0
visible: height > 0
clip: true
- color: Theme.palette.warning
+ color: Theme.palette.error
Behavior on height { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
@@ -56,7 +56,7 @@ Item {
elide: Text.ElideMiddle
font.pixelSize: 12
font.weight: Font.Medium
- color: Theme.palette.background
+ color: Theme.palette.text
text: qsTr("Unable to connect to network")
}
}
From b2753aa0c98c27577526541a607dd6f9fae2f9ef Mon Sep 17 00:00:00 2001
From: r4bbit <445106+0x-r4bbit@users.noreply.github.com>
Date: Fri, 26 Jun 2026 09:22:42 +0200
Subject: [PATCH 04/10] feat(apps/amm): add create-pool / new liquidity
position flow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add a "Create Pool" flow to the AMM app that lets a user open a new
liquidity position — seeding a pool's initial liquidity — from token
selection and amount entry, through a confirmation dialog, to on-chain
submission.
- client crate (apps/amm/client): pure, testable protocol logic — account
decoding, pair/position modelling, and quote/plan computation — exposed to
the app over a C ABI (config/networks.json drives network selection).
- C++ runtime + backend: AmmClient, ActiveNetwork, and NewPositionRuntime,
wired into AmmUiBackend (new resolve/quote/submit slots).
- QML flow: NewPositionForm, NewPositionFlow state, NewPositionConfirmation-
Dialog, TokenSelectorModal, and reusable Amm* presentational components
(theme, surfaces, buttons) + AmountMath.js.
- tests: C++ (NewPositionRuntimeTest, ActiveNetworkTest) and QML
(tst_NewPositionForm, tst_LiquidityPage, tst_TokenAmountInput, …).
---
Cargo.lock | 41 +
Cargo.toml | 1 +
apps/amm/CMakeLists.txt | 45 +-
apps/amm/README.md | 15 +-
apps/amm/VALIDATION.md | 52 +
apps/amm/client/Cargo.toml | 24 +
apps/amm/client/include/amm_client.h | 20 +
apps/amm/client/src/account.rs | 152 ++
apps/amm/client/src/api/accounts.rs | 226 +++
apps/amm/client/src/api/clock.rs | 12 +
apps/amm/client/src/api/commitment.rs | 51 +
apps/amm/client/src/api/config.rs | 25 +
apps/amm/client/src/api/context.rs | 254 +++
apps/amm/client/src/api/funding.rs | 106 ++
apps/amm/client/src/api/holding.rs | 64 +
apps/amm/client/src/api/mod.rs | 97 ++
apps/amm/client/src/api/pair.rs | 93 +
apps/amm/client/src/api/plan.rs | 136 ++
apps/amm/client/src/api/position.rs | 229 +++
apps/amm/client/src/api/quote.rs | 726 ++++++++
apps/amm/client/src/api/quote_error.rs | 25 +
apps/amm/client/src/api/request.rs | 116 ++
apps/amm/client/src/api/tests.rs | 703 ++++++++
apps/amm/client/src/ffi.rs | 140 ++
apps/amm/client/src/lib.rs | 12 +
apps/amm/client/tests/public_api.rs | 13 +
apps/amm/config/networks.json | 18 +
apps/amm/flake.lock | 891 +++++-----
apps/amm/flake.nix | 8 +
apps/amm/metadata.json | 7 +-
apps/amm/qml/Main.qml | 6 +-
.../components/liquidity/AddLiquidityForm.qml | 222 ---
.../components/liquidity/AmmActionCard.qml | 15 +
.../components/liquidity/AmmPairSeparator.qml | 61 +
.../components/liquidity/AmmPrimaryButton.qml | 43 +
.../amm/qml/components/liquidity/AmmTheme.qml | 50 +
.../liquidity/AmmTokenAccessory.qml | 46 +
.../liquidity/AmmTokenAmountSurface.qml | 195 +++
.../liquidity/AmmTokenSelectButton.qml | 80 +
.../qml/components/liquidity/AmountMath.js | 295 ++++
.../liquidity/LiquidityActionTabs.qml | 91 -
.../LiquidityConfirmationSummary.qml | 104 +-
.../components/liquidity/NewPositionForm.qml | 1542 +++++++++++++++++
.../liquidity/PoolPositionSummary.qml | 98 --
.../liquidity/RemoveLiquidityForm.qml | 492 ------
.../qml/components/liquidity/SummaryRow.qml | 5 +-
.../components/liquidity/TokenAmountInput.qml | 247 ++-
.../liquidity/TokenSelectorModal.qml | 489 ++++++
apps/amm/qml/pages/LiquidityPage.qml | 375 ++--
apps/amm/qml/state/DummyPoolState.qml | 205 ---
apps/amm/qml/state/NewPositionFlow.qml | 375 ++++
apps/amm/src/ActiveNetwork.cpp | 141 ++
apps/amm/src/ActiveNetwork.h | 36 +
apps/amm/src/AmmClient.cpp | 71 +
apps/amm/src/AmmClient.h | 30 +
apps/amm/src/AmmUiBackend.cpp | 193 ++-
apps/amm/src/AmmUiBackend.h | 25 +
apps/amm/src/AmmUiBackend.rep | 13 +-
apps/amm/src/NewPositionRuntime.cpp | 390 +++++
apps/amm/src/NewPositionRuntime.h | 42 +
apps/amm/tests/cpp/ActiveNetworkTest.cpp | 65 +
apps/amm/tests/cpp/NewPositionRuntimeTest.cpp | 230 +++
.../qml/tst_LiquidityConfirmationDialog.qml | 29 +
apps/amm/tests/qml/tst_LiquidityPage.qml | 335 ++++
apps/amm/tests/qml/tst_NewPositionForm.qml | 633 +++++++
apps/amm/tests/qml/tst_ResponsivePopups.qml | 47 +
apps/amm/tests/qml/tst_TokenAmountInput.qml | 280 +++
flake.nix | 30 +-
68 files changed, 10046 insertions(+), 1882 deletions(-)
create mode 100644 apps/amm/VALIDATION.md
create mode 100644 apps/amm/client/Cargo.toml
create mode 100644 apps/amm/client/include/amm_client.h
create mode 100644 apps/amm/client/src/account.rs
create mode 100644 apps/amm/client/src/api/accounts.rs
create mode 100644 apps/amm/client/src/api/clock.rs
create mode 100644 apps/amm/client/src/api/commitment.rs
create mode 100644 apps/amm/client/src/api/config.rs
create mode 100644 apps/amm/client/src/api/context.rs
create mode 100644 apps/amm/client/src/api/funding.rs
create mode 100644 apps/amm/client/src/api/holding.rs
create mode 100644 apps/amm/client/src/api/mod.rs
create mode 100644 apps/amm/client/src/api/pair.rs
create mode 100644 apps/amm/client/src/api/plan.rs
create mode 100644 apps/amm/client/src/api/position.rs
create mode 100644 apps/amm/client/src/api/quote.rs
create mode 100644 apps/amm/client/src/api/quote_error.rs
create mode 100644 apps/amm/client/src/api/request.rs
create mode 100644 apps/amm/client/src/api/tests.rs
create mode 100644 apps/amm/client/src/ffi.rs
create mode 100644 apps/amm/client/src/lib.rs
create mode 100644 apps/amm/client/tests/public_api.rs
create mode 100644 apps/amm/config/networks.json
delete mode 100644 apps/amm/qml/components/liquidity/AddLiquidityForm.qml
create mode 100644 apps/amm/qml/components/liquidity/AmmActionCard.qml
create mode 100644 apps/amm/qml/components/liquidity/AmmPairSeparator.qml
create mode 100644 apps/amm/qml/components/liquidity/AmmPrimaryButton.qml
create mode 100644 apps/amm/qml/components/liquidity/AmmTheme.qml
create mode 100644 apps/amm/qml/components/liquidity/AmmTokenAccessory.qml
create mode 100644 apps/amm/qml/components/liquidity/AmmTokenAmountSurface.qml
create mode 100644 apps/amm/qml/components/liquidity/AmmTokenSelectButton.qml
create mode 100644 apps/amm/qml/components/liquidity/AmountMath.js
delete mode 100644 apps/amm/qml/components/liquidity/LiquidityActionTabs.qml
create mode 100644 apps/amm/qml/components/liquidity/NewPositionForm.qml
delete mode 100644 apps/amm/qml/components/liquidity/PoolPositionSummary.qml
delete mode 100644 apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml
create mode 100644 apps/amm/qml/components/liquidity/TokenSelectorModal.qml
delete mode 100644 apps/amm/qml/state/DummyPoolState.qml
create mode 100644 apps/amm/qml/state/NewPositionFlow.qml
create mode 100644 apps/amm/src/ActiveNetwork.cpp
create mode 100644 apps/amm/src/ActiveNetwork.h
create mode 100644 apps/amm/src/AmmClient.cpp
create mode 100644 apps/amm/src/AmmClient.h
create mode 100644 apps/amm/src/NewPositionRuntime.cpp
create mode 100644 apps/amm/src/NewPositionRuntime.h
create mode 100644 apps/amm/tests/cpp/ActiveNetworkTest.cpp
create mode 100644 apps/amm/tests/cpp/NewPositionRuntimeTest.cpp
create mode 100644 apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml
create mode 100644 apps/amm/tests/qml/tst_LiquidityPage.qml
create mode 100644 apps/amm/tests/qml/tst_NewPositionForm.qml
create mode 100644 apps/amm/tests/qml/tst_ResponsivePopups.qml
create mode 100644 apps/amm/tests/qml/tst_TokenAmountInput.qml
diff --git a/Cargo.lock b/Cargo.lock
index 575725c5..5da900e2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -76,6 +76,25 @@ dependencies = [
"risc0-zkvm",
]
+[[package]]
+name = "amm_client"
+version = "0.1.0"
+dependencies = [
+ "alloy-primitives",
+ "amm_core",
+ "borsh",
+ "clock_core",
+ "hex",
+ "lee_core",
+ "pretty_assertions",
+ "risc0-zkvm",
+ "serde",
+ "serde_json",
+ "sha2",
+ "token_core",
+ "twap_oracle_core",
+]
+
[[package]]
name = "amm_client_ffi"
version = "0.1.0"
@@ -1309,6 +1328,12 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
[[package]]
name = "digest"
version = "0.9.0"
@@ -2761,6 +2786,16 @@ dependencies = [
"zerocopy",
]
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
[[package]]
name = "primitive-types"
version = "0.12.2"
@@ -4878,6 +4913,12 @@ dependencies = [
"hashlink",
]
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
[[package]]
name = "yoke"
version = "0.8.3"
diff --git a/Cargo.toml b/Cargo.toml
index d38a8c44..5631a811 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,6 @@
[workspace]
members = [
+ "apps/amm/client",
"programs/token/core",
"programs/token",
"programs/token/methods",
diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt
index 8d66e29f..037867cb 100644
--- a/apps/amm/CMakeLists.txt
+++ b/apps/amm/CMakeLists.txt
@@ -4,6 +4,11 @@ project(AmmUiPlugin LANGUAGES CXX)
find_package(Qt6 6.8 REQUIRED COMPONENTS Core Gui Network Qml Quick QuickControls2)
qt_standard_project_setup(REQUIRES 6.8)
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(BASE58 REQUIRED IMPORTED_TARGET libbase58)
+
+include(CTest)
+
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else()
@@ -31,14 +36,50 @@ logos_module(
src/AmmUiPlugin.cpp
src/AmmUiBackend.h
src/AmmUiBackend.cpp
+ src/ActiveNetwork.h
+ src/ActiveNetwork.cpp
+ src/AmmClient.h
+ src/AmmClient.cpp
+ src/NewPositionRuntime.h
+ src/NewPositionRuntime.cpp
FIND_PACKAGES
Qt6Gui
Qt6Network
LINK_LIBRARIES
Qt6::Gui
Qt6::Network
- EXTERNAL_LIBS
- amm_client_ffi
+ PkgConfig::BASE58
LINK_TARGETS
logos_wallet_access
+ EXTERNAL_LIBS
+ amm_client_ffi
+ amm_client
)
+
+qt_add_resources(amm_ui_module_plugin amm_ui_config
+ PREFIX "/amm"
+ FILES
+ config/networks.json
+)
+
+if(BUILD_TESTING)
+ add_executable(amm_active_network_test
+ tests/cpp/ActiveNetworkTest.cpp
+ src/ActiveNetwork.cpp
+ )
+ target_include_directories(amm_active_network_test PRIVATE src)
+ target_link_libraries(amm_active_network_test PRIVATE Qt6::Core)
+ add_test(NAME amm_active_network COMMAND amm_active_network_test)
+
+ add_executable(amm_new_position_runtime_test
+ tests/cpp/NewPositionRuntimeTest.cpp
+ src/NewPositionRuntime.cpp
+ )
+ target_include_directories(amm_new_position_runtime_test PRIVATE src)
+ target_link_libraries(amm_new_position_runtime_test PRIVATE
+ Qt6::Core
+ PkgConfig::BASE58
+ logos_wallet_access
+ )
+ add_test(NAME amm_new_position_runtime COMMAND amm_new_position_runtime_test)
+endif()
diff --git a/apps/amm/README.md b/apps/amm/README.md
index 9a8621c7..7219a3a5 100644
--- a/apps/amm/README.md
+++ b/apps/amm/README.md
@@ -27,10 +27,12 @@ Account/keystore sharing follows the runtime:
startup the backend **adopts** the already-open wallet (see
`openOrAdoptWallet()`), surfacing **shared** accounts across apps.
-> 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()` /
-> `_open_default()` upstream would let the app drop its path handling entirely.
+> Follow-up: the app reconstructs the wallet paths itself because the
+> `logos_execution_zone` module only exposes path-taking `create_new`/`open`.
+> LEZ's wallet FFI now provides path-free variants (`wallet_ffi_create_new_default`,
+> `wallet_ffi_open_default`, plus `wallet_ffi_default_config_path` /
+> `_storage_path` / `wallet_ffi_wallet_exists_default`). Once the module surfaces
+> those over QtRO, the app can drop its `defaultWalletHome/Config/Storage` logic.
## Setup
@@ -204,6 +206,11 @@ TOKENS_CONFIG=$(pwd)/amm-tokens.json \
nix run .#amm-ui
```
+## Validation
+
+New Position validation commands and acceptance criteria live in
+[VALIDATION.md](VALIDATION.md).
+
## Updating Dependencies
To update the pinned versions of dependencies in `flake.lock`:
diff --git a/apps/amm/VALIDATION.md b/apps/amm/VALIDATION.md
new file mode 100644
index 00000000..8663bfa5
--- /dev/null
+++ b/apps/amm/VALIDATION.md
@@ -0,0 +1,52 @@
+# AMM UI Validation
+
+Validation for the New Position flow covers the shipped Rust client, QML
+syntax, and the complete module build.
+
+## Commands
+
+Run from the repository root:
+
+```bash
+cargo +1.94.0 test -p amm_client
+cargo +1.94.0 clippy -p amm_client --all-targets -- -D warnings
+logos_qml=$(nix build github:logos-co/logos-design-system/6176f0d7a5dfeb64a7f0f98e7ca2bf71a4804772 --no-link --print-out-paths)
+amm_qml=$(nix build ./apps/amm#packages.x86_64-linux.default --no-link --print-out-paths)
+qt_qml=$(nix-store --query --requisites "$amm_qml" | rg -m1 -- '-qtdeclarative-[0-9]')
+qt_svg=$(nix-store --query --requisites "$amm_qml" | rg -m1 -- '-qtsvg-[0-9]')
+export QT_QPA_PLATFORM=offscreen QT_QUICK_BACKEND=software QT_PLUGIN_PATH="$qt_svg/lib/qt-6/plugins"
+amm_import="$amm_qml/lib/qml"
+test -f "$amm_import/Logos/Wallet/qmldir"
+"$qt_qml/bin/qmltestrunner" -import "$qt_qml/lib/qt-6/qml" -import "$logos_qml/lib" -import "$amm_import" -input apps/shared/wallet/tests/qml
+"$qt_qml/bin/qmltestrunner" -import "$qt_qml/lib/qt-6/qml" -import "$logos_qml/lib" -import "$amm_import" -input apps/amm/tests/qml
+"$qt_qml/bin/qmllint" -I "$qt_qml/lib/qt-6/qml" -I "$logos_qml/lib" -I "$amm_qml/lib" -I "$amm_import" apps/amm/qml/pages/LiquidityPage.qml apps/amm/qml/components/liquidity/NewPositionForm.qml apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml
+```
+
+When shared AMM program types or instruction signatures change, also run the
+affected Rust checks:
+
+```bash
+RISC0_DEV_MODE=1 cargo +1.94.0 test -p amm_program
+cargo +1.94.0 build -p idl-gen --release
+./target/release/idl-gen programs/amm/methods/guest/src/bin/amm.rs > /tmp/amm-idl.json
+diff artifacts/amm-idl.json /tmp/amm-idl.json
+```
+
+Live wallet/sequencer validation uses the configured Network and an External
+Wallet Provider. Open the AMM UI, select two TokenProgram-scoped fungible token
+definitions, preview an active or missing Pool, submit, and verify the returned
+transaction ID is displayed.
+
+## Acceptance Checklist
+
+- Context exposes Wallet-Scoped Holdings and configured token definitions.
+- Unsupported fee tiers are disabled and explain why.
+- Active pool fee tier is fixed to the stored pool fee.
+- Missing pool flow accepts an editable `X Token A = Y Token B` ratio and
+ scales either deposit from the minimum that mints more than
+ `MINIMUM_LIQUIDITY`.
+- Active Pool flow keeps the reserve ratio and previews expected LP output.
+- `new_definition` and `add_liquidity` account lists match the committed AMM IDL
+ order.
+- Submit re-quotes and surfaces `quote_changed` when the request no longer
+ matches the preview hash.
diff --git a/apps/amm/client/Cargo.toml b/apps/amm/client/Cargo.toml
new file mode 100644
index 00000000..eb5014cd
--- /dev/null
+++ b/apps/amm/client/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "amm_client"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+alloy-primitives = { version = "1", default-features = false }
+amm_core = { workspace = true }
+borsh = { workspace = true }
+clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0" }
+hex = "0.4"
+nssa_core = { workspace = true }
+risc0-zkvm = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+sha2 = "0.10"
+token_core = { workspace = true }
+twap_oracle_core = { workspace = true }
+
+[dev-dependencies]
+pretty_assertions = "1"
diff --git a/apps/amm/client/include/amm_client.h b/apps/amm/client/include/amm_client.h
new file mode 100644
index 00000000..83c56a6d
--- /dev/null
+++ b/apps/amm/client/include/amm_client.h
@@ -0,0 +1,20 @@
+#ifndef AMM_CLIENT_H
+#define AMM_CLIENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+char *amm_config_id(const char *request_json);
+char *amm_token_ids(const char *request_json);
+char *amm_pair_ids(const char *request_json);
+char *amm_context(const char *request_json);
+char *amm_quote(const char *request_json);
+char *amm_plan(const char *request_json);
+void amm_free(char *value);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/apps/amm/client/src/account.rs b/apps/amm/client/src/account.rs
new file mode 100644
index 00000000..2a8d0203
--- /dev/null
+++ b/apps/amm/client/src/account.rs
@@ -0,0 +1,152 @@
+use std::str::FromStr;
+
+use nssa_core::{
+ account::{Account, AccountId, Data, Nonce},
+ program::ProgramId,
+};
+use serde::Deserialize;
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct AccountRead {
+ pub id: String,
+ pub status: String,
+ #[serde(default)]
+ pub account: Option,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+pub struct WalletAccount {
+ pub program_owner: String,
+ pub balance: String,
+ pub nonce: String,
+ pub data: String,
+}
+
+pub(crate) fn parse_hex_32(value: &str, label: &str) -> Result<[u8; 32], String> {
+ if value.len() != 64
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(format!(
+ "{label} must be 64 lowercase hexadecimal characters"
+ ));
+ }
+
+ let mut bytes = [0_u8; 32];
+ hex::decode_to_slice(value, &mut bytes).map_err(|error| format!("invalid {label}: {error}"))?;
+ Ok(bytes)
+}
+
+pub(crate) fn parse_program_id(value: &str) -> Result {
+ let bytes = parse_hex_32(value, "program id")?;
+ let mut program_id = [0_u32; 8];
+ for (word, chunk) in program_id.iter_mut().zip(bytes.chunks_exact(4)) {
+ let chunk: [u8; 4] = chunk
+ .try_into()
+ .map_err(|_| String::from("program id word has invalid length"))?;
+ *word = u32::from_le_bytes(chunk);
+ }
+ Ok(program_id)
+}
+
+#[cfg(test)]
+pub(crate) fn program_id_hex(program_id: ProgramId) -> String {
+ let bytes = program_id
+ .iter()
+ .flat_map(|word| word.to_le_bytes())
+ .collect::>();
+ hex::encode(bytes)
+}
+
+pub(crate) fn program_id_base58(program_id: ProgramId) -> String {
+ AccountId::new(program_id_bytes(program_id)).to_string()
+}
+
+pub(crate) fn program_id_bytes(program_id: ProgramId) -> [u8; 32] {
+ let mut bytes = [0_u8; 32];
+ for (chunk, word) in bytes.chunks_exact_mut(4).zip(program_id) {
+ chunk.copy_from_slice(&word.to_le_bytes());
+ }
+ bytes
+}
+
+pub(crate) fn parse_base58_id(value: &str, label: &str) -> Result {
+ AccountId::from_str(value).map_err(|error| format!("invalid {label}: {error}"))
+}
+
+pub(crate) fn account_id_from_hex(value: &str, label: &str) -> Result {
+ Ok(AccountId::new(parse_hex_32(value, label)?))
+}
+
+pub(crate) fn account_id_hex(account_id: AccountId) -> String {
+ hex::encode(account_id.into_value())
+}
+
+fn parse_le_u128(value: &str, label: &str) -> Result {
+ if value.len() != 32
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(format!(
+ "{label} must be 32 lowercase hexadecimal characters"
+ ));
+ }
+ let mut bytes = [0_u8; 16];
+ hex::decode_to_slice(value, &mut bytes).map_err(|error| format!("invalid {label}: {error}"))?;
+ Ok(u128::from_le_bytes(bytes))
+}
+
+pub(crate) fn decode_account(read: &AccountRead) -> Result<(AccountId, Account), String> {
+ if read.status != "ok" {
+ return Err(String::from("account read failed"));
+ }
+ let account_id = account_id_from_hex(&read.id, "account id")?;
+ let source = read
+ .account
+ .as_ref()
+ .ok_or_else(|| String::from("successful account read has no account"))?;
+ let program_owner = parse_program_id(&source.program_owner)?;
+ let balance = parse_le_u128(&source.balance, "account balance")?;
+ let nonce = parse_le_u128(&source.nonce, "account nonce")?;
+ if source.data.len() % 2 != 0
+ || !source
+ .data
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(String::from(
+ "account data must be lowercase even-length hexadecimal",
+ ));
+ }
+ let data =
+ hex::decode(&source.data).map_err(|error| format!("invalid account data: {error}"))?;
+ let data =
+ Data::try_from(data).map_err(|error| format!("account data is too large: {error}"))?;
+
+ Ok((
+ account_id,
+ Account {
+ program_owner,
+ balance,
+ data,
+ nonce: Nonce(nonce),
+ },
+ ))
+}
+
+#[cfg(test)]
+pub(crate) fn account_read(id: AccountId, account: &Account) -> AccountRead {
+ AccountRead {
+ id: account_id_hex(id),
+ status: String::from("ok"),
+ account: Some(WalletAccount {
+ program_owner: program_id_hex(account.program_owner),
+ balance: hex::encode(account.balance.to_le_bytes()),
+ nonce: hex::encode(account.nonce.0.to_le_bytes()),
+ data: hex::encode(account.data.as_ref()),
+ }),
+ }
+}
diff --git a/apps/amm/client/src/api/accounts.rs b/apps/amm/client/src/api/accounts.rs
new file mode 100644
index 00000000..a1c3c881
--- /dev/null
+++ b/apps/amm/client/src/api/accounts.rs
@@ -0,0 +1,226 @@
+use amm_core::PoolDefinition;
+use nssa_core::program::ProgramId;
+
+use super::{
+ funding::{append_holding_source, sources_from_reads},
+ pair::PairIds,
+ position::{AccountPlan, AccountPlanHoldings, AccountPlanRow},
+ QuoteRequest,
+};
+
+pub(super) fn missing_account_plan(
+ input: &QuoteRequest,
+ pair: PairIds,
+ amm_program: ProgramId,
+ holdings: AccountPlanHoldings<'_>,
+) -> Result {
+ let mut sources = sources_from_reads(&[
+ ("config", &input.snapshot.config),
+ ("token_a", &input.snapshot.token_a),
+ ("token_b", &input.snapshot.token_b),
+ ("pool", &input.snapshot.pool),
+ ("vault_a", &input.snapshot.vault_a),
+ ("vault_b", &input.snapshot.vault_b),
+ ("lp_definition", &input.snapshot.lp_definition),
+ ("lp_lock_holding", &input.snapshot.lp_lock_holding),
+ ("current_tick", &input.snapshot.current_tick),
+ ])?;
+ append_holding_source(&mut sources, "holding_a", holdings.token_a);
+ append_holding_source(&mut sources, "holding_b", holdings.token_b);
+ Ok(AccountPlan {
+ rows: vec![
+ AccountPlanRow::new(
+ "config",
+ Some(pair.config),
+ Some(amm_program),
+ "read",
+ false,
+ false,
+ ),
+ AccountPlanRow::new(
+ "pool",
+ Some(pair.pool),
+ Some(amm_program),
+ "create",
+ false,
+ true,
+ ),
+ AccountPlanRow::new(
+ "vault_a",
+ Some(pair.vault_a),
+ Some(pair.token_program),
+ "create",
+ false,
+ true,
+ ),
+ AccountPlanRow::new(
+ "vault_b",
+ Some(pair.vault_b),
+ Some(pair.token_program),
+ "create",
+ false,
+ true,
+ ),
+ AccountPlanRow::new(
+ "lp_definition",
+ Some(pair.lp_definition),
+ Some(pair.token_program),
+ "create",
+ false,
+ true,
+ ),
+ AccountPlanRow::new(
+ "lp_lock_holding",
+ Some(pair.lp_lock_holding),
+ Some(pair.token_program),
+ "create",
+ false,
+ true,
+ ),
+ AccountPlanRow::new(
+ "user_holding_a",
+ holdings.token_a.map(|value| value.id),
+ Some(pair.token_program),
+ "update",
+ true,
+ false,
+ ),
+ AccountPlanRow::new(
+ "user_holding_b",
+ holdings.token_b.map(|value| value.id),
+ Some(pair.token_program),
+ "update",
+ true,
+ false,
+ ),
+ AccountPlanRow::new(
+ "user_holding_lp",
+ None,
+ Some(pair.token_program),
+ "create",
+ true,
+ true,
+ ),
+ AccountPlanRow::new(
+ "current_tick",
+ Some(pair.current_tick),
+ Some(pair.twap_program),
+ "create",
+ false,
+ true,
+ ),
+ AccountPlanRow::new("clock", Some(pair.clock), None, "read", false, false),
+ ],
+ sources,
+ })
+}
+
+pub(super) fn active_account_plan(
+ input: &QuoteRequest,
+ pair: PairIds,
+ amm_program: ProgramId,
+ pool: &PoolDefinition,
+ stored_reversed: bool,
+ holdings: AccountPlanHoldings<'_>,
+) -> Result {
+ let (stored_holding_a, stored_holding_b) = if stored_reversed {
+ (holdings.token_b, holdings.token_a)
+ } else {
+ (holdings.token_a, holdings.token_b)
+ };
+ let mut sources = sources_from_reads(&[
+ ("config", &input.snapshot.config),
+ ("token_a", &input.snapshot.token_a),
+ ("token_b", &input.snapshot.token_b),
+ ("pool", &input.snapshot.pool),
+ ("vault_a", &input.snapshot.vault_a),
+ ("vault_b", &input.snapshot.vault_b),
+ ("lp_definition", &input.snapshot.lp_definition),
+ ("current_tick", &input.snapshot.current_tick),
+ ])?;
+ append_holding_source(&mut sources, "holding_a", holdings.token_a);
+ append_holding_source(&mut sources, "holding_b", holdings.token_b);
+ append_holding_source(&mut sources, "holding_lp", holdings.lp);
+ Ok(AccountPlan {
+ rows: vec![
+ AccountPlanRow::new(
+ "config",
+ Some(pair.config),
+ Some(amm_program),
+ "read",
+ false,
+ false,
+ ),
+ AccountPlanRow::new(
+ "pool",
+ Some(pair.pool),
+ Some(amm_program),
+ "update",
+ false,
+ false,
+ ),
+ AccountPlanRow::new(
+ "vault_a",
+ Some(pool.vault_a_id),
+ Some(pair.token_program),
+ "update",
+ false,
+ false,
+ ),
+ AccountPlanRow::new(
+ "vault_b",
+ Some(pool.vault_b_id),
+ Some(pair.token_program),
+ "update",
+ false,
+ false,
+ ),
+ AccountPlanRow::new(
+ "lp_definition",
+ Some(pair.lp_definition),
+ Some(pair.token_program),
+ "update",
+ false,
+ false,
+ ),
+ AccountPlanRow::new(
+ "user_holding_a",
+ stored_holding_a.map(|value| value.id),
+ Some(pair.token_program),
+ "update",
+ true,
+ false,
+ ),
+ AccountPlanRow::new(
+ "user_holding_b",
+ stored_holding_b.map(|value| value.id),
+ Some(pair.token_program),
+ "update",
+ true,
+ false,
+ ),
+ AccountPlanRow::new(
+ "user_holding_lp",
+ holdings.lp.map(|value| value.id),
+ Some(pair.token_program),
+ if holdings.lp.is_some() {
+ "update"
+ } else {
+ "create"
+ },
+ holdings.lp.is_none(),
+ holdings.lp.is_none(),
+ ),
+ AccountPlanRow::new(
+ "current_tick",
+ Some(pair.current_tick),
+ Some(pair.twap_program),
+ "update",
+ false,
+ false,
+ ),
+ AccountPlanRow::new("clock", Some(pair.clock), None, "read", false, false),
+ ],
+ sources,
+ })
+}
diff --git a/apps/amm/client/src/api/clock.rs b/apps/amm/client/src/api/clock.rs
new file mode 100644
index 00000000..358750db
--- /dev/null
+++ b/apps/amm/client/src/api/clock.rs
@@ -0,0 +1,12 @@
+use borsh::from_slice;
+use clock_core::ClockAccountData;
+use nssa_core::account::AccountId;
+
+use crate::account::{decode_account, AccountRead};
+
+pub(super) fn decode_clock(read: &AccountRead) -> Result<(AccountId, ClockAccountData), String> {
+ let (id, account) = decode_account(read)?;
+ let clock = from_slice(account.data.as_ref())
+ .map_err(|error| format!("invalid clock account: {error}"))?;
+ Ok((id, clock))
+}
diff --git a/apps/amm/client/src/api/commitment.rs b/apps/amm/client/src/api/commitment.rs
new file mode 100644
index 00000000..4a6dc9fa
--- /dev/null
+++ b/apps/amm/client/src/api/commitment.rs
@@ -0,0 +1,51 @@
+use borsh::BorshSerialize;
+
+#[derive(BorshSerialize)]
+pub(super) enum RequestCommitment {
+ Missing {
+ amount_a: u128,
+ amount_b: u128,
+ },
+ Active {
+ max_a: u128,
+ max_b: u128,
+ slippage_bps: u32,
+ },
+}
+
+#[derive(BorshSerialize)]
+pub(super) struct SourceCommitment {
+ pub(super) role: String,
+ pub(super) commitment: [u8; 32],
+}
+
+#[derive(BorshSerialize)]
+pub(super) struct FundingCommitment {
+ pub(super) token_id: [u8; 32],
+ pub(super) holding_id: Option<[u8; 32]>,
+ pub(super) available: u128,
+ pub(super) requested: u128,
+}
+
+#[derive(BorshSerialize)]
+pub(super) struct QuoteCommitment {
+ pub(super) schema: String,
+ pub(super) network_id: String,
+ pub(super) network_fingerprint: String,
+ pub(super) amm_program_id: [u8; 32],
+ pub(super) token_a_id: [u8; 32],
+ pub(super) token_b_id: [u8; 32],
+ pub(super) fee_bps: u32,
+ pub(super) pool_status: u8,
+ pub(super) request: RequestCommitment,
+ pub(super) max_a: u128,
+ pub(super) max_b: u128,
+ pub(super) actual_a: u128,
+ pub(super) actual_b: u128,
+ pub(super) expected_lp: u128,
+ pub(super) lp_guard: u128,
+ pub(super) requires_fresh_lp: bool,
+ pub(super) sources: Vec,
+ pub(super) funding: Vec,
+ pub(super) warnings: Vec,
+}
diff --git a/apps/amm/client/src/api/config.rs b/apps/amm/client/src/api/config.rs
new file mode 100644
index 00000000..563537df
--- /dev/null
+++ b/apps/amm/client/src/api/config.rs
@@ -0,0 +1,25 @@
+use amm_core::{compute_config_pda, AmmConfig};
+use nssa_core::{account::Account, program::ProgramId};
+use serde_json::{json, Value};
+
+use super::ConfigIdRequest;
+use crate::account::{account_id_hex, decode_account, parse_program_id, AccountRead};
+
+pub(super) fn config_id(request: ConfigIdRequest) -> Result {
+ let amm_program = parse_program_id(&request.amm_program_id)?;
+ Ok(json!({
+ "status": "ok",
+ "configId": account_id_hex(compute_config_pda(amm_program)),
+ }))
+}
+
+pub(super) fn load_config(amm_program: ProgramId, read: &AccountRead) -> Result {
+ let (id, account) = decode_account(read)?;
+ if id != compute_config_pda(amm_program)
+ || account.program_owner != amm_program
+ || account == Account::default()
+ {
+ return Err(String::from("AMM config is unavailable"));
+ }
+ AmmConfig::try_from(&account.data).map_err(|_| String::from("AMM config is invalid"))
+}
diff --git a/apps/amm/client/src/api/context.rs b/apps/amm/client/src/api/context.rs
new file mode 100644
index 00000000..601bdc44
--- /dev/null
+++ b/apps/amm/client/src/api/context.rs
@@ -0,0 +1,254 @@
+use std::collections::{BTreeMap, BTreeSet};
+
+use amm_core::{
+ FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5, MINIMUM_LIQUIDITY,
+};
+use nssa_core::{account::AccountId, program::ProgramId};
+use serde_json::{json, Value};
+use token_core::TokenDefinition;
+
+use super::{
+ config::load_config,
+ holding::{select_holding, wallet_holdings, SelectedHolding},
+ quote_error::issue,
+ ContextRequest, TokenIdsRequest, SCHEMA,
+};
+use crate::account::{
+ account_id_from_hex, account_id_hex, decode_account, parse_base58_id, parse_program_id,
+ program_id_base58, AccountRead,
+};
+
+pub(super) fn token_ids(request: TokenIdsRequest) -> Result {
+ let amm_program = parse_program_id(&request.amm_program_id)?;
+ let Ok(config) = load_config(amm_program, &request.config) else {
+ return Ok(manifest_error("config_unavailable"));
+ };
+
+ let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
+ let mut token_ids = BTreeSet::new();
+ for id in &request.configured_token_ids {
+ if let Ok(id) = account_id_from_hex(id, "configured token id") {
+ token_ids.insert(id);
+ }
+ }
+ for id in request
+ .recent_token_ids
+ .iter()
+ .chain(&request.resolved_token_ids)
+ {
+ if let Ok(id) = parse_base58_id(id, "token id") {
+ token_ids.insert(id);
+ }
+ }
+ token_ids.extend(holdings.into_iter().map(|holding| holding.definition_id));
+
+ Ok(json!({
+ "status": "ok",
+ "tokenIds": token_ids.into_iter().map(account_id_hex).collect::>(),
+ }))
+}
+
+fn manifest_error(code: &str) -> Value {
+ json!({ "status": "error", "code": code, "tokenIds": [] })
+}
+
+pub(super) fn context(request: ContextRequest) -> Result {
+ let amm_program = parse_program_id(&request.amm_program_id)?;
+ let Ok(config) = load_config(amm_program, &request.config) else {
+ return Ok(context_error(&request, "config_unavailable"));
+ };
+
+ let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
+ let source_map = token_sources(&request, &holdings);
+ let mut rows = Vec::new();
+ let mut warnings = Vec::new();
+
+ for (token_id, sources) in source_map {
+ let read = request
+ .token_definitions
+ .iter()
+ .find(|read| account_id_from_hex(&read.id, "token definition id") == Ok(token_id));
+ let (name, total_supply, metadata_id) =
+ match fungible_definition(read, token_id, config.token_program_id) {
+ Ok(definition) => definition,
+ Err(error) => {
+ rows.push(unavailable_token_row(token_id, sources, error.code));
+ if error.warn {
+ warnings.push(issue(
+ error.code,
+ "Token definition could not be read.",
+ &[],
+ json!({ "tokenId": token_id.to_string() }),
+ ));
+ }
+ continue;
+ }
+ };
+
+ let selected = select_holding(&holdings, token_id);
+ let mut row = json!({
+ "definitionId": token_id.to_string(),
+ "name": name,
+ "metadataId": metadata_id.map(|id| id.to_string()),
+ "totalSupplyRaw": total_supply.to_string(),
+ "ownerProgramId": program_id_base58(config.token_program_id),
+ "public": true,
+ "fungible": true,
+ "selectable": true,
+ "status": "available",
+ "code": "available",
+ "sources": sources,
+ });
+ if let Some(selected) = selected {
+ row["holdingId"] = json!(selected.id.to_string());
+ row["balanceRaw"] = json!(selected.balance.to_string());
+ }
+ rows.push(row);
+ }
+
+ rows.sort_by(|left, right| {
+ let left_holding = left.get("holdingId").is_some();
+ let right_holding = right.get("holdingId").is_some();
+ right_holding.cmp(&left_holding).then_with(|| {
+ left["definitionId"]
+ .as_str()
+ .cmp(&right["definitionId"].as_str())
+ })
+ });
+
+ Ok(json!({
+ "schema": SCHEMA,
+ "status": if request.wallet_available { "ready" } else { "no_wallet" },
+ "networkId": request.network_id,
+ "networkFingerprint": request.network_fingerprint,
+ "walletAvailable": request.wallet_available,
+ "minimumLiquidityRaw": MINIMUM_LIQUIDITY.to_string(),
+ "programIds": {
+ "amm": program_id_base58(amm_program),
+ "token": program_id_base58(config.token_program_id),
+ "twapOracle": program_id_base58(config.twap_oracle_program_id),
+ },
+ "tokens": rows,
+ "feeTiers": fee_tiers(),
+ "warnings": warnings,
+ }))
+}
+
+fn context_error(request: &ContextRequest, code: &str) -> Value {
+ json!({
+ "schema": SCHEMA,
+ "status": "error",
+ "code": code,
+ "networkId": request.network_id,
+ "networkFingerprint": request.network_fingerprint,
+ "walletAvailable": request.wallet_available,
+ "tokens": [],
+ "feeTiers": fee_tiers(),
+ "warnings": [],
+ })
+}
+
+fn fee_tiers() -> Value {
+ json!([
+ { "feeBps": FEE_TIER_BPS_1, "label": "0.01%", "enabled": true },
+ { "feeBps": FEE_TIER_BPS_5, "label": "0.05%", "enabled": true },
+ { "feeBps": FEE_TIER_BPS_30, "label": "0.30%", "enabled": true },
+ { "feeBps": FEE_TIER_BPS_100, "label": "1.00%", "enabled": true },
+ ])
+}
+
+fn token_sources(
+ request: &ContextRequest,
+ holdings: &[SelectedHolding],
+) -> BTreeMap> {
+ let mut sources: BTreeMap> = BTreeMap::new();
+ for id in &request.configured_token_ids {
+ if let Ok(id) = account_id_from_hex(id, "configured token id") {
+ sources
+ .entry(id)
+ .or_default()
+ .insert(String::from("config"));
+ }
+ }
+ for (ids, source) in [
+ (&request.recent_token_ids, "recent"),
+ (&request.resolved_token_ids, "resolved"),
+ ] {
+ for id in ids {
+ if let Ok(id) = parse_base58_id(id, "token id") {
+ sources.entry(id).or_default().insert(String::from(source));
+ }
+ }
+ }
+ for holding in holdings {
+ sources
+ .entry(holding.definition_id)
+ .or_default()
+ .insert(String::from("holding"));
+ }
+ sources
+ .into_iter()
+ .map(|(id, values)| (id, values.into_iter().collect()))
+ .collect()
+}
+
+fn unavailable_token_row(token_id: AccountId, sources: Vec, code: &str) -> Value {
+ json!({
+ "definitionId": token_id.to_string(),
+ "name": "",
+ "metadataId": Value::Null,
+ "totalSupplyRaw": "0",
+ "selectable": false,
+ "status": code,
+ "code": code,
+ "sources": sources,
+ })
+}
+
+pub(super) struct DefinitionError {
+ pub(super) code: &'static str,
+ pub(super) warn: bool,
+}
+
+pub(super) fn fungible_definition(
+ read: Option<&AccountRead>,
+ token_id: AccountId,
+ token_program: ProgramId,
+) -> Result<(String, u128, Option), DefinitionError> {
+ let Some(read) = read else {
+ return Err(DefinitionError {
+ code: "token_definition_unreadable",
+ warn: true,
+ });
+ };
+ let Ok((id, account)) = decode_account(read) else {
+ return Err(DefinitionError {
+ code: "token_definition_unreadable",
+ warn: true,
+ });
+ };
+ if id != token_id {
+ return Err(DefinitionError {
+ code: "token_definition_unreadable",
+ warn: false,
+ });
+ }
+ if account.program_owner != token_program {
+ return Err(DefinitionError {
+ code: "token_program_mismatch",
+ warn: false,
+ });
+ }
+ match TokenDefinition::try_from(&account.data) {
+ Ok(TokenDefinition::Fungible {
+ name,
+ total_supply,
+ metadata_id,
+ ..
+ }) => Ok((name, total_supply, metadata_id)),
+ _ => Err(DefinitionError {
+ code: "token_not_fungible",
+ warn: false,
+ }),
+ }
+}
diff --git a/apps/amm/client/src/api/funding.rs b/apps/amm/client/src/api/funding.rs
new file mode 100644
index 00000000..b328c4c4
--- /dev/null
+++ b/apps/amm/client/src/api/funding.rs
@@ -0,0 +1,106 @@
+use nssa_core::Commitment;
+use serde_json::{json, Value};
+use sha2::{Digest as _, Sha256};
+
+use super::{
+ commitment::{FundingCommitment, QuoteCommitment, SourceCommitment},
+ holding::SelectedHolding,
+ pair::PairIds,
+ quote_error::issue,
+};
+use crate::account::{decode_account, AccountRead};
+
+pub(super) fn funding_issues(
+ wallet_available: bool,
+ pair: PairIds,
+ holding_a: &Option,
+ requested_a: u128,
+ holding_b: &Option,
+ requested_b: u128,
+ fields: [&str; 2],
+) -> Vec {
+ if !wallet_available {
+ return vec![issue(
+ "no_wallet",
+ "Connect a wallet to submit.",
+ &[],
+ json!({}),
+ )];
+ }
+ let mut errors = Vec::new();
+ for (token_id, holding, requested, field) in [
+ (pair.token_a, holding_a, requested_a, fields[0]),
+ (pair.token_b, holding_b, requested_b, fields[1]),
+ ] {
+ let available = holding.as_ref().map_or(0, |value| value.balance);
+ if available < requested {
+ errors.push(issue(
+ "amount_exceeds_balance",
+ "Amount exceeds the selected wallet holding balance.",
+ &[field],
+ json!({
+ "requestedRaw": requested.to_string(),
+ "availableRaw": available.to_string(),
+ "holdingFound": holding.is_some(),
+ "tokenId": token_id.to_string(),
+ }),
+ ));
+ }
+ }
+ errors
+}
+
+pub(super) fn funding_commitments(
+ pair: PairIds,
+ holding_a: &Option,
+ requested_a: u128,
+ holding_b: &Option,
+ requested_b: u128,
+) -> Vec {
+ [
+ (pair.token_a, holding_a, requested_a),
+ (pair.token_b, holding_b, requested_b),
+ ]
+ .into_iter()
+ .map(|(token_id, holding, requested)| FundingCommitment {
+ token_id: token_id.into_value(),
+ holding_id: holding.as_ref().map(|value| value.id.into_value()),
+ available: holding.as_ref().map_or(0, |value| value.balance),
+ requested,
+ })
+ .collect()
+}
+
+pub(super) fn sources_from_reads(
+ reads: &[(&str, &AccountRead)],
+) -> Result, String> {
+ reads
+ .iter()
+ .map(|(role, read)| {
+ let (id, account) = decode_account(read)?;
+ Ok(SourceCommitment {
+ role: String::from(*role),
+ commitment: Commitment::new(&id, &account).to_byte_array(),
+ })
+ })
+ .collect()
+}
+
+pub(super) fn append_holding_source(
+ sources: &mut Vec,
+ role: &str,
+ holding: Option<&SelectedHolding>,
+) {
+ if let Some(holding) = holding {
+ sources.push(SourceCommitment {
+ role: String::from(role),
+ commitment: Commitment::new(&holding.id, &holding.account).to_byte_array(),
+ });
+ }
+}
+
+pub(super) fn hash_quote(commitment: &QuoteCommitment) -> Result {
+ let bytes = borsh::to_vec(commitment)
+ .map_err(|error| format!("quote commitment serialization failed: {error}"))?;
+ Ok(format!("sha256:{}", hex::encode(Sha256::digest(bytes))))
+}
diff --git a/apps/amm/client/src/api/holding.rs b/apps/amm/client/src/api/holding.rs
new file mode 100644
index 00000000..84810406
--- /dev/null
+++ b/apps/amm/client/src/api/holding.rs
@@ -0,0 +1,64 @@
+use nssa_core::{
+ account::{Account, AccountId},
+ program::ProgramId,
+};
+use token_core::TokenHolding;
+
+use crate::account::{decode_account, AccountRead};
+
+#[derive(Clone)]
+pub(super) struct SelectedHolding {
+ pub(super) id: AccountId,
+ pub(super) definition_id: AccountId,
+ pub(super) balance: u128,
+ pub(super) account: Account,
+}
+
+pub(super) fn wallet_holdings(
+ reads: &[AccountRead],
+ token_program: ProgramId,
+) -> Vec {
+ reads
+ .iter()
+ .filter_map(|read| decode_fungible_holding(read, token_program).ok())
+ .collect()
+}
+
+pub(super) fn decode_fungible_holding(
+ read: &AccountRead,
+ token_program: ProgramId,
+) -> Result {
+ let (id, account) = decode_account(read)?;
+ if account.program_owner != token_program {
+ return Err(String::from("holding owner mismatch"));
+ }
+ let TokenHolding::Fungible {
+ definition_id,
+ balance,
+ } = TokenHolding::try_from(&account.data)
+ .map_err(|_| String::from("invalid fungible holding"))?
+ else {
+ return Err(String::from("invalid fungible holding"));
+ };
+ Ok(SelectedHolding {
+ id,
+ definition_id,
+ balance,
+ account,
+ })
+}
+
+pub(super) fn select_holding(
+ holdings: &[SelectedHolding],
+ definition_id: AccountId,
+) -> Option {
+ holdings
+ .iter()
+ .filter(|holding| holding.definition_id == definition_id)
+ .max_by(|left, right| {
+ left.balance
+ .cmp(&right.balance)
+ .then_with(|| right.id.cmp(&left.id))
+ })
+ .cloned()
+}
diff --git a/apps/amm/client/src/api/mod.rs b/apps/amm/client/src/api/mod.rs
new file mode 100644
index 00000000..fdae2431
--- /dev/null
+++ b/apps/amm/client/src/api/mod.rs
@@ -0,0 +1,97 @@
+//! Transport-independent AMM client operations.
+
+mod accounts;
+mod clock;
+mod commitment;
+mod config;
+mod context;
+mod funding;
+mod holding;
+mod pair;
+mod plan;
+mod position;
+mod quote;
+mod quote_error;
+mod request;
+
+#[cfg(test)]
+mod tests;
+
+use std::{error::Error, fmt};
+
+pub use request::{
+ ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest,
+ QuoteRequest, TokenIdsRequest,
+};
+use serde_json::Value;
+
+pub use crate::account::{AccountRead, WalletAccount};
+
+/// Schema identifier expected by position quote and plan requests.
+pub const NEW_POSITION_SCHEMA: &str = "new-position.v1";
+
+pub(crate) const SCHEMA: &str = NEW_POSITION_SCHEMA;
+
+/// JSON response shared by direct Rust callers and transport adapters.
+pub type AmmResponse = Value;
+
+/// Result returned by AMM client operations.
+pub type AmmResult = Result;
+
+/// Failure produced before an AMM response can be constructed.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AmmApiError {
+ message: String,
+}
+
+impl AmmApiError {
+ /// Returns the stable human-readable failure detail.
+ #[must_use]
+ pub fn message(&self) -> &str {
+ &self.message
+ }
+}
+
+impl fmt::Display for AmmApiError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(&self.message)
+ }
+}
+
+impl Error for AmmApiError {}
+
+impl From for AmmApiError {
+ fn from(message: String) -> Self {
+ Self { message }
+ }
+}
+
+/// Derives the AMM configuration account ID.
+pub fn config_id(request: ConfigIdRequest) -> AmmResult {
+ config::config_id(request).map_err(Into::into)
+}
+
+/// Discovers token definition IDs available to the active wallet and app.
+pub fn token_ids(request: TokenIdsRequest) -> AmmResult {
+ context::token_ids(request).map_err(Into::into)
+}
+
+/// Derives canonical accounts for one token pair.
+pub fn pair_ids(request: PairIdsRequest) -> AmmResult {
+ pair::pair_ids(request).map_err(Into::into)
+}
+
+/// Builds network, token, holding, and fee-tier context.
+pub fn context(request: ContextRequest) -> AmmResult {
+ context::context(request).map_err(Into::into)
+}
+
+/// Evaluates a pool-creation or add-liquidity request.
+pub fn quote(request: QuoteRequest) -> AmmResult {
+ quote::quote(request).map_err(Into::into)
+}
+
+/// Materializes a previously quoted request into wallet submission arguments.
+pub fn plan(request: PlanRequest) -> AmmResult {
+ plan::plan(request).map_err(Into::into)
+}
diff --git a/apps/amm/client/src/api/pair.rs b/apps/amm/client/src/api/pair.rs
new file mode 100644
index 00000000..f6cb3c74
--- /dev/null
+++ b/apps/amm/client/src/api/pair.rs
@@ -0,0 +1,93 @@
+use amm_core::{
+ compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda,
+ compute_vault_pda,
+};
+use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
+use nssa_core::{account::AccountId, program::ProgramId};
+use serde_json::{json, Value};
+use twap_oracle_core::compute_current_tick_account_pda;
+
+use super::{config::load_config, PairIdsRequest};
+use crate::account::{account_id_hex, parse_base58_id, parse_program_id, AccountRead};
+
+#[derive(Clone, Copy)]
+pub(super) struct PairIds {
+ pub(super) token_a: AccountId,
+ pub(super) token_b: AccountId,
+ pub(super) config: AccountId,
+ pub(super) pool: AccountId,
+ pub(super) vault_a: AccountId,
+ pub(super) vault_b: AccountId,
+ pub(super) lp_definition: AccountId,
+ pub(super) lp_lock_holding: AccountId,
+ pub(super) current_tick: AccountId,
+ pub(super) clock: AccountId,
+ pub(super) token_program: ProgramId,
+ pub(super) twap_program: ProgramId,
+}
+
+pub(super) fn pair_ids(request: PairIdsRequest) -> Result {
+ let amm_program = parse_program_id(&request.amm_program_id)?;
+ let Ok(token_a) = parse_base58_id(&request.token_a_id, "token A id") else {
+ return Ok(json!({ "status": "error", "code": "invalid_token_id" }));
+ };
+ let Ok(token_b) = parse_base58_id(&request.token_b_id, "token B id") else {
+ return Ok(json!({ "status": "error", "code": "invalid_token_id" }));
+ };
+ if token_a == token_b {
+ return Ok(json!({ "status": "error", "code": "same_token_pair" }));
+ }
+ if !is_canonical_pair(token_a, token_b) {
+ return Ok(json!({ "status": "error", "code": "non_canonical_pair" }));
+ }
+
+ let Ok(pair) = derive_pair(amm_program, token_a, token_b, &request.config) else {
+ return Ok(json!({ "status": "error", "code": "config_unavailable" }));
+ };
+ Ok(pair_json(pair))
+}
+
+pub(super) fn is_canonical_pair(token_a: AccountId, token_b: AccountId) -> bool {
+ token_a.value() > token_b.value()
+}
+
+pub(super) fn derive_pair(
+ amm_program: ProgramId,
+ token_a: AccountId,
+ token_b: AccountId,
+ config_read: &AccountRead,
+) -> Result {
+ let config_id = compute_config_pda(amm_program);
+ let config = load_config(amm_program, config_read)?;
+ let pool = compute_pool_pda(amm_program, token_a, token_b);
+ Ok(PairIds {
+ token_a,
+ token_b,
+ config: config_id,
+ pool,
+ vault_a: compute_vault_pda(amm_program, pool, token_a),
+ vault_b: compute_vault_pda(amm_program, pool, token_b),
+ lp_definition: compute_liquidity_token_pda(amm_program, pool),
+ lp_lock_holding: compute_lp_lock_holding_pda(amm_program, pool),
+ current_tick: compute_current_tick_account_pda(config.twap_oracle_program_id, pool),
+ clock: CLOCK_01_PROGRAM_ACCOUNT_ID,
+ token_program: config.token_program_id,
+ twap_program: config.twap_oracle_program_id,
+ })
+}
+
+fn pair_json(pair: PairIds) -> Value {
+ json!({
+ "status": "ok",
+ "tokenAId": account_id_hex(pair.token_a),
+ "tokenBId": account_id_hex(pair.token_b),
+ "configId": account_id_hex(pair.config),
+ "poolId": account_id_hex(pair.pool),
+ "vaultAId": account_id_hex(pair.vault_a),
+ "vaultBId": account_id_hex(pair.vault_b),
+ "lpDefinitionId": account_id_hex(pair.lp_definition),
+ "lpLockHoldingId": account_id_hex(pair.lp_lock_holding),
+ "currentTickId": account_id_hex(pair.current_tick),
+ "clockId": account_id_hex(pair.clock),
+ })
+}
diff --git a/apps/amm/client/src/api/plan.rs b/apps/amm/client/src/api/plan.rs
new file mode 100644
index 00000000..b87ce0bf
--- /dev/null
+++ b/apps/amm/client/src/api/plan.rs
@@ -0,0 +1,136 @@
+use nssa_core::account::Account;
+use serde_json::{json, Value};
+
+use super::{
+ clock::decode_clock,
+ position::{NewPositionPlan, QuoteBranch, QuoteComputation},
+ quote::compute_quote,
+ PlanRequest, QuoteRequest, SCHEMA,
+};
+use crate::account::{account_id_hex, decode_account, AccountRead};
+
+const DEADLINE_WINDOW_MS: u64 = 1_200_000;
+
+pub(super) fn plan(input: PlanRequest) -> Result {
+ let quote_input = QuoteRequest {
+ network_id: input.network_id,
+ network_fingerprint: input.network_fingerprint,
+ amm_program_id: input.amm_program_id.clone(),
+ request: input.request,
+ snapshot: input.snapshot,
+ };
+ let quote = compute_quote("e_input)?;
+ if quote.quote_hash() != Some(input.quote_hash.as_str()) {
+ return Ok(json!({
+ "schema": SCHEMA,
+ "status": "error",
+ "code": "quote_changed",
+ "recoverable": true,
+ "quote": quote.into_value("e_input.request),
+ }));
+ }
+ let evaluated = match quote {
+ QuoteComputation::Evaluated(evaluated) => evaluated,
+ QuoteComputation::Failed(failure) => {
+ return Ok(json!({
+ "schema": SCHEMA,
+ "status": "error",
+ "code": "quote_not_submittable",
+ "recoverable": true,
+ "quote": failure.into_value("e_input.request),
+ }))
+ }
+ };
+ let Some(plan) = evaluated.plan else {
+ return Ok(json!({
+ "schema": SCHEMA,
+ "status": "error",
+ "code": "quote_not_submittable",
+ "recoverable": true,
+ "quote": evaluated.value,
+ }));
+ };
+ let fresh_lp = if plan.requires_fresh_lp() {
+ let Some(read) = input.fresh_lp.as_ref() else {
+ return Ok(json!({
+ "schema": SCHEMA,
+ "status": "needs_fresh_lp",
+ "code": "fresh_lp_required",
+ }));
+ };
+ let Ok((id, account)) = decode_account(read) else {
+ return Ok(plan_error("wallet_submission_failed"));
+ };
+ if account != Account::default() || plan.accounts.contains(id) {
+ return Ok(plan_error("wallet_submission_failed"));
+ }
+ Some(id)
+ } else {
+ None
+ };
+ let deadline = input
+ .now_ms
+ .checked_add(DEADLINE_WINDOW_MS)
+ .ok_or_else(|| String::from("transaction deadline overflow"))?;
+ let clock_timestamp = clock_timestamp("e_input.snapshot.clock)?;
+ if clock_timestamp >= deadline {
+ return Ok(plan_error("transaction_deadline_expired"));
+ }
+ let NewPositionPlan { accounts, branch } = plan;
+ let (account_ids, signing_requirements) = accounts.wallet_args(fresh_lp)?;
+ let instruction = match branch {
+ QuoteBranch::Missing { amount_a, amount_b } => {
+ let instruction = amm_core::Instruction::NewDefinition {
+ token_a_amount: amount_a,
+ token_b_amount: amount_b,
+ fees: u128::from(quote_input.request.fee_bps),
+ deadline,
+ };
+ risc0_zkvm::serde::to_vec(&instruction)
+ .map_err(|error| format!("instruction serialization failed: {error}"))?
+ }
+ QuoteBranch::Active {
+ max_a,
+ max_b,
+ minimum_lp,
+ stored_reversed,
+ } => {
+ let (stored_max_a, stored_max_b) = if stored_reversed {
+ (max_b, max_a)
+ } else {
+ (max_a, max_b)
+ };
+ let instruction = amm_core::Instruction::AddLiquidity {
+ min_amount_liquidity: minimum_lp,
+ max_amount_to_add_token_a: stored_max_a,
+ max_amount_to_add_token_b: stored_max_b,
+ deadline,
+ };
+ risc0_zkvm::serde::to_vec(&instruction)
+ .map_err(|error| format!("instruction serialization failed: {error}"))?
+ }
+ };
+
+ Ok(json!({
+ "schema": SCHEMA,
+ "status": "ready",
+ "programId": input.amm_program_id,
+ "accountIds": account_ids.into_iter().map(account_id_hex).collect::>(),
+ "signingRequirements": signing_requirements,
+ "instruction": instruction,
+ "deadlineMs": deadline.to_string(),
+ }))
+}
+
+fn plan_error(code: &str) -> Value {
+ json!({
+ "schema": SCHEMA,
+ "status": "error",
+ "code": code,
+ "recoverable": true,
+ })
+}
+
+fn clock_timestamp(read: &AccountRead) -> Result {
+ decode_clock(read).map(|(_, clock)| clock.timestamp)
+}
diff --git a/apps/amm/client/src/api/position.rs b/apps/amm/client/src/api/position.rs
new file mode 100644
index 00000000..c173faa5
--- /dev/null
+++ b/apps/amm/client/src/api/position.rs
@@ -0,0 +1,229 @@
+use nssa_core::{account::AccountId, program::ProgramId};
+use serde_json::{json, Value};
+
+use super::{
+ commitment::SourceCommitment, holding::SelectedHolding, pair::PairIds, quote_error::issue,
+ PairSnapshot, PositionRequest, SCHEMA,
+};
+use crate::account::{account_id_from_hex, program_id_base58};
+
+#[derive(Clone)]
+pub(super) enum QuoteBranch {
+ Missing {
+ amount_a: u128,
+ amount_b: u128,
+ },
+ Active {
+ max_a: u128,
+ max_b: u128,
+ minimum_lp: u128,
+ stored_reversed: bool,
+ },
+}
+
+pub(super) struct EvaluatedQuote {
+ pub(super) value: Value,
+ pub(super) quote_hash: String,
+ pub(super) plan: Option,
+}
+
+pub(super) enum QuoteComputation {
+ Failed(QuoteFailure),
+ Evaluated(EvaluatedQuote),
+}
+
+pub(super) struct QuoteFailure {
+ pub(super) code: &'static str,
+ pub(super) fields: Vec<&'static str>,
+ pub(super) details: Value,
+}
+
+pub(super) struct NewPositionPlan {
+ pub(super) accounts: AccountPlan,
+ pub(super) branch: QuoteBranch,
+}
+
+pub(super) struct AccountPlan {
+ pub(super) rows: Vec,
+ pub(super) sources: Vec,
+}
+
+pub(super) struct AccountPlanRow {
+ pub(super) role: &'static str,
+ pub(super) account_id: Option,
+ pub(super) expected_program: Option,
+ pub(super) action: &'static str,
+ pub(super) signer: bool,
+ pub(super) init: bool,
+}
+
+pub(super) struct AccountPlanHoldings<'a> {
+ pub(super) token_a: Option<&'a SelectedHolding>,
+ pub(super) token_b: Option<&'a SelectedHolding>,
+ pub(super) lp: Option<&'a SelectedHolding>,
+}
+
+impl QuoteComputation {
+ pub(super) fn into_value(self, request: &PositionRequest) -> Value {
+ match self {
+ Self::Failed(failure) => failure.into_value(request),
+ Self::Evaluated(EvaluatedQuote { value, .. }) => value,
+ }
+ }
+
+ pub(super) fn quote_hash(&self) -> Option<&str> {
+ match self {
+ Self::Failed(_) => None,
+ Self::Evaluated(quote) => Some("e.quote_hash),
+ }
+ }
+}
+
+impl QuoteFailure {
+ pub(super) fn into_value(self, request: &PositionRequest) -> Value {
+ json!({
+ "schema": SCHEMA,
+ "status": "error",
+ "canSubmit": false,
+ "code": self.code,
+ "poolStatus": "unavailable_pool",
+ "tokenAId": request.token_a_id,
+ "tokenBId": request.token_b_id,
+ "accountPreview": [],
+ "errors": [issue(
+ self.code,
+ "Position quote is unavailable.",
+ &self.fields,
+ self.details,
+ )],
+ "warnings": [],
+ })
+ }
+}
+
+impl NewPositionPlan {
+ pub(super) fn new(accounts: AccountPlan, branch: QuoteBranch) -> Result {
+ accounts.validate_ready()?;
+ Ok(Self { accounts, branch })
+ }
+
+ pub(super) fn requires_fresh_lp(&self) -> bool {
+ self.accounts.requires_fresh_lp()
+ }
+}
+
+impl AccountPlan {
+ pub(super) fn validate_snapshot_ids(
+ pair: &PairIds,
+ snapshot: &PairSnapshot,
+ ) -> Option<&'static str> {
+ let expected = [
+ (&snapshot.config, pair.config, "config"),
+ (&snapshot.token_a, pair.token_a, "token_a"),
+ (&snapshot.token_b, pair.token_b, "token_b"),
+ (&snapshot.pool, pair.pool, "pool"),
+ (&snapshot.vault_a, pair.vault_a, "vault_a"),
+ (&snapshot.vault_b, pair.vault_b, "vault_b"),
+ (&snapshot.lp_definition, pair.lp_definition, "lp_definition"),
+ (
+ &snapshot.lp_lock_holding,
+ pair.lp_lock_holding,
+ "lp_lock_holding",
+ ),
+ (&snapshot.current_tick, pair.current_tick, "current_tick"),
+ (&snapshot.clock, pair.clock, "clock"),
+ ];
+ expected.into_iter().find_map(|(read, id, role)| {
+ match account_id_from_hex(&read.id, "account id") {
+ Ok(actual) if actual == id => None,
+ _ => Some(role),
+ }
+ })
+ }
+
+ pub(super) fn preview(&self) -> Vec {
+ self.rows
+ .iter()
+ .enumerate()
+ .map(|(order, row)| row.preview(order))
+ .collect()
+ }
+
+ pub(super) fn take_sources(&mut self) -> Vec {
+ std::mem::take(&mut self.sources)
+ }
+
+ pub(super) fn requires_fresh_lp(&self) -> bool {
+ self.rows
+ .iter()
+ .any(|row| row.role == "user_holding_lp" && row.account_id.is_none())
+ }
+
+ pub(super) fn contains(&self, account_id: AccountId) -> bool {
+ self.rows
+ .iter()
+ .any(|row| row.account_id == Some(account_id))
+ }
+
+ pub(super) fn validate_ready(&self) -> Result<(), String> {
+ if self.rows.iter().any(|row| {
+ row.account_id.is_none() && !(row.role == "user_holding_lp" && row.signer && row.init)
+ }) {
+ return Err(String::from("submittable quote has an unresolved account"));
+ }
+ Ok(())
+ }
+
+ pub(super) fn wallet_args(
+ &self,
+ fresh_lp: Option,
+ ) -> Result<(Vec, Vec), String> {
+ let mut account_ids = Vec::with_capacity(self.rows.len());
+ let mut signing_requirements = Vec::with_capacity(self.rows.len());
+ for row in &self.rows {
+ let account_id = match row.account_id {
+ Some(account_id) => account_id,
+ None if row.role == "user_holding_lp" => {
+ fresh_lp.ok_or_else(|| String::from("transaction plan has no LP holding"))?
+ }
+ None => return Err(String::from("transaction plan has an unresolved account")),
+ };
+ account_ids.push(account_id);
+ signing_requirements.push(row.signer);
+ }
+ Ok((account_ids, signing_requirements))
+ }
+}
+
+impl AccountPlanRow {
+ pub(super) fn new(
+ role: &'static str,
+ account_id: Option,
+ expected_program: Option,
+ action: &'static str,
+ signer: bool,
+ init: bool,
+ ) -> Self {
+ Self {
+ role,
+ account_id,
+ expected_program,
+ action,
+ signer,
+ init,
+ }
+ }
+
+ fn preview(&self, order: usize) -> Value {
+ json!({
+ "order": order,
+ "role": self.role,
+ "accountId": self.account_id.map(|id| id.to_string()),
+ "expectedProgramId": self.expected_program.map(program_id_base58),
+ "action": self.action,
+ "writable": self.action != "read",
+ "signer": self.signer,
+ "init": self.init,
+ })
+ }
+}
diff --git a/apps/amm/client/src/api/quote.rs b/apps/amm/client/src/api/quote.rs
new file mode 100644
index 00000000..9d4e3226
--- /dev/null
+++ b/apps/amm/client/src/api/quote.rs
@@ -0,0 +1,726 @@
+use alloy_primitives::U256;
+use amm_core::{
+ is_supported_fee_tier, isqrt_product, mul_div_floor, spot_price_q64_64, PoolDefinition,
+ FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY,
+};
+use nssa_core::{
+ account::{Account, AccountId},
+ program::ProgramId,
+};
+use serde_json::{json, Value};
+use token_core::TokenDefinition;
+use twap_oracle_core::CurrentTickAccount;
+
+use super::{
+ accounts::{active_account_plan, missing_account_plan},
+ clock::decode_clock,
+ commitment::{QuoteCommitment, RequestCommitment},
+ context::fungible_definition,
+ funding::{funding_commitments, funding_issues, hash_quote},
+ holding::{decode_fungible_holding, select_holding, wallet_holdings},
+ pair::{derive_pair, is_canonical_pair, PairIds},
+ position::{
+ AccountPlan, AccountPlanHoldings, EvaluatedQuote, NewPositionPlan, QuoteBranch,
+ QuoteComputation,
+ },
+ quote_error::{fatal_quote, issue},
+ QuoteRequest, SCHEMA,
+};
+use crate::account::{
+ decode_account, parse_base58_id, parse_program_id, program_id_bytes, AccountRead,
+};
+
+const DEFAULT_SLIPPAGE_BPS: u32 = 50;
+const MAX_SLIPPAGE_BPS: u32 = 5_000;
+const HIGH_SLIPPAGE_BPS: u32 = 2_000;
+pub(super) const Q64: u128 = 1_u128 << 64;
+
+pub(super) fn quote(request: QuoteRequest) -> Result {
+ Ok(compute_quote(&request)?.into_value(&request.request))
+}
+
+pub(super) fn compute_quote(input: &QuoteRequest) -> Result {
+ if input.request.schema != SCHEMA {
+ return Ok(fatal_quote(
+ "unsupported_schema",
+ &["schema"],
+ json!({ "received": input.request.schema }),
+ ));
+ }
+ let amm_program = parse_program_id(&input.amm_program_id)?;
+ let token_a = match parse_base58_id(&input.request.token_a_id, "token A id") {
+ Ok(id) => id,
+ Err(_) => return Ok(fatal_quote("invalid_token_id", &["tokenAId"], json!({}))),
+ };
+ let token_b = match parse_base58_id(&input.request.token_b_id, "token B id") {
+ Ok(id) => id,
+ Err(_) => return Ok(fatal_quote("invalid_token_id", &["tokenBId"], json!({}))),
+ };
+ if token_a == token_b {
+ return Ok(fatal_quote(
+ "same_token_pair",
+ &["tokenAId", "tokenBId"],
+ json!({}),
+ ));
+ }
+ if !is_canonical_pair(token_a, token_b) {
+ return Ok(fatal_quote(
+ "non_canonical_pair",
+ &["tokenAId", "tokenBId"],
+ json!({}),
+ ));
+ }
+ if !is_supported_fee_tier(u128::from(input.request.fee_bps)) {
+ return Ok(fatal_quote(
+ "invalid_fee_tier",
+ &["feeBps"],
+ json!({ "feeBps": input.request.fee_bps }),
+ ));
+ }
+
+ let pair = match derive_pair(amm_program, token_a, token_b, &input.snapshot.config) {
+ Ok(pair) => pair,
+ Err(_) => return Ok(fatal_quote("config_unavailable", &[], json!({}))),
+ };
+ if let Some(error) = AccountPlan::validate_snapshot_ids(&pair, &input.snapshot) {
+ return Ok(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": error }),
+ ));
+ }
+ for (read, token_id, field) in [
+ (&input.snapshot.token_a, token_a, "tokenAId"),
+ (&input.snapshot.token_b, token_b, "tokenBId"),
+ ] {
+ if let Err(error) = fungible_definition(Some(read), token_id, pair.token_program) {
+ return Ok(fatal_quote(
+ error.code,
+ &[field],
+ json!({ "tokenId": token_id.to_string() }),
+ ));
+ }
+ }
+
+ let (_, pool_account) = match decode_account(&input.snapshot.pool) {
+ Ok(value) => value,
+ Err(_) => {
+ return Ok(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": "pool", "accountId": pair.pool.to_string() }),
+ ))
+ }
+ };
+ if pool_account == Account::default() {
+ compute_missing_quote(input, amm_program, pair)
+ } else {
+ compute_active_quote(input, amm_program, pair, pool_account)
+ }
+}
+
+fn compute_missing_quote(
+ input: &QuoteRequest,
+ amm_program: ProgramId,
+ pair: PairIds,
+) -> Result {
+ for (read, role) in [
+ (&input.snapshot.vault_a, "vault_a"),
+ (&input.snapshot.vault_b, "vault_b"),
+ (&input.snapshot.lp_definition, "lp_definition"),
+ (&input.snapshot.lp_lock_holding, "lp_lock_holding"),
+ (&input.snapshot.current_tick, "current_tick"),
+ ] {
+ let Ok((_, account)) = decode_account(read) else {
+ return Ok(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": role }),
+ ));
+ };
+ if account != Account::default() {
+ return Ok(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "role": role }),
+ ));
+ }
+ }
+ if !valid_clock(&input.snapshot.clock, pair.clock) {
+ return Ok(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": "clock" }),
+ ));
+ }
+
+ let requested_price = match raw_value(input.request.initial_price_real_raw.as_deref()) {
+ Ok(value) if value > 0 => value,
+ Ok(_) => {
+ return Ok(fatal_quote(
+ "amount_must_be_positive",
+ &["initialPriceRealRaw"],
+ json!({}),
+ ))
+ }
+ Err(code) => return Ok(fatal_quote(code, &["initialPriceRealRaw"], json!({}))),
+ };
+ let (minimum_a, minimum_b) = minimum_opening_pair(requested_price)?;
+ let direct_amounts =
+ input.request.amount_a_raw.is_some() || input.request.amount_b_raw.is_some();
+ let (amount_a, amount_b) = if direct_amounts {
+ let amount_a = match raw_value(input.request.amount_a_raw.as_deref()) {
+ Ok(value) if value > 0 => value,
+ Ok(_) => {
+ return Ok(fatal_quote(
+ "amount_must_be_positive",
+ &["amountARaw"],
+ json!({}),
+ ))
+ }
+ Err(code) => return Ok(fatal_quote(code, &["amountARaw"], json!({}))),
+ };
+ let amount_b = match raw_value(input.request.amount_b_raw.as_deref()) {
+ Ok(value) if value > 0 => value,
+ Ok(_) => {
+ return Ok(fatal_quote(
+ "amount_must_be_positive",
+ &["amountBRaw"],
+ json!({}),
+ ))
+ }
+ Err(code) => return Ok(fatal_quote(code, &["amountBRaw"], json!({}))),
+ };
+ if spot_price_q64_64(amount_a, amount_b) != requested_price {
+ return Ok(fatal_quote(
+ "deposit_ratio_mismatch",
+ &["amountARaw", "amountBRaw"],
+ json!({}),
+ ));
+ }
+ (amount_a, amount_b)
+ } else {
+ (minimum_a, minimum_b)
+ };
+ let initial_lp = isqrt_product(amount_a, amount_b);
+ if initial_lp <= MINIMUM_LIQUIDITY {
+ return Ok(fatal_quote(
+ "amount_too_low",
+ &["amountARaw", "amountBRaw"],
+ json!({ "minimumLiquidityRaw": MINIMUM_LIQUIDITY.to_string() }),
+ ));
+ }
+ let expected_lp = initial_lp - MINIMUM_LIQUIDITY;
+
+ let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
+ let holding_a = select_holding(&holdings, pair.token_a);
+ let holding_b = select_holding(&holdings, pair.token_b);
+ let funding = funding_issues(
+ input.snapshot.wallet_available,
+ pair,
+ &holding_a,
+ amount_a,
+ &holding_b,
+ amount_b,
+ ["amountARaw", "amountBRaw"],
+ );
+ let can_submit = funding.is_empty();
+ let mut account_plan = missing_account_plan(
+ input,
+ pair,
+ amm_program,
+ AccountPlanHoldings {
+ token_a: holding_a.as_ref(),
+ token_b: holding_b.as_ref(),
+ lp: None,
+ },
+ )?;
+ let sources = account_plan.take_sources();
+ let funding_commitment = funding_commitments(pair, &holding_a, amount_a, &holding_b, amount_b);
+ let commitment = QuoteCommitment {
+ schema: String::from(SCHEMA),
+ network_id: input.network_id.clone(),
+ network_fingerprint: input.network_fingerprint.clone(),
+ amm_program_id: program_id_bytes(amm_program),
+ token_a_id: pair.token_a.into_value(),
+ token_b_id: pair.token_b.into_value(),
+ fee_bps: input.request.fee_bps,
+ pool_status: 0,
+ request: RequestCommitment::Missing { amount_a, amount_b },
+ max_a: amount_a,
+ max_b: amount_b,
+ actual_a: amount_a,
+ actual_b: amount_b,
+ expected_lp,
+ lp_guard: MINIMUM_LIQUIDITY,
+ requires_fresh_lp: true,
+ sources,
+ funding: funding_commitment,
+ warnings: Vec::new(),
+ };
+ let quote_hash = hash_quote(&commitment)?;
+ let preview = account_plan.preview();
+ let value = json!({
+ "schema": SCHEMA,
+ "status": "ok",
+ "canSubmit": can_submit,
+ "code": if can_submit { "ready" } else { "funding_required" },
+ "poolStatus": "missing_pool",
+ "instruction": "NewDefinition",
+ "quoteHash": quote_hash,
+ "feeBps": input.request.fee_bps,
+ "poolId": pair.pool.to_string(),
+ "tokenAId": pair.token_a.to_string(),
+ "tokenBId": pair.token_b.to_string(),
+ "maxAmountARaw": amount_a.to_string(),
+ "maxAmountBRaw": amount_b.to_string(),
+ "actualAmountARaw": amount_a.to_string(),
+ "actualAmountBRaw": amount_b.to_string(),
+ "expectedLpRaw": expected_lp.to_string(),
+ "lockedLpRaw": MINIMUM_LIQUIDITY.to_string(),
+ "initialPriceRealRaw": spot_price_q64_64(amount_a, amount_b).to_string(),
+ "minimumAmountARaw": minimum_a.to_string(),
+ "minimumAmountBRaw": minimum_b.to_string(),
+ "requiresFreshLp": true,
+ "accountPreview": preview,
+ "errors": funding,
+ "warnings": [],
+ });
+ let plan = if can_submit {
+ Some(NewPositionPlan::new(
+ account_plan,
+ QuoteBranch::Missing { amount_a, amount_b },
+ )?)
+ } else {
+ None
+ };
+ Ok(QuoteComputation::Evaluated(EvaluatedQuote {
+ value,
+ quote_hash,
+ plan,
+ }))
+}
+
+fn compute_active_quote(
+ input: &QuoteRequest,
+ amm_program: ProgramId,
+ pair: PairIds,
+ pool_account: Account,
+) -> Result {
+ if pool_account.program_owner != amm_program {
+ return Ok(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "owner_mismatch" }),
+ ));
+ }
+ let Ok(pool) = PoolDefinition::try_from(&pool_account.data) else {
+ return Ok(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "invalid_pool_data" }),
+ ));
+ };
+ let stored_reversed = if pool.definition_token_a_id == pair.token_a
+ && pool.definition_token_b_id == pair.token_b
+ {
+ false
+ } else if pool.definition_token_a_id == pair.token_b
+ && pool.definition_token_b_id == pair.token_a
+ {
+ true
+ } else {
+ return Ok(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "pair_mismatch" }),
+ ));
+ };
+ if pool.reserve_a == 0 || pool.reserve_b == 0 || pool.liquidity_pool_supply == 0 {
+ return Ok(fatal_quote("pool_inactive", &[], json!({})));
+ }
+ if pool.fees != u128::from(input.request.fee_bps) {
+ return Ok(fatal_quote(
+ "fee_tier_mismatch",
+ &["feeBps"],
+ json!({ "poolFeeBps": pool.fees.to_string() }),
+ ));
+ }
+ if !is_supported_fee_tier(pool.fees) {
+ return Ok(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "unsupported_pool_fee" }),
+ ));
+ }
+
+ let max_a = match raw_value(input.request.max_amount_a_raw.as_deref()) {
+ Ok(value) if value > 0 => value,
+ Ok(_) => {
+ return Ok(fatal_quote(
+ "amount_must_be_positive",
+ &["maxAmountARaw"],
+ json!({}),
+ ))
+ }
+ Err(code) => return Ok(fatal_quote(code, &["maxAmountARaw"], json!({}))),
+ };
+ let max_b = match raw_value(input.request.max_amount_b_raw.as_deref()) {
+ Ok(value) if value > 0 => value,
+ Ok(_) => {
+ return Ok(fatal_quote(
+ "amount_must_be_positive",
+ &["maxAmountBRaw"],
+ json!({}),
+ ))
+ }
+ Err(code) => return Ok(fatal_quote(code, &["maxAmountBRaw"], json!({}))),
+ };
+ let slippage_bps = input.request.slippage_bps.unwrap_or(DEFAULT_SLIPPAGE_BPS);
+ if slippage_bps > MAX_SLIPPAGE_BPS {
+ return Ok(fatal_quote(
+ "invalid_slippage",
+ &["slippageBps"],
+ json!({ "maximum": MAX_SLIPPAGE_BPS }),
+ ));
+ }
+
+ let (stored_max_a, stored_max_b) = if stored_reversed {
+ (max_b, max_a)
+ } else {
+ (max_a, max_b)
+ };
+ let ideal_a = mul_div_floor(pool.reserve_a, stored_max_b, pool.reserve_b);
+ let ideal_b = mul_div_floor(pool.reserve_b, stored_max_a, pool.reserve_a);
+ let stored_actual_a = stored_max_a.min(ideal_a);
+ let stored_actual_b = stored_max_b.min(ideal_b);
+ if stored_actual_a == 0 || stored_actual_b == 0 {
+ return Ok(fatal_quote(
+ "amount_too_low",
+ &["maxAmountARaw", "maxAmountBRaw"],
+ json!({}),
+ ));
+ }
+ let expected_lp =
+ mul_div_floor(pool.liquidity_pool_supply, stored_actual_a, pool.reserve_a).min(
+ mul_div_floor(pool.liquidity_pool_supply, stored_actual_b, pool.reserve_b),
+ );
+ if expected_lp == 0 {
+ return Ok(fatal_quote(
+ "amount_too_low",
+ &["maxAmountARaw", "maxAmountBRaw"],
+ json!({}),
+ ));
+ }
+ let minimum_lp = mul_div_floor(
+ expected_lp,
+ FEE_BPS_DENOMINATOR - u128::from(slippage_bps),
+ FEE_BPS_DENOMINATOR,
+ );
+ if minimum_lp == 0 {
+ return Ok(fatal_quote("minimum_lp_zero", &["slippageBps"], json!({})));
+ }
+ let (actual_a, actual_b, reserve_a, reserve_b) = if stored_reversed {
+ (
+ stored_actual_b,
+ stored_actual_a,
+ pool.reserve_b,
+ pool.reserve_a,
+ )
+ } else {
+ (
+ stored_actual_a,
+ stored_actual_b,
+ pool.reserve_a,
+ pool.reserve_b,
+ )
+ };
+
+ if let Some(error) = validate_active_accounts(input, pair, &pool, stored_reversed) {
+ return Ok(error);
+ }
+ let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
+ let holding_a = select_holding(&holdings, pair.token_a);
+ let holding_b = select_holding(&holdings, pair.token_b);
+ let lp_holding = select_holding(&holdings, pair.lp_definition);
+ let requires_fresh_lp = lp_holding.is_none();
+ let funding = funding_issues(
+ input.snapshot.wallet_available,
+ pair,
+ &holding_a,
+ actual_a,
+ &holding_b,
+ actual_b,
+ ["maxAmountARaw", "maxAmountBRaw"],
+ );
+ let can_submit = funding.is_empty();
+ let warnings = if slippage_bps >= HIGH_SLIPPAGE_BPS {
+ vec![issue(
+ "high_slippage",
+ "High slippage tolerance.",
+ &["slippageBps"],
+ json!({ "slippageBps": slippage_bps }),
+ )]
+ } else {
+ Vec::new()
+ };
+ let warning_codes = warnings
+ .iter()
+ .filter_map(|warning| warning["code"].as_str().map(String::from))
+ .collect();
+ let mut account_plan = active_account_plan(
+ input,
+ pair,
+ amm_program,
+ &pool,
+ stored_reversed,
+ AccountPlanHoldings {
+ token_a: holding_a.as_ref(),
+ token_b: holding_b.as_ref(),
+ lp: lp_holding.as_ref(),
+ },
+ )?;
+ let sources = account_plan.take_sources();
+ let commitment = QuoteCommitment {
+ schema: String::from(SCHEMA),
+ network_id: input.network_id.clone(),
+ network_fingerprint: input.network_fingerprint.clone(),
+ amm_program_id: program_id_bytes(amm_program),
+ token_a_id: pair.token_a.into_value(),
+ token_b_id: pair.token_b.into_value(),
+ fee_bps: input.request.fee_bps,
+ pool_status: 1,
+ request: RequestCommitment::Active {
+ max_a,
+ max_b,
+ slippage_bps,
+ },
+ max_a,
+ max_b,
+ actual_a,
+ actual_b,
+ expected_lp,
+ lp_guard: minimum_lp,
+ requires_fresh_lp,
+ sources,
+ funding: funding_commitments(pair, &holding_a, actual_a, &holding_b, actual_b),
+ warnings: warning_codes,
+ };
+ let quote_hash = hash_quote(&commitment)?;
+ let preview = account_plan.preview();
+ let value = json!({
+ "schema": SCHEMA,
+ "status": "ok",
+ "canSubmit": can_submit,
+ "code": if can_submit { "ready" } else { "funding_required" },
+ "poolStatus": "active_pool",
+ "instruction": "AddLiquidity",
+ "quoteHash": quote_hash,
+ "feeBps": input.request.fee_bps,
+ "poolFeeBps": pool.fees.to_string(),
+ "poolId": pair.pool.to_string(),
+ "tokenAId": pair.token_a.to_string(),
+ "tokenBId": pair.token_b.to_string(),
+ "maxAmountARaw": max_a.to_string(),
+ "maxAmountBRaw": max_b.to_string(),
+ "actualAmountARaw": actual_a.to_string(),
+ "actualAmountBRaw": actual_b.to_string(),
+ "reserveARaw": reserve_a.to_string(),
+ "reserveBRaw": reserve_b.to_string(),
+ "liquiditySupplyRaw": pool.liquidity_pool_supply.to_string(),
+ "expectedLpRaw": expected_lp.to_string(),
+ "minimumLpRaw": minimum_lp.to_string(),
+ "initialPriceRealRaw": spot_price_q64_64(reserve_a, reserve_b).to_string(),
+ "requiresFreshLp": requires_fresh_lp,
+ "accountPreview": preview,
+ "errors": funding,
+ "warnings": warnings,
+ });
+ let plan = if can_submit {
+ Some(NewPositionPlan::new(
+ account_plan,
+ QuoteBranch::Active {
+ max_a,
+ max_b,
+ minimum_lp,
+ stored_reversed,
+ },
+ )?)
+ } else {
+ None
+ };
+ Ok(QuoteComputation::Evaluated(EvaluatedQuote {
+ value,
+ quote_hash,
+ plan,
+ }))
+}
+
+fn validate_active_accounts(
+ input: &QuoteRequest,
+ pair: PairIds,
+ pool: &PoolDefinition,
+ stored_reversed: bool,
+) -> Option {
+ let expected_vault_a = if stored_reversed {
+ pair.vault_b
+ } else {
+ pair.vault_a
+ };
+ let expected_vault_b = if stored_reversed {
+ pair.vault_a
+ } else {
+ pair.vault_b
+ };
+ if pool.vault_a_id != expected_vault_a
+ || pool.vault_b_id != expected_vault_b
+ || pool.liquidity_pool_id != pair.lp_definition
+ {
+ return Some(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "pool_account_mismatch" }),
+ ));
+ }
+ let (canonical_vault_a, canonical_vault_b) = (
+ decode_holding(&input.snapshot.vault_a, pair.token_program, pair.token_a),
+ decode_holding(&input.snapshot.vault_b, pair.token_program, pair.token_b),
+ );
+ let (Ok(vault_a_balance), Ok(vault_b_balance)) = (canonical_vault_a, canonical_vault_b) else {
+ return Some(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": "vault" }),
+ ));
+ };
+ let (reserve_a, reserve_b) = if stored_reversed {
+ (pool.reserve_b, pool.reserve_a)
+ } else {
+ (pool.reserve_a, pool.reserve_b)
+ };
+ if vault_a_balance < reserve_a || vault_b_balance < reserve_b {
+ return Some(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "vault_below_reserve" }),
+ ));
+ }
+ let Ok((lp_id, lp_account)) = decode_account(&input.snapshot.lp_definition) else {
+ return Some(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": "lp_definition" }),
+ ));
+ };
+ if lp_id != pair.lp_definition
+ || lp_account.program_owner != pair.token_program
+ || !matches!(
+ TokenDefinition::try_from(&lp_account.data),
+ Ok(TokenDefinition::Fungible { .. })
+ )
+ {
+ return Some(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "invalid_lp_definition" }),
+ ));
+ }
+ let Ok((tick_id, tick_account)) = decode_account(&input.snapshot.current_tick) else {
+ return Some(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": "current_tick" }),
+ ));
+ };
+ if tick_id != pair.current_tick
+ || tick_account.program_owner != pair.twap_program
+ || CurrentTickAccount::try_from(&tick_account.data).is_err()
+ {
+ return Some(fatal_quote(
+ "pool_unavailable",
+ &[],
+ json!({ "reason": "invalid_current_tick" }),
+ ));
+ }
+ if !valid_clock(&input.snapshot.clock, pair.clock) {
+ return Some(fatal_quote(
+ "account_read_failed",
+ &[],
+ json!({ "role": "clock" }),
+ ));
+ }
+ None
+}
+
+fn decode_holding(
+ read: &AccountRead,
+ token_program: ProgramId,
+ definition_id: AccountId,
+) -> Result {
+ let holding = decode_fungible_holding(read, token_program)?;
+ if holding.definition_id != definition_id {
+ return Err(String::from("invalid fungible holding"));
+ }
+ Ok(holding.balance)
+}
+
+fn valid_clock(read: &AccountRead, expected_id: AccountId) -> bool {
+ matches!(decode_clock(read), Ok((id, _)) if id == expected_id)
+}
+
+fn raw_value(value: Option<&str>) -> Result {
+ let Some(value) = value else {
+ return Err("amount_required");
+ };
+ if value.is_empty() {
+ return Err("amount_required");
+ }
+ if !value.bytes().all(|byte| byte.is_ascii_digit()) {
+ return Err("invalid_raw_amount");
+ }
+ value.parse().map_err(|_| "invalid_raw_amount")
+}
+
+pub(super) fn minimum_opening_pair(price: u128) -> Result<(u128, u128), String> {
+ let minimum_initial_lp = U256::from(MINIMUM_LIQUIDITY + 1);
+ let target_product = minimum_initial_lp
+ .checked_mul(minimum_initial_lp)
+ .ok_or_else(|| String::from("minimum liquidity product overflow"))?;
+ if price >= Q64 {
+ let amount_a = binary_search_min(1, MINIMUM_LIQUIDITY + 1, |amount_a| {
+ let amount_b = div_ceil_u256(U256::from(amount_a) * U256::from(price), U256::from(Q64));
+ U256::from(amount_a) * amount_b >= target_product
+ });
+ let amount_b = div_ceil_u256(U256::from(amount_a) * U256::from(price), U256::from(Q64));
+ Ok((
+ amount_a,
+ u128::try_from(amount_b).map_err(|_| String::from("opening amount overflow"))?,
+ ))
+ } else {
+ let amount_b = binary_search_min(1, MINIMUM_LIQUIDITY + 1, |amount_b| {
+ let amount_a = div_ceil_u256(U256::from(amount_b) * U256::from(Q64), U256::from(price));
+ amount_a * U256::from(amount_b) >= target_product
+ });
+ let amount_a = div_ceil_u256(U256::from(amount_b) * U256::from(Q64), U256::from(price));
+ Ok((
+ u128::try_from(amount_a).map_err(|_| String::from("opening amount overflow"))?,
+ amount_b,
+ ))
+ }
+}
+
+fn binary_search_min(mut low: u128, mut high: u128, predicate: impl Fn(u128) -> bool) -> u128 {
+ while low < high {
+ let mid = low + (high - low) / 2;
+ if predicate(mid) {
+ high = mid;
+ } else {
+ low = mid + 1;
+ }
+ }
+ low
+}
+
+pub(super) fn div_ceil_u256(numerator: U256, denominator: U256) -> U256 {
+ numerator.div_ceil(denominator)
+}
diff --git a/apps/amm/client/src/api/quote_error.rs b/apps/amm/client/src/api/quote_error.rs
new file mode 100644
index 00000000..df10ffd7
--- /dev/null
+++ b/apps/amm/client/src/api/quote_error.rs
@@ -0,0 +1,25 @@
+use serde_json::{json, Value};
+
+use super::position::{QuoteComputation, QuoteFailure};
+
+pub(super) fn issue(code: &str, message: &str, fields: &[&str], details: Value) -> Value {
+ json!({
+ "code": code,
+ "message": message,
+ "details": details,
+ "recoverable": true,
+ "blockingFields": fields,
+ })
+}
+
+pub(super) fn fatal_quote(
+ code: &'static str,
+ fields: &[&'static str],
+ details: Value,
+) -> QuoteComputation {
+ QuoteComputation::Failed(QuoteFailure {
+ code,
+ fields: fields.to_vec(),
+ details,
+ })
+}
diff --git a/apps/amm/client/src/api/request.rs b/apps/amm/client/src/api/request.rs
new file mode 100644
index 00000000..082b143d
--- /dev/null
+++ b/apps/amm/client/src/api/request.rs
@@ -0,0 +1,116 @@
+use serde::Deserialize;
+
+use crate::account::AccountRead;
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct ConfigIdRequest {
+ pub amm_program_id: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct TokenIdsRequest {
+ pub amm_program_id: String,
+ pub config: AccountRead,
+ #[serde(default)]
+ pub wallet_accounts: Vec,
+ #[serde(default)]
+ pub configured_token_ids: Vec,
+ #[serde(default)]
+ pub recent_token_ids: Vec,
+ #[serde(default)]
+ pub resolved_token_ids: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct ContextRequest {
+ pub network_id: String,
+ pub network_fingerprint: String,
+ pub amm_program_id: String,
+ pub wallet_available: bool,
+ pub config: AccountRead,
+ #[serde(default)]
+ pub wallet_accounts: Vec,
+ #[serde(default)]
+ pub token_definitions: Vec,
+ #[serde(default)]
+ pub configured_token_ids: Vec,
+ #[serde(default)]
+ pub recent_token_ids: Vec,
+ #[serde(default)]
+ pub resolved_token_ids: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct PairIdsRequest {
+ pub amm_program_id: String,
+ pub config: AccountRead,
+ pub token_a_id: String,
+ pub token_b_id: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct PositionRequest {
+ pub schema: String,
+ pub token_a_id: String,
+ pub token_b_id: String,
+ pub fee_bps: u32,
+ #[serde(default)]
+ pub amount_a_raw: Option,
+ #[serde(default)]
+ pub amount_b_raw: Option,
+ #[serde(default)]
+ pub max_amount_a_raw: Option,
+ #[serde(default)]
+ pub max_amount_b_raw: Option,
+ #[serde(default)]
+ pub slippage_bps: Option,
+ #[serde(default)]
+ pub initial_price_real_raw: Option,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct PairSnapshot {
+ pub config: AccountRead,
+ pub token_a: AccountRead,
+ pub token_b: AccountRead,
+ pub pool: AccountRead,
+ pub vault_a: AccountRead,
+ pub vault_b: AccountRead,
+ pub lp_definition: AccountRead,
+ pub lp_lock_holding: AccountRead,
+ pub current_tick: AccountRead,
+ pub clock: AccountRead,
+ pub wallet_available: bool,
+ #[serde(default)]
+ pub wallet_accounts: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct QuoteRequest {
+ pub network_id: String,
+ pub network_fingerprint: String,
+ pub amm_program_id: String,
+ pub request: PositionRequest,
+ pub snapshot: PairSnapshot,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct PlanRequest {
+ pub network_id: String,
+ pub network_fingerprint: String,
+ pub amm_program_id: String,
+ pub request: PositionRequest,
+ pub snapshot: PairSnapshot,
+ pub quote_hash: String,
+ pub now_ms: u64,
+ #[serde(default)]
+ pub fresh_lp: Option,
+}
diff --git a/apps/amm/client/src/api/tests.rs b/apps/amm/client/src/api/tests.rs
new file mode 100644
index 00000000..520899da
--- /dev/null
+++ b/apps/amm/client/src/api/tests.rs
@@ -0,0 +1,703 @@
+use alloy_primitives::U256;
+use amm_core::{
+ compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda,
+ compute_vault_pda, isqrt_product, spot_price_q64_64, AmmConfig, PoolDefinition,
+ MINIMUM_LIQUIDITY,
+};
+use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
+use nssa_core::{
+ account::{Account, AccountId, Data, Nonce},
+ program::ProgramId,
+};
+use pretty_assertions::assert_eq;
+use serde_json::{json, Value};
+use token_core::{TokenDefinition, TokenHolding};
+use twap_oracle_core::{compute_current_tick_account_pda, CurrentTickAccount};
+
+use super::{
+ accounts::{active_account_plan, missing_account_plan},
+ context::{context, token_ids},
+ holding::{select_holding, wallet_holdings, SelectedHolding},
+ pair::{is_canonical_pair, pair_ids, PairIds},
+ plan::plan,
+ position::AccountPlanHoldings,
+ quote::{div_ceil_u256, minimum_opening_pair, quote, Q64},
+ ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest, QuoteRequest,
+ TokenIdsRequest, SCHEMA,
+};
+use crate::{
+ account::{account_id_hex, account_read, decode_account, parse_base58_id, program_id_bytes},
+ AccountRead,
+};
+
+const AMM_PROGRAM: ProgramId = [11; 8];
+const TOKEN_PROGRAM: ProgramId = [22; 8];
+const TWAP_PROGRAM: ProgramId = [33; 8];
+
+fn account(owner: ProgramId, data: Data) -> Account {
+ Account {
+ program_owner: owner,
+ balance: 0,
+ data,
+ nonce: Nonce(0),
+ }
+}
+
+fn default_read(id: AccountId) -> AccountRead {
+ account_read(id, &Account::default())
+}
+
+fn config_account() -> Account {
+ account(
+ AMM_PROGRAM,
+ Data::from(&AmmConfig {
+ token_program_id: TOKEN_PROGRAM,
+ twap_oracle_program_id: TWAP_PROGRAM,
+ authority: AccountId::new([7; 32]),
+ }),
+ )
+}
+
+fn token_definition(name: &str, supply: u128) -> Account {
+ account(
+ TOKEN_PROGRAM,
+ Data::from(&TokenDefinition::Fungible {
+ name: String::from(name),
+ total_supply: supply,
+ metadata_id: None,
+ authority: None,
+ }),
+ )
+}
+
+fn token_holding(definition_id: AccountId, balance: u128) -> Account {
+ account(
+ TOKEN_PROGRAM,
+ Data::from(&TokenHolding::Fungible {
+ definition_id,
+ balance,
+ }),
+ )
+}
+
+fn clock_account() -> Account {
+ account(
+ [44; 8],
+ Data::try_from(
+ ClockAccountData {
+ block_id: 10,
+ timestamp: 1_000,
+ }
+ .to_bytes(),
+ )
+ .unwrap(),
+ )
+}
+
+fn ids() -> PairIds {
+ let token_a = AccountId::new([2; 32]);
+ let token_b = AccountId::new([1; 32]);
+ let config = compute_config_pda(AMM_PROGRAM);
+ let pool = compute_pool_pda(AMM_PROGRAM, token_a, token_b);
+ PairIds {
+ token_a,
+ token_b,
+ config,
+ pool,
+ vault_a: compute_vault_pda(AMM_PROGRAM, pool, token_a),
+ vault_b: compute_vault_pda(AMM_PROGRAM, pool, token_b),
+ lp_definition: compute_liquidity_token_pda(AMM_PROGRAM, pool),
+ lp_lock_holding: compute_lp_lock_holding_pda(AMM_PROGRAM, pool),
+ current_tick: compute_current_tick_account_pda(TWAP_PROGRAM, pool),
+ clock: CLOCK_01_PROGRAM_ACCOUNT_ID,
+ token_program: TOKEN_PROGRAM,
+ twap_program: TWAP_PROGRAM,
+ }
+}
+
+fn base_snapshot(pair: PairIds) -> PairSnapshot {
+ let holding_a_id = AccountId::new([61; 32]);
+ let holding_b_id = AccountId::new([62; 32]);
+ PairSnapshot {
+ config: account_read(pair.config, &config_account()),
+ token_a: account_read(pair.token_a, &token_definition("A", 1_000_000)),
+ token_b: account_read(pair.token_b, &token_definition("B", 2_000_000)),
+ pool: default_read(pair.pool),
+ vault_a: default_read(pair.vault_a),
+ vault_b: default_read(pair.vault_b),
+ lp_definition: default_read(pair.lp_definition),
+ lp_lock_holding: default_read(pair.lp_lock_holding),
+ current_tick: default_read(pair.current_tick),
+ clock: account_read(pair.clock, &clock_account()),
+ wallet_available: true,
+ wallet_accounts: vec![
+ account_read(holding_a_id, &token_holding(pair.token_a, 1_000_000)),
+ account_read(holding_b_id, &token_holding(pair.token_b, 1_000_000)),
+ ],
+ }
+}
+
+fn request(pair: PairIds) -> PositionRequest {
+ assert!(is_canonical_pair(pair.token_a, pair.token_b));
+ PositionRequest {
+ schema: String::from(SCHEMA),
+ token_a_id: pair.token_a.to_string(),
+ token_b_id: pair.token_b.to_string(),
+ fee_bps: 30,
+ amount_a_raw: None,
+ amount_b_raw: None,
+ max_amount_a_raw: None,
+ max_amount_b_raw: None,
+ slippage_bps: None,
+ initial_price_real_raw: Some(Q64.to_string()),
+ }
+}
+
+fn amm_program_id() -> String {
+ hex::encode(program_id_bytes(AMM_PROGRAM))
+}
+
+struct Scenario {
+ pair: PairIds,
+ request: PositionRequest,
+ snapshot: PairSnapshot,
+ network_id: &'static str,
+ network_fingerprint: &'static str,
+}
+
+impl Scenario {
+ fn devnet() -> Self {
+ Self::new("devnet", "channel:test")
+ }
+
+ fn testnet() -> Self {
+ Self::new("testnet", "block10:test")
+ }
+
+ fn new(network_id: &'static str, network_fingerprint: &'static str) -> Self {
+ let pair = ids();
+ Self {
+ pair,
+ request: request(pair),
+ snapshot: base_snapshot(pair),
+ network_id,
+ network_fingerprint,
+ }
+ }
+
+ fn quote_request(&self) -> QuoteRequest {
+ QuoteRequest {
+ network_id: String::from(self.network_id),
+ network_fingerprint: String::from(self.network_fingerprint),
+ amm_program_id: amm_program_id(),
+ request: self.request.clone(),
+ snapshot: self.snapshot.clone(),
+ }
+ }
+
+ fn quote(&self) -> Value {
+ quote(self.quote_request()).unwrap()
+ }
+
+ fn plan(self, quote_hash: impl Into, fresh_lp: Option) -> Value {
+ plan(PlanRequest {
+ network_id: String::from(self.network_id),
+ network_fingerprint: String::from(self.network_fingerprint),
+ amm_program_id: amm_program_id(),
+ request: self.request,
+ snapshot: self.snapshot,
+ quote_hash: quote_hash.into(),
+ now_ms: 2_000,
+ fresh_lp,
+ })
+ .unwrap()
+ }
+}
+
+fn assert_preview_matches_plan(
+ quote_value: &Value,
+ plan_value: &Value,
+ fresh_lp: Option,
+) {
+ let preview = quote_value["accountPreview"].as_array().unwrap();
+ let account_ids = plan_value["accountIds"].as_array().unwrap();
+ let signing_requirements = plan_value["signingRequirements"].as_array().unwrap();
+ assert_eq!(preview.len(), account_ids.len());
+ assert_eq!(preview.len(), signing_requirements.len());
+
+ for (order, row) in preview.iter().enumerate() {
+ assert_eq!(row["order"], order);
+ assert_eq!(row["signer"], signing_requirements[order]);
+ if let Some(account_id) = row["accountId"].as_str() {
+ let account_id = parse_base58_id(account_id, "preview account id").unwrap();
+ assert_eq!(account_ids[order], account_id_hex(account_id));
+ } else {
+ assert_eq!(row["role"], "user_holding_lp");
+ assert_eq!(account_ids[order], account_id_hex(fresh_lp.unwrap()));
+ }
+ }
+}
+
+#[test]
+fn account_plan_sources_follow_pool_branch() {
+ let scenario = Scenario::devnet();
+ let pair = scenario.pair;
+ let input = scenario.quote_request();
+ let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
+ let holding_a = select_holding(&holdings, pair.token_a);
+ let holding_b = select_holding(&holdings, pair.token_b);
+
+ let missing = missing_account_plan(
+ &input,
+ pair,
+ AMM_PROGRAM,
+ AccountPlanHoldings {
+ token_a: holding_a.as_ref(),
+ token_b: holding_b.as_ref(),
+ lp: None,
+ },
+ )
+ .unwrap();
+ assert_eq!(
+ missing
+ .sources
+ .iter()
+ .map(|source| source.role.as_str())
+ .collect::>(),
+ vec![
+ "config",
+ "token_a",
+ "token_b",
+ "pool",
+ "vault_a",
+ "vault_b",
+ "lp_definition",
+ "lp_lock_holding",
+ "current_tick",
+ "holding_a",
+ "holding_b",
+ ]
+ );
+
+ let active = active_account_plan(
+ &input,
+ pair,
+ AMM_PROGRAM,
+ &PoolDefinition::default(),
+ false,
+ AccountPlanHoldings {
+ token_a: holding_a.as_ref(),
+ token_b: holding_b.as_ref(),
+ lp: None,
+ },
+ )
+ .unwrap();
+ assert_eq!(
+ active
+ .sources
+ .iter()
+ .map(|source| source.role.as_str())
+ .collect::>(),
+ vec![
+ "config",
+ "token_a",
+ "token_b",
+ "pool",
+ "vault_a",
+ "vault_b",
+ "lp_definition",
+ "current_tick",
+ "holding_a",
+ "holding_b",
+ ]
+ );
+}
+
+#[test]
+fn minimum_pair_exceeds_protocol_lock() {
+ for price in [1, Q64 / 2_500, Q64 / 10, Q64, Q64 * 2, u128::MAX] {
+ let (amount_a, amount_b) = minimum_opening_pair(price).unwrap();
+ assert!(amount_a > 0);
+ assert!(amount_b > 0);
+ assert!(isqrt_product(amount_a, amount_b) > MINIMUM_LIQUIDITY);
+ }
+}
+
+#[test]
+fn minimum_pair_is_minimal_on_price_base_side() {
+ let (amount_a, amount_b) = minimum_opening_pair(Q64 * 2).unwrap();
+ assert!(isqrt_product(amount_a, amount_b) > MINIMUM_LIQUIDITY);
+ let previous_b = div_ceil_u256(
+ U256::from(amount_a - 1) * U256::from(Q64 * 2),
+ U256::from(Q64),
+ );
+ assert!(
+ U256::from(amount_a - 1) * previous_b <= U256::from(MINIMUM_LIQUIDITY * MINIMUM_LIQUIDITY)
+ );
+}
+
+#[test]
+fn highest_balance_holding_wins_then_lowest_id() {
+ let definition = AccountId::new([9; 32]);
+ let holding = |id: u8, balance| SelectedHolding {
+ id: AccountId::new([id; 32]),
+ definition_id: definition,
+ balance,
+ account: account(
+ TOKEN_PROGRAM,
+ Data::from(&TokenHolding::Fungible {
+ definition_id: definition,
+ balance,
+ }),
+ ),
+ };
+ let selected = select_holding(
+ &[holding(4, 10), holding(2, 20), holding(1, 20)],
+ definition,
+ )
+ .unwrap();
+ assert_eq!(selected.id, AccountId::new([1; 32]));
+}
+
+#[test]
+fn pair_manifest_uses_canonical_ids_and_current_program_types() {
+ let token_a = AccountId::new([2; 32]);
+ let token_b = AccountId::new([1; 32]);
+ let config_id = compute_config_pda(AMM_PROGRAM);
+ let result = pair_ids(PairIdsRequest {
+ amm_program_id: amm_program_id(),
+ config: account_read(config_id, &config_account()),
+ token_a_id: token_a.to_string(),
+ token_b_id: token_b.to_string(),
+ })
+ .unwrap();
+ assert_eq!(result["status"], "ok");
+ assert_eq!(result["tokenAId"], account_id_hex(token_a));
+ assert_eq!(result["tokenBId"], account_id_hex(token_b));
+ assert_eq!(
+ result["poolId"],
+ account_id_hex(compute_pool_pda(AMM_PROGRAM, token_a, token_b))
+ );
+}
+
+#[test]
+fn pair_manifest_reports_invalid_token_as_domain_error() {
+ let pair = ids();
+ let result = pair_ids(PairIdsRequest {
+ amm_program_id: amm_program_id(),
+ config: account_read(pair.config, &config_account()),
+ token_a_id: String::from("not-a-token-id"),
+ token_b_id: pair.token_b.to_string(),
+ })
+ .expect("invalid user input is a domain result");
+
+ assert_eq!(result["status"], "error");
+ assert_eq!(result["code"], "invalid_token_id");
+}
+
+#[test]
+fn pair_manifest_reports_unavailable_config_as_domain_error() {
+ let pair = ids();
+ let result = pair_ids(PairIdsRequest {
+ amm_program_id: amm_program_id(),
+ config: default_read(pair.config),
+ token_a_id: pair.token_a.to_string(),
+ token_b_id: pair.token_b.to_string(),
+ })
+ .expect("unavailable chain state is a domain result");
+
+ assert_eq!(result["status"], "error");
+ assert_eq!(result["code"], "config_unavailable");
+}
+
+#[test]
+fn token_manifest_includes_compatible_wallet_holdings() {
+ let config_id = compute_config_pda(AMM_PROGRAM);
+ let configured = AccountId::new([1; 32]);
+ let held = AccountId::new([2; 32]);
+ let recent = AccountId::new([3; 32]);
+ let resolved = AccountId::new([4; 32]);
+
+ let value = token_ids(TokenIdsRequest {
+ amm_program_id: amm_program_id(),
+ config: account_read(config_id, &config_account()),
+ wallet_accounts: vec![account_read(
+ AccountId::new([5; 32]),
+ &token_holding(held, 9),
+ )],
+ configured_token_ids: vec![account_id_hex(configured)],
+ recent_token_ids: vec![recent.to_string()],
+ resolved_token_ids: vec![resolved.to_string()],
+ })
+ .unwrap();
+
+ assert_eq!(
+ value["tokenIds"],
+ json!([
+ account_id_hex(configured),
+ account_id_hex(held),
+ account_id_hex(recent),
+ account_id_hex(resolved),
+ ])
+ );
+}
+
+#[test]
+fn wrong_program_holdings_do_not_contribute_token_candidates() {
+ let config_id = compute_config_pda(AMM_PROGRAM);
+ let config = config_account();
+ let definition = AccountId::new([2; 32]);
+ let wrong_owner_holding = account(
+ [99; 8],
+ Data::from(&TokenHolding::Fungible {
+ definition_id: definition,
+ balance: 9,
+ }),
+ );
+ let wallet_accounts = vec![account_read(AccountId::new([3; 32]), &wrong_owner_holding)];
+
+ let manifest = token_ids(TokenIdsRequest {
+ amm_program_id: amm_program_id(),
+ config: account_read(config_id, &config),
+ wallet_accounts: wallet_accounts.clone(),
+ configured_token_ids: Vec::new(),
+ recent_token_ids: Vec::new(),
+ resolved_token_ids: Vec::new(),
+ })
+ .unwrap();
+ assert_eq!(manifest["tokenIds"], json!([]));
+
+ let value = context(ContextRequest {
+ network_id: String::from("testnet"),
+ network_fingerprint: String::from("block10:abc"),
+ amm_program_id: amm_program_id(),
+ wallet_available: true,
+ config: account_read(config_id, &config),
+ wallet_accounts,
+ token_definitions: vec![account_read(
+ definition,
+ &token_definition("Token", 1_000_000),
+ )],
+ configured_token_ids: Vec::new(),
+ recent_token_ids: Vec::new(),
+ resolved_token_ids: Vec::new(),
+ })
+ .unwrap();
+ assert_eq!(value["tokens"], json!([]));
+}
+
+#[test]
+fn context_selects_tokens_without_holdings() {
+ let token_id = AccountId::new([3; 32]);
+ let config_id = compute_config_pda(AMM_PROGRAM);
+ let value = context(ContextRequest {
+ network_id: String::from("testnet"),
+ network_fingerprint: String::from("block10:abc"),
+ amm_program_id: amm_program_id(),
+ wallet_available: true,
+ config: account_read(config_id, &config_account()),
+ wallet_accounts: Vec::new(),
+ token_definitions: vec![account_read(
+ token_id,
+ &token_definition("Token", 1_000_000),
+ )],
+ configured_token_ids: vec![account_id_hex(token_id)],
+ recent_token_ids: Vec::new(),
+ resolved_token_ids: Vec::new(),
+ })
+ .unwrap();
+ assert_eq!(value["tokens"][0]["selectable"], true);
+ assert_eq!(value["tokens"][0]["sources"], json!(["config"]));
+ assert!(value["tokens"][0].get("holdingId").is_none());
+}
+
+#[test]
+fn missing_pool_snapshot_defaults_remain_real_accounts() {
+ let id = AccountId::new([5; 32]);
+ let read = default_read(id);
+ let (decoded_id, decoded) = decode_account(&read).unwrap();
+ assert_eq!(decoded_id, id);
+ assert_eq!(decoded, Account::default());
+}
+
+#[test]
+fn missing_pool_quote_and_plan_use_current_account_order() {
+ let scenario = Scenario::devnet();
+ let quote_value = scenario.quote();
+ assert_eq!(quote_value["status"], "ok");
+ assert_eq!(quote_value["poolStatus"], "missing_pool");
+ assert_eq!(quote_value["canSubmit"], true);
+ assert_eq!(quote_value["accountPreview"].as_array().unwrap().len(), 11);
+ let quote_hash = quote_value["quoteHash"].as_str().unwrap().to_owned();
+
+ let fresh_lp = AccountId::new([63; 32]);
+ let plan_value = scenario.plan(quote_hash, Some(default_read(fresh_lp)));
+ assert_eq!(plan_value["status"], "ready");
+ assert_eq!(plan_value["accountIds"].as_array().unwrap().len(), 11);
+ assert_eq!(plan_value["accountIds"][8], account_id_hex(fresh_lp));
+ assert_eq!(plan_value["signingRequirements"][6], true);
+ assert_eq!(plan_value["signingRequirements"][7], true);
+ assert_eq!(plan_value["signingRequirements"][8], true);
+ assert_preview_matches_plan("e_value, &plan_value, Some(fresh_lp));
+}
+
+#[test]
+fn missing_pool_plan_rejects_fresh_lp_account_collision() {
+ let scenario = Scenario::devnet();
+ let pool = scenario.pair.pool;
+ let quote_value = scenario.quote();
+ let quote_hash = quote_value["quoteHash"].as_str().unwrap().to_owned();
+
+ let plan_value = scenario.plan(quote_hash, Some(default_read(pool)));
+
+ assert_eq!(plan_value["status"], "error");
+ assert_eq!(plan_value["code"], "wallet_submission_failed");
+}
+
+#[test]
+fn missing_pool_quote_accepts_large_direct_raw_amounts() {
+ let mut scenario = Scenario::devnet();
+ let amount_a = 100_000_000;
+ let amount_b = 150_000_000;
+ scenario.request.amount_a_raw = Some(amount_a.to_string());
+ scenario.request.amount_b_raw = Some(amount_b.to_string());
+ scenario.request.initial_price_real_raw =
+ Some(spot_price_q64_64(amount_a, amount_b).to_string());
+
+ let quote_value = scenario.quote();
+
+ assert_eq!(quote_value["status"], "ok");
+ assert_eq!(quote_value["actualAmountARaw"], amount_a.to_string());
+ assert_eq!(quote_value["actualAmountBRaw"], amount_b.to_string());
+ assert!(quote_value.get("depositScaleBps").is_none());
+}
+
+#[test]
+fn advancing_clock_does_not_stale_quote() {
+ let mut scenario = Scenario::testnet();
+ let quote_value = scenario.quote();
+
+ scenario.snapshot.clock = account_read(
+ scenario.pair.clock,
+ &account(
+ [44; 8],
+ Data::try_from(
+ ClockAccountData {
+ block_id: 11,
+ timestamp: 1_500,
+ }
+ .to_bytes(),
+ )
+ .unwrap(),
+ ),
+ );
+ let plan_value = scenario.plan(
+ quote_value["quoteHash"].as_str().unwrap(),
+ Some(default_read(AccountId::new([63; 32]))),
+ );
+
+ assert_eq!(plan_value["status"], "ready");
+}
+
+#[test]
+fn active_pool_quote_uses_ratio_and_existing_lp_holding() {
+ let mut scenario = Scenario::testnet();
+ let pair = scenario.pair;
+ let pool = PoolDefinition {
+ definition_token_a_id: pair.token_a,
+ definition_token_b_id: pair.token_b,
+ vault_a_id: pair.vault_a,
+ vault_b_id: pair.vault_b,
+ liquidity_pool_id: pair.lp_definition,
+ liquidity_pool_supply: 10_000,
+ reserve_a: 10_000,
+ reserve_b: 20_000,
+ fees: 30,
+ };
+ scenario.snapshot.pool = account_read(pair.pool, &account(AMM_PROGRAM, Data::from(&pool)));
+ scenario.snapshot.vault_a =
+ account_read(pair.vault_a, &token_holding(pair.token_a, pool.reserve_a));
+ scenario.snapshot.vault_b =
+ account_read(pair.vault_b, &token_holding(pair.token_b, pool.reserve_b));
+ scenario.snapshot.lp_definition = account_read(
+ pair.lp_definition,
+ &account(
+ TOKEN_PROGRAM,
+ Data::from(&TokenDefinition::Fungible {
+ name: String::from("LP"),
+ total_supply: pool.liquidity_pool_supply,
+ metadata_id: None,
+ authority: Some(pair.lp_definition),
+ }),
+ ),
+ );
+ scenario.snapshot.current_tick = account_read(
+ pair.current_tick,
+ &account(
+ TWAP_PROGRAM,
+ Data::from(&CurrentTickAccount {
+ tick: 0,
+ last_updated: 1_000,
+ }),
+ ),
+ );
+ scenario.snapshot.wallet_accounts = vec![
+ account_read(
+ AccountId::new([61; 32]),
+ &token_holding(pair.token_a, 1_000),
+ ),
+ account_read(
+ AccountId::new([62; 32]),
+ &token_holding(pair.token_b, 2_000),
+ ),
+ ];
+ let lp_holding = AccountId::new([64; 32]);
+ scenario.snapshot.wallet_accounts.push(account_read(
+ lp_holding,
+ &token_holding(pair.lp_definition, 500),
+ ));
+ scenario.request.initial_price_real_raw = None;
+ scenario.request.max_amount_a_raw = Some(String::from("1000"));
+ scenario.request.max_amount_b_raw = Some(String::from("3000"));
+ scenario.request.slippage_bps = Some(50);
+
+ let quote_value = scenario.quote();
+ assert_eq!(quote_value["poolStatus"], "active_pool");
+ assert_eq!(quote_value["actualAmountARaw"], "1000");
+ assert_eq!(quote_value["actualAmountBRaw"], "2000");
+ assert_eq!(quote_value["expectedLpRaw"], "1000");
+ assert_eq!(quote_value["minimumLpRaw"], "995");
+ assert_eq!(quote_value["requiresFreshLp"], false);
+ assert_eq!(quote_value["canSubmit"], true);
+ assert_eq!(quote_value["errors"], json!([]));
+
+ let plan_value = scenario.plan(quote_value["quoteHash"].as_str().unwrap(), None);
+ assert_eq!(plan_value["status"], "ready");
+ assert_eq!(plan_value["accountIds"].as_array().unwrap().len(), 10);
+ assert_eq!(plan_value["accountIds"][7], account_id_hex(lp_holding));
+ assert_eq!(plan_value["signingRequirements"][7], false);
+ assert_preview_matches_plan("e_value, &plan_value, None);
+}
+
+#[test]
+fn matching_unfunded_quote_has_no_transaction_plan() {
+ let mut scenario = Scenario::devnet();
+ scenario.snapshot.wallet_available = false;
+ scenario.snapshot.wallet_accounts.clear();
+ let quote_value = scenario.quote();
+ assert_eq!(quote_value["canSubmit"], false);
+
+ let plan_value = scenario.plan(quote_value["quoteHash"].as_str().unwrap(), None);
+
+ assert_eq!(plan_value["status"], "error");
+ assert_eq!(plan_value["code"], "quote_not_submittable");
+ assert_eq!(plan_value["quote"], quote_value);
+}
+
+#[test]
+fn stale_hash_returns_recomputed_quote_without_plan() {
+ let value = Scenario::devnet().plan("sha256:deadbeef", None);
+ assert_eq!(value["status"], "error");
+ assert_eq!(value["code"], "quote_changed");
+ assert_eq!(value["quote"]["status"], "ok");
+}
diff --git a/apps/amm/client/src/ffi.rs b/apps/amm/client/src/ffi.rs
new file mode 100644
index 00000000..552e9508
--- /dev/null
+++ b/apps/amm/client/src/ffi.rs
@@ -0,0 +1,140 @@
+use std::{
+ ffi::{c_char, CStr, CString},
+ panic::{catch_unwind, AssertUnwindSafe},
+};
+
+use serde::{de::DeserializeOwned, Serialize};
+
+use crate::api::{
+ self, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PlanRequest,
+ QuoteRequest, TokenIdsRequest,
+};
+
+#[derive(Serialize)]
+struct Envelope {
+ ok: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ value: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ error: Option,
+}
+
+impl Envelope {
+ fn success(value: serde_json::Value) -> Self {
+ Self {
+ ok: true,
+ value: Some(value),
+ error: None,
+ }
+ }
+
+ fn failure(error: impl Into) -> Self {
+ Self {
+ ok: false,
+ value: None,
+ error: Some(error.into()),
+ }
+ }
+}
+
+fn call(request: *const c_char, operation: fn(T) -> AmmResult) -> *mut c_char {
+ let result = catch_unwind(AssertUnwindSafe(|| {
+ let request = request_text(request).map_err(AmmApiError::from)?;
+ let request = serde_json::from_str::(&request)
+ .map_err(|error| AmmApiError::from(format!("invalid request JSON: {error}")))?;
+ operation(request)
+ }));
+
+ let envelope = match result {
+ Ok(Ok(value)) => Envelope::success(value),
+ Ok(Err(error)) => Envelope::failure(error.to_string()),
+ Err(_) => Envelope::failure("internal panic"),
+ };
+ encode_envelope(&envelope)
+}
+
+fn request_text(request: *const c_char) -> Result {
+ if request.is_null() {
+ return Err(String::from("request pointer is null"));
+ }
+ // SAFETY: The C++ caller passes a live NUL-terminated UTF-8 buffer for this call.
+ let request = unsafe { CStr::from_ptr(request) };
+ request
+ .to_str()
+ .map(String::from)
+ .map_err(|error| format!("request is not UTF-8: {error}"))
+}
+
+fn encode_envelope(envelope: &Envelope) -> *mut c_char {
+ let json = serde_json::to_string(envelope).unwrap_or_else(|_| {
+ String::from(r#"{"ok":false,"error":"response serialization failed"}"#)
+ });
+ match CString::new(json) {
+ Ok(value) => value.into_raw(),
+ Err(_) => CString::new(r#"{"ok":false,"error":"response contains NUL"}"#)
+ .map_or(std::ptr::null_mut(), CString::into_raw),
+ }
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn amm_config_id(request_json: *const c_char) -> *mut c_char {
+ call::(request_json, api::config_id)
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn amm_token_ids(request_json: *const c_char) -> *mut c_char {
+ call::(request_json, api::token_ids)
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn amm_pair_ids(request_json: *const c_char) -> *mut c_char {
+ call::(request_json, api::pair_ids)
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn amm_context(request_json: *const c_char) -> *mut c_char {
+ call::(request_json, api::context)
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn amm_quote(request_json: *const c_char) -> *mut c_char {
+ call::(request_json, api::quote)
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn amm_plan(request_json: *const c_char) -> *mut c_char {
+ call::(request_json, api::plan)
+}
+
+/// Releases a string returned by an `amm_*` operation.
+///
+/// # Safety
+/// `value` must be null or a pointer returned by this library that has not been freed.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn amm_free(value: *mut c_char) {
+ if value.is_null() {
+ return;
+ }
+ // SAFETY: The caller contract requires a pointer produced by CString::into_raw above.
+ drop(unsafe { CString::from_raw(value) });
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ffi::CString;
+
+ use super::*;
+
+ #[test]
+ fn malformed_json_uses_boundary_failure_envelope() {
+ let request = CString::new("{").unwrap();
+ let response = amm_config_id(request.as_ptr());
+ assert!(!response.is_null());
+ // SAFETY: response was returned by amm_config_id and remains live until amm_free.
+ let text = unsafe { CStr::from_ptr(response) }.to_str().unwrap();
+ let value: serde_json::Value = serde_json::from_str(text).unwrap();
+ assert_eq!(value["ok"], false);
+ // SAFETY: response was allocated by this library and has not been freed.
+ unsafe { amm_free(response) };
+ }
+}
diff --git a/apps/amm/client/src/lib.rs b/apps/amm/client/src/lib.rs
new file mode 100644
index 00000000..9b407f7b
--- /dev/null
+++ b/apps/amm/client/src/lib.rs
@@ -0,0 +1,12 @@
+#![deny(unsafe_op_in_unsafe_fn)]
+
+mod account;
+mod ffi;
+
+pub mod api;
+
+pub use api::{
+ config_id, context, pair_ids, plan, quote, token_ids, AccountRead, AmmApiError, AmmResponse,
+ AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest,
+ PositionRequest, QuoteRequest, TokenIdsRequest, WalletAccount, NEW_POSITION_SCHEMA,
+};
diff --git a/apps/amm/client/tests/public_api.rs b/apps/amm/client/tests/public_api.rs
new file mode 100644
index 00000000..7d38289d
--- /dev/null
+++ b/apps/amm/client/tests/public_api.rs
@@ -0,0 +1,13 @@
+use amm_client::{config_id, ConfigIdRequest, NEW_POSITION_SCHEMA};
+
+#[test]
+fn direct_rust_api_does_not_require_ffi() {
+ let response = config_id(ConfigIdRequest {
+ amm_program_id: "0000000000000000000000000000000000000000000000000000000000000000".into(),
+ })
+ .expect("valid program ID should produce a response");
+
+ assert_eq!(response["status"], "ok");
+ assert!(response["configId"].is_string());
+ assert_eq!(NEW_POSITION_SCHEMA, "new-position.v1");
+}
diff --git a/apps/amm/config/networks.json b/apps/amm/config/networks.json
new file mode 100644
index 00000000..75e03eba
--- /dev/null
+++ b/apps/amm/config/networks.json
@@ -0,0 +1,18 @@
+{
+ "testnet": {
+ "checkpointHash": "0d25d71fca70d7008a892f6b3f768a4c66badbcd64e67d79ca595b92f1db544a",
+ "ammProgramId": "77eeaa23668ad2675fb768cd7ecb1893387be464b9a51f16756006c1d307db07",
+ "tokenDefinitionIds": [
+ "7b464ff9dd0d3bc07f7e2e0b0667ccd066d85ad12be4c79fc55687a863910aa6",
+ "48c81cf032e601ca367fc9816b957dbf5c0e4c11cf7008e8f4581ec1a67aab42",
+ "159caef810ea545951b3bd913efe625ee45008c80865c330e72a72ed48b61649",
+ "75f33110b185717209e3955f228d4a4448801d0ce8ba438a4a268050eeff3f44",
+ "fbd107ca4bb66bc58f59ac2d32a759be3ee0fb453f8fecd1991c11837d9660c7",
+ "5547fcb72644d95a385d313b887a96be41ff263bce6150b49fd87276839822bf",
+ "fa43e74a97d79c5f907ff3edabda5ad89bfbd3b0922572e675d4ad3c7b6029c7",
+ "4f3231d8a01e1d79f163bc27fce0c860a4a2f6890280e9d135eafbde0d68ed79",
+ "fa32f354408857006f8ea396b0419823bd04436eadb2d273d2618a46b4793ed8",
+ "00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb"
+ ]
+ }
+}
diff --git a/apps/amm/flake.lock b/apps/amm/flake.lock
index 8f79e67e..f5272d3d 100644
--- a/apps/amm/flake.lock
+++ b/apps/amm/flake.lock
@@ -1,6 +1,36 @@
{
"nodes": {
+ "amm_client": {
+ "inputs": {
+ "crane": "crane",
+ "nixpkgs": "nixpkgs"
+ },
+ "locked": {
+ "path": "../..",
+ "type": "path"
+ },
+ "original": {
+ "path": "../..",
+ "type": "path"
+ },
+ "parent": []
+ },
"crane": {
+ "locked": {
+ "lastModified": 1783203018,
+ "narHash": "sha256-G6R9IT/xwFuu+CYBWDUAok6AdC4ERC4ZfPPFtEpxnZE=",
+ "owner": "ipetkov",
+ "repo": "crane",
+ "rev": "80db5bdc391be8a1794f6d8a2d56e3a84ebcede2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ipetkov",
+ "repo": "crane",
+ "type": "github"
+ }
+ },
+ "crane_2": {
"locked": {
"lastModified": 1780532242,
"narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=",
@@ -76,7 +106,7 @@
},
"logos-blockchain-circuits": {
"inputs": {
- "nixpkgs": "nixpkgs_124"
+ "nixpkgs": "nixpkgs_125"
},
"locked": {
"lastModified": 1781004244,
@@ -2225,7 +2255,7 @@
},
"logos-execution-zone": {
"inputs": {
- "crane": "crane",
+ "crane": "crane_2",
"logos-blockchain-circuits": "logos-blockchain-circuits",
"logos-liblogos": "logos-liblogos_4",
"nixpkgs": [
@@ -2238,17 +2268,17 @@
"rust-rapidsnark": "rust-rapidsnark"
},
"locked": {
- "lastModified": 1784202021,
- "narHash": "sha256-BG94Ob5tbJyw7GclNBdnOWqcBLRld8sc+xeOU8LI5RQ=",
+ "lastModified": 1782428741,
+ "narHash": "sha256-ltLcysXUdVUXAe25Tl8x7e7ZsTzj1sHlyS3glp97TAo=",
"owner": "logos-blockchain",
"repo": "logos-execution-zone",
- "rev": "a7e06a660940a00093b1760560d37ff84aff5a05",
+ "rev": "e37876a64028a335eb693198a1ed6a0e875ec5b4",
"type": "github"
},
"original": {
"owner": "logos-blockchain",
"repo": "logos-execution-zone",
- "rev": "a7e06a660940a00093b1760560d37ff84aff5a05",
+ "rev": "e37876a64028a335eb693198a1ed6a0e875ec5b4",
"type": "github"
}
},
@@ -4763,7 +4793,7 @@
},
"logos-nix": {
"inputs": {
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1774455309,
@@ -4781,7 +4811,7 @@
},
"logos-nix_10": {
"inputs": {
- "nixpkgs": "nixpkgs_10"
+ "nixpkgs": "nixpkgs_11"
},
"locked": {
"lastModified": 1774455309,
@@ -4799,7 +4829,7 @@
},
"logos-nix_100": {
"inputs": {
- "nixpkgs": "nixpkgs_100"
+ "nixpkgs": "nixpkgs_101"
},
"locked": {
"lastModified": 1773955630,
@@ -4817,7 +4847,7 @@
},
"logos-nix_101": {
"inputs": {
- "nixpkgs": "nixpkgs_101"
+ "nixpkgs": "nixpkgs_102"
},
"locked": {
"lastModified": 1773955630,
@@ -4835,7 +4865,7 @@
},
"logos-nix_102": {
"inputs": {
- "nixpkgs": "nixpkgs_102"
+ "nixpkgs": "nixpkgs_103"
},
"locked": {
"lastModified": 1773955630,
@@ -4853,7 +4883,7 @@
},
"logos-nix_103": {
"inputs": {
- "nixpkgs": "nixpkgs_103"
+ "nixpkgs": "nixpkgs_104"
},
"locked": {
"lastModified": 1773955630,
@@ -4871,7 +4901,7 @@
},
"logos-nix_104": {
"inputs": {
- "nixpkgs": "nixpkgs_104"
+ "nixpkgs": "nixpkgs_105"
},
"locked": {
"lastModified": 1773955630,
@@ -4889,7 +4919,7 @@
},
"logos-nix_105": {
"inputs": {
- "nixpkgs": "nixpkgs_105"
+ "nixpkgs": "nixpkgs_106"
},
"locked": {
"lastModified": 1774455309,
@@ -4907,7 +4937,7 @@
},
"logos-nix_106": {
"inputs": {
- "nixpkgs": "nixpkgs_106"
+ "nixpkgs": "nixpkgs_107"
},
"locked": {
"lastModified": 1773955630,
@@ -4925,7 +4955,7 @@
},
"logos-nix_107": {
"inputs": {
- "nixpkgs": "nixpkgs_107"
+ "nixpkgs": "nixpkgs_108"
},
"locked": {
"lastModified": 1774455309,
@@ -4943,7 +4973,7 @@
},
"logos-nix_108": {
"inputs": {
- "nixpkgs": "nixpkgs_108"
+ "nixpkgs": "nixpkgs_109"
},
"locked": {
"lastModified": 1774455309,
@@ -4961,7 +4991,7 @@
},
"logos-nix_109": {
"inputs": {
- "nixpkgs": "nixpkgs_109"
+ "nixpkgs": "nixpkgs_110"
},
"locked": {
"lastModified": 1773955630,
@@ -4979,7 +5009,7 @@
},
"logos-nix_11": {
"inputs": {
- "nixpkgs": "nixpkgs_11"
+ "nixpkgs": "nixpkgs_12"
},
"locked": {
"lastModified": 1773955630,
@@ -4997,7 +5027,7 @@
},
"logos-nix_110": {
"inputs": {
- "nixpkgs": "nixpkgs_110"
+ "nixpkgs": "nixpkgs_111"
},
"locked": {
"lastModified": 1773955630,
@@ -5015,7 +5045,7 @@
},
"logos-nix_111": {
"inputs": {
- "nixpkgs": "nixpkgs_111"
+ "nixpkgs": "nixpkgs_112"
},
"locked": {
"lastModified": 1774455309,
@@ -5033,7 +5063,7 @@
},
"logos-nix_112": {
"inputs": {
- "nixpkgs": "nixpkgs_112"
+ "nixpkgs": "nixpkgs_113"
},
"locked": {
"lastModified": 1774455309,
@@ -5051,7 +5081,7 @@
},
"logos-nix_113": {
"inputs": {
- "nixpkgs": "nixpkgs_113"
+ "nixpkgs": "nixpkgs_114"
},
"locked": {
"lastModified": 1773955630,
@@ -5069,7 +5099,7 @@
},
"logos-nix_114": {
"inputs": {
- "nixpkgs": "nixpkgs_114"
+ "nixpkgs": "nixpkgs_115"
},
"locked": {
"lastModified": 1773955630,
@@ -5087,7 +5117,7 @@
},
"logos-nix_115": {
"inputs": {
- "nixpkgs": "nixpkgs_115"
+ "nixpkgs": "nixpkgs_116"
},
"locked": {
"lastModified": 1774455309,
@@ -5105,7 +5135,7 @@
},
"logos-nix_116": {
"inputs": {
- "nixpkgs": "nixpkgs_116"
+ "nixpkgs": "nixpkgs_117"
},
"locked": {
"lastModified": 1774455309,
@@ -5123,7 +5153,7 @@
},
"logos-nix_117": {
"inputs": {
- "nixpkgs": "nixpkgs_117"
+ "nixpkgs": "nixpkgs_118"
},
"locked": {
"lastModified": 1773955630,
@@ -5141,7 +5171,7 @@
},
"logos-nix_118": {
"inputs": {
- "nixpkgs": "nixpkgs_118"
+ "nixpkgs": "nixpkgs_119"
},
"locked": {
"lastModified": 1773955630,
@@ -5159,7 +5189,7 @@
},
"logos-nix_119": {
"inputs": {
- "nixpkgs": "nixpkgs_119"
+ "nixpkgs": "nixpkgs_120"
},
"locked": {
"lastModified": 1773955630,
@@ -5177,7 +5207,7 @@
},
"logos-nix_12": {
"inputs": {
- "nixpkgs": "nixpkgs_12"
+ "nixpkgs": "nixpkgs_13"
},
"locked": {
"lastModified": 1774455309,
@@ -5195,7 +5225,7 @@
},
"logos-nix_120": {
"inputs": {
- "nixpkgs": "nixpkgs_120"
+ "nixpkgs": "nixpkgs_121"
},
"locked": {
"lastModified": 1773955630,
@@ -5213,7 +5243,7 @@
},
"logos-nix_121": {
"inputs": {
- "nixpkgs": "nixpkgs_121"
+ "nixpkgs": "nixpkgs_122"
},
"locked": {
"lastModified": 1774455309,
@@ -5231,7 +5261,7 @@
},
"logos-nix_122": {
"inputs": {
- "nixpkgs": "nixpkgs_122"
+ "nixpkgs": "nixpkgs_123"
},
"locked": {
"lastModified": 1773955630,
@@ -5249,7 +5279,7 @@
},
"logos-nix_123": {
"inputs": {
- "nixpkgs": "nixpkgs_123"
+ "nixpkgs": "nixpkgs_124"
},
"locked": {
"lastModified": 1773955630,
@@ -5267,7 +5297,7 @@
},
"logos-nix_124": {
"inputs": {
- "nixpkgs": "nixpkgs_125"
+ "nixpkgs": "nixpkgs_126"
},
"locked": {
"lastModified": 1774455309,
@@ -5285,7 +5315,7 @@
},
"logos-nix_125": {
"inputs": {
- "nixpkgs": "nixpkgs_126"
+ "nixpkgs": "nixpkgs_127"
},
"locked": {
"lastModified": 1774455309,
@@ -5303,7 +5333,7 @@
},
"logos-nix_126": {
"inputs": {
- "nixpkgs": "nixpkgs_127"
+ "nixpkgs": "nixpkgs_128"
},
"locked": {
"lastModified": 1774455309,
@@ -5321,7 +5351,7 @@
},
"logos-nix_127": {
"inputs": {
- "nixpkgs": "nixpkgs_128"
+ "nixpkgs": "nixpkgs_129"
},
"locked": {
"lastModified": 1774455309,
@@ -5339,7 +5369,7 @@
},
"logos-nix_128": {
"inputs": {
- "nixpkgs": "nixpkgs_129"
+ "nixpkgs": "nixpkgs_130"
},
"locked": {
"lastModified": 1773955630,
@@ -5357,7 +5387,7 @@
},
"logos-nix_129": {
"inputs": {
- "nixpkgs": "nixpkgs_130"
+ "nixpkgs": "nixpkgs_131"
},
"locked": {
"lastModified": 1774455309,
@@ -5375,7 +5405,7 @@
},
"logos-nix_13": {
"inputs": {
- "nixpkgs": "nixpkgs_13"
+ "nixpkgs": "nixpkgs_14"
},
"locked": {
"lastModified": 1773955630,
@@ -5393,7 +5423,7 @@
},
"logos-nix_130": {
"inputs": {
- "nixpkgs": "nixpkgs_131"
+ "nixpkgs": "nixpkgs_132"
},
"locked": {
"lastModified": 1774455309,
@@ -5411,7 +5441,7 @@
},
"logos-nix_131": {
"inputs": {
- "nixpkgs": "nixpkgs_132"
+ "nixpkgs": "nixpkgs_133"
},
"locked": {
"lastModified": 1774455309,
@@ -5429,7 +5459,7 @@
},
"logos-nix_132": {
"inputs": {
- "nixpkgs": "nixpkgs_133"
+ "nixpkgs": "nixpkgs_134"
},
"locked": {
"lastModified": 1774455309,
@@ -5447,7 +5477,7 @@
},
"logos-nix_133": {
"inputs": {
- "nixpkgs": "nixpkgs_134"
+ "nixpkgs": "nixpkgs_135"
},
"locked": {
"lastModified": 1774455309,
@@ -5465,7 +5495,7 @@
},
"logos-nix_134": {
"inputs": {
- "nixpkgs": "nixpkgs_135"
+ "nixpkgs": "nixpkgs_136"
},
"locked": {
"lastModified": 1774455309,
@@ -5483,7 +5513,7 @@
},
"logos-nix_135": {
"inputs": {
- "nixpkgs": "nixpkgs_136"
+ "nixpkgs": "nixpkgs_137"
},
"locked": {
"lastModified": 1773955630,
@@ -5501,7 +5531,7 @@
},
"logos-nix_136": {
"inputs": {
- "nixpkgs": "nixpkgs_137"
+ "nixpkgs": "nixpkgs_138"
},
"locked": {
"lastModified": 1774455309,
@@ -5519,7 +5549,7 @@
},
"logos-nix_137": {
"inputs": {
- "nixpkgs": "nixpkgs_138"
+ "nixpkgs": "nixpkgs_139"
},
"locked": {
"lastModified": 1773955630,
@@ -5537,7 +5567,7 @@
},
"logos-nix_138": {
"inputs": {
- "nixpkgs": "nixpkgs_139"
+ "nixpkgs": "nixpkgs_140"
},
"locked": {
"lastModified": 1774455309,
@@ -5555,7 +5585,7 @@
},
"logos-nix_139": {
"inputs": {
- "nixpkgs": "nixpkgs_140"
+ "nixpkgs": "nixpkgs_141"
},
"locked": {
"lastModified": 1773955630,
@@ -5573,7 +5603,7 @@
},
"logos-nix_14": {
"inputs": {
- "nixpkgs": "nixpkgs_14"
+ "nixpkgs": "nixpkgs_15"
},
"locked": {
"lastModified": 1774455309,
@@ -5591,7 +5621,7 @@
},
"logos-nix_140": {
"inputs": {
- "nixpkgs": "nixpkgs_141"
+ "nixpkgs": "nixpkgs_142"
},
"locked": {
"lastModified": 1774455309,
@@ -5609,7 +5639,7 @@
},
"logos-nix_141": {
"inputs": {
- "nixpkgs": "nixpkgs_142"
+ "nixpkgs": "nixpkgs_143"
},
"locked": {
"lastModified": 1774455309,
@@ -5627,7 +5657,7 @@
},
"logos-nix_142": {
"inputs": {
- "nixpkgs": "nixpkgs_143"
+ "nixpkgs": "nixpkgs_144"
},
"locked": {
"lastModified": 1773955630,
@@ -5645,7 +5675,7 @@
},
"logos-nix_143": {
"inputs": {
- "nixpkgs": "nixpkgs_144"
+ "nixpkgs": "nixpkgs_145"
},
"locked": {
"lastModified": 1774455309,
@@ -5663,7 +5693,7 @@
},
"logos-nix_144": {
"inputs": {
- "nixpkgs": "nixpkgs_145"
+ "nixpkgs": "nixpkgs_146"
},
"locked": {
"lastModified": 1773955630,
@@ -5681,7 +5711,7 @@
},
"logos-nix_145": {
"inputs": {
- "nixpkgs": "nixpkgs_146"
+ "nixpkgs": "nixpkgs_147"
},
"locked": {
"lastModified": 1774455309,
@@ -5699,7 +5729,7 @@
},
"logos-nix_146": {
"inputs": {
- "nixpkgs": "nixpkgs_147"
+ "nixpkgs": "nixpkgs_148"
},
"locked": {
"lastModified": 1773955630,
@@ -5717,7 +5747,7 @@
},
"logos-nix_147": {
"inputs": {
- "nixpkgs": "nixpkgs_148"
+ "nixpkgs": "nixpkgs_149"
},
"locked": {
"lastModified": 1774455309,
@@ -5735,7 +5765,7 @@
},
"logos-nix_148": {
"inputs": {
- "nixpkgs": "nixpkgs_149"
+ "nixpkgs": "nixpkgs_150"
},
"locked": {
"lastModified": 1773955630,
@@ -5753,7 +5783,7 @@
},
"logos-nix_149": {
"inputs": {
- "nixpkgs": "nixpkgs_150"
+ "nixpkgs": "nixpkgs_151"
},
"locked": {
"lastModified": 1773955630,
@@ -5771,7 +5801,7 @@
},
"logos-nix_15": {
"inputs": {
- "nixpkgs": "nixpkgs_15"
+ "nixpkgs": "nixpkgs_16"
},
"locked": {
"lastModified": 1773955630,
@@ -5789,7 +5819,7 @@
},
"logos-nix_150": {
"inputs": {
- "nixpkgs": "nixpkgs_151"
+ "nixpkgs": "nixpkgs_152"
},
"locked": {
"lastModified": 1774455309,
@@ -5807,7 +5837,7 @@
},
"logos-nix_151": {
"inputs": {
- "nixpkgs": "nixpkgs_152"
+ "nixpkgs": "nixpkgs_153"
},
"locked": {
"lastModified": 1773955630,
@@ -5825,7 +5855,7 @@
},
"logos-nix_152": {
"inputs": {
- "nixpkgs": "nixpkgs_153"
+ "nixpkgs": "nixpkgs_154"
},
"locked": {
"lastModified": 1773955630,
@@ -5843,7 +5873,7 @@
},
"logos-nix_153": {
"inputs": {
- "nixpkgs": "nixpkgs_154"
+ "nixpkgs": "nixpkgs_155"
},
"locked": {
"lastModified": 1773955630,
@@ -5861,7 +5891,7 @@
},
"logos-nix_154": {
"inputs": {
- "nixpkgs": "nixpkgs_155"
+ "nixpkgs": "nixpkgs_156"
},
"locked": {
"lastModified": 1773955630,
@@ -5879,7 +5909,7 @@
},
"logos-nix_155": {
"inputs": {
- "nixpkgs": "nixpkgs_156"
+ "nixpkgs": "nixpkgs_157"
},
"locked": {
"lastModified": 1774455309,
@@ -5897,7 +5927,7 @@
},
"logos-nix_156": {
"inputs": {
- "nixpkgs": "nixpkgs_157"
+ "nixpkgs": "nixpkgs_158"
},
"locked": {
"lastModified": 1773955630,
@@ -5915,7 +5945,7 @@
},
"logos-nix_157": {
"inputs": {
- "nixpkgs": "nixpkgs_158"
+ "nixpkgs": "nixpkgs_159"
},
"locked": {
"lastModified": 1774455309,
@@ -5933,7 +5963,7 @@
},
"logos-nix_158": {
"inputs": {
- "nixpkgs": "nixpkgs_159"
+ "nixpkgs": "nixpkgs_160"
},
"locked": {
"lastModified": 1774455309,
@@ -5951,7 +5981,7 @@
},
"logos-nix_159": {
"inputs": {
- "nixpkgs": "nixpkgs_160"
+ "nixpkgs": "nixpkgs_161"
},
"locked": {
"lastModified": 1773955630,
@@ -5969,7 +5999,7 @@
},
"logos-nix_16": {
"inputs": {
- "nixpkgs": "nixpkgs_16"
+ "nixpkgs": "nixpkgs_17"
},
"locked": {
"lastModified": 1774455309,
@@ -5987,7 +6017,7 @@
},
"logos-nix_160": {
"inputs": {
- "nixpkgs": "nixpkgs_161"
+ "nixpkgs": "nixpkgs_162"
},
"locked": {
"lastModified": 1773955630,
@@ -6005,7 +6035,7 @@
},
"logos-nix_161": {
"inputs": {
- "nixpkgs": "nixpkgs_162"
+ "nixpkgs": "nixpkgs_163"
},
"locked": {
"lastModified": 1773955630,
@@ -6023,7 +6053,7 @@
},
"logos-nix_162": {
"inputs": {
- "nixpkgs": "nixpkgs_163"
+ "nixpkgs": "nixpkgs_164"
},
"locked": {
"lastModified": 1773955630,
@@ -6041,7 +6071,7 @@
},
"logos-nix_163": {
"inputs": {
- "nixpkgs": "nixpkgs_164"
+ "nixpkgs": "nixpkgs_165"
},
"locked": {
"lastModified": 1773955630,
@@ -6059,7 +6089,7 @@
},
"logos-nix_164": {
"inputs": {
- "nixpkgs": "nixpkgs_165"
+ "nixpkgs": "nixpkgs_166"
},
"locked": {
"lastModified": 1774455309,
@@ -6077,7 +6107,7 @@
},
"logos-nix_165": {
"inputs": {
- "nixpkgs": "nixpkgs_166"
+ "nixpkgs": "nixpkgs_167"
},
"locked": {
"lastModified": 1773955630,
@@ -6095,7 +6125,7 @@
},
"logos-nix_166": {
"inputs": {
- "nixpkgs": "nixpkgs_167"
+ "nixpkgs": "nixpkgs_168"
},
"locked": {
"lastModified": 1774455309,
@@ -6113,7 +6143,7 @@
},
"logos-nix_167": {
"inputs": {
- "nixpkgs": "nixpkgs_168"
+ "nixpkgs": "nixpkgs_169"
},
"locked": {
"lastModified": 1774455309,
@@ -6131,7 +6161,7 @@
},
"logos-nix_168": {
"inputs": {
- "nixpkgs": "nixpkgs_169"
+ "nixpkgs": "nixpkgs_170"
},
"locked": {
"lastModified": 1773955630,
@@ -6149,7 +6179,7 @@
},
"logos-nix_169": {
"inputs": {
- "nixpkgs": "nixpkgs_170"
+ "nixpkgs": "nixpkgs_171"
},
"locked": {
"lastModified": 1773955630,
@@ -6167,7 +6197,7 @@
},
"logos-nix_17": {
"inputs": {
- "nixpkgs": "nixpkgs_17"
+ "nixpkgs": "nixpkgs_18"
},
"locked": {
"lastModified": 1773955630,
@@ -6185,7 +6215,7 @@
},
"logos-nix_170": {
"inputs": {
- "nixpkgs": "nixpkgs_171"
+ "nixpkgs": "nixpkgs_172"
},
"locked": {
"lastModified": 1774455309,
@@ -6203,7 +6233,7 @@
},
"logos-nix_171": {
"inputs": {
- "nixpkgs": "nixpkgs_172"
+ "nixpkgs": "nixpkgs_173"
},
"locked": {
"lastModified": 1774455309,
@@ -6221,7 +6251,7 @@
},
"logos-nix_172": {
"inputs": {
- "nixpkgs": "nixpkgs_173"
+ "nixpkgs": "nixpkgs_174"
},
"locked": {
"lastModified": 1773955630,
@@ -6239,7 +6269,7 @@
},
"logos-nix_173": {
"inputs": {
- "nixpkgs": "nixpkgs_174"
+ "nixpkgs": "nixpkgs_175"
},
"locked": {
"lastModified": 1773955630,
@@ -6257,7 +6287,7 @@
},
"logos-nix_174": {
"inputs": {
- "nixpkgs": "nixpkgs_175"
+ "nixpkgs": "nixpkgs_176"
},
"locked": {
"lastModified": 1774455309,
@@ -6275,7 +6305,7 @@
},
"logos-nix_175": {
"inputs": {
- "nixpkgs": "nixpkgs_176"
+ "nixpkgs": "nixpkgs_177"
},
"locked": {
"lastModified": 1774455309,
@@ -6293,7 +6323,7 @@
},
"logos-nix_176": {
"inputs": {
- "nixpkgs": "nixpkgs_177"
+ "nixpkgs": "nixpkgs_178"
},
"locked": {
"lastModified": 1773955630,
@@ -6311,7 +6341,7 @@
},
"logos-nix_177": {
"inputs": {
- "nixpkgs": "nixpkgs_178"
+ "nixpkgs": "nixpkgs_179"
},
"locked": {
"lastModified": 1773955630,
@@ -6329,7 +6359,7 @@
},
"logos-nix_178": {
"inputs": {
- "nixpkgs": "nixpkgs_179"
+ "nixpkgs": "nixpkgs_180"
},
"locked": {
"lastModified": 1773955630,
@@ -6347,7 +6377,7 @@
},
"logos-nix_179": {
"inputs": {
- "nixpkgs": "nixpkgs_180"
+ "nixpkgs": "nixpkgs_181"
},
"locked": {
"lastModified": 1773955630,
@@ -6365,7 +6395,7 @@
},
"logos-nix_18": {
"inputs": {
- "nixpkgs": "nixpkgs_18"
+ "nixpkgs": "nixpkgs_19"
},
"locked": {
"lastModified": 1773955630,
@@ -6383,7 +6413,7 @@
},
"logos-nix_180": {
"inputs": {
- "nixpkgs": "nixpkgs_181"
+ "nixpkgs": "nixpkgs_182"
},
"locked": {
"lastModified": 1774455309,
@@ -6401,7 +6431,7 @@
},
"logos-nix_181": {
"inputs": {
- "nixpkgs": "nixpkgs_182"
+ "nixpkgs": "nixpkgs_183"
},
"locked": {
"lastModified": 1773955630,
@@ -6419,7 +6449,7 @@
},
"logos-nix_182": {
"inputs": {
- "nixpkgs": "nixpkgs_183"
+ "nixpkgs": "nixpkgs_184"
},
"locked": {
"lastModified": 1773955630,
@@ -6437,7 +6467,7 @@
},
"logos-nix_183": {
"inputs": {
- "nixpkgs": "nixpkgs_184"
+ "nixpkgs": "nixpkgs_185"
},
"locked": {
"lastModified": 1774455309,
@@ -6455,7 +6485,7 @@
},
"logos-nix_184": {
"inputs": {
- "nixpkgs": "nixpkgs_185"
+ "nixpkgs": "nixpkgs_186"
},
"locked": {
"lastModified": 1773955630,
@@ -6473,7 +6503,7 @@
},
"logos-nix_185": {
"inputs": {
- "nixpkgs": "nixpkgs_186"
+ "nixpkgs": "nixpkgs_187"
},
"locked": {
"lastModified": 1774455309,
@@ -6491,7 +6521,7 @@
},
"logos-nix_186": {
"inputs": {
- "nixpkgs": "nixpkgs_187"
+ "nixpkgs": "nixpkgs_188"
},
"locked": {
"lastModified": 1773955630,
@@ -6509,7 +6539,7 @@
},
"logos-nix_187": {
"inputs": {
- "nixpkgs": "nixpkgs_188"
+ "nixpkgs": "nixpkgs_189"
},
"locked": {
"lastModified": 1774455309,
@@ -6527,7 +6557,7 @@
},
"logos-nix_188": {
"inputs": {
- "nixpkgs": "nixpkgs_189"
+ "nixpkgs": "nixpkgs_190"
},
"locked": {
"lastModified": 1773955630,
@@ -6545,7 +6575,7 @@
},
"logos-nix_189": {
"inputs": {
- "nixpkgs": "nixpkgs_190"
+ "nixpkgs": "nixpkgs_191"
},
"locked": {
"lastModified": 1774455309,
@@ -6563,7 +6593,7 @@
},
"logos-nix_19": {
"inputs": {
- "nixpkgs": "nixpkgs_19"
+ "nixpkgs": "nixpkgs_20"
},
"locked": {
"lastModified": 1774455309,
@@ -6581,7 +6611,7 @@
},
"logos-nix_190": {
"inputs": {
- "nixpkgs": "nixpkgs_191"
+ "nixpkgs": "nixpkgs_192"
},
"locked": {
"lastModified": 1773955630,
@@ -6599,7 +6629,7 @@
},
"logos-nix_191": {
"inputs": {
- "nixpkgs": "nixpkgs_192"
+ "nixpkgs": "nixpkgs_193"
},
"locked": {
"lastModified": 1774455309,
@@ -6617,7 +6647,7 @@
},
"logos-nix_192": {
"inputs": {
- "nixpkgs": "nixpkgs_193"
+ "nixpkgs": "nixpkgs_194"
},
"locked": {
"lastModified": 1773955630,
@@ -6635,7 +6665,7 @@
},
"logos-nix_193": {
"inputs": {
- "nixpkgs": "nixpkgs_194"
+ "nixpkgs": "nixpkgs_195"
},
"locked": {
"lastModified": 1773955630,
@@ -6653,7 +6683,7 @@
},
"logos-nix_194": {
"inputs": {
- "nixpkgs": "nixpkgs_195"
+ "nixpkgs": "nixpkgs_196"
},
"locked": {
"lastModified": 1774455309,
@@ -6671,7 +6701,7 @@
},
"logos-nix_195": {
"inputs": {
- "nixpkgs": "nixpkgs_196"
+ "nixpkgs": "nixpkgs_197"
},
"locked": {
"lastModified": 1773955630,
@@ -6689,7 +6719,7 @@
},
"logos-nix_196": {
"inputs": {
- "nixpkgs": "nixpkgs_197"
+ "nixpkgs": "nixpkgs_198"
},
"locked": {
"lastModified": 1773955630,
@@ -6707,7 +6737,7 @@
},
"logos-nix_197": {
"inputs": {
- "nixpkgs": "nixpkgs_198"
+ "nixpkgs": "nixpkgs_199"
},
"locked": {
"lastModified": 1773955630,
@@ -6725,7 +6755,7 @@
},
"logos-nix_198": {
"inputs": {
- "nixpkgs": "nixpkgs_199"
+ "nixpkgs": "nixpkgs_200"
},
"locked": {
"lastModified": 1773955630,
@@ -6743,7 +6773,7 @@
},
"logos-nix_199": {
"inputs": {
- "nixpkgs": "nixpkgs_200"
+ "nixpkgs": "nixpkgs_201"
},
"locked": {
"lastModified": 1774455309,
@@ -6761,7 +6791,7 @@
},
"logos-nix_2": {
"inputs": {
- "nixpkgs": "nixpkgs_2"
+ "nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1773955630,
@@ -6779,7 +6809,7 @@
},
"logos-nix_20": {
"inputs": {
- "nixpkgs": "nixpkgs_20"
+ "nixpkgs": "nixpkgs_21"
},
"locked": {
"lastModified": 1773955630,
@@ -6797,7 +6827,7 @@
},
"logos-nix_200": {
"inputs": {
- "nixpkgs": "nixpkgs_201"
+ "nixpkgs": "nixpkgs_202"
},
"locked": {
"lastModified": 1773955630,
@@ -6815,7 +6845,7 @@
},
"logos-nix_201": {
"inputs": {
- "nixpkgs": "nixpkgs_202"
+ "nixpkgs": "nixpkgs_203"
},
"locked": {
"lastModified": 1774455309,
@@ -6833,7 +6863,7 @@
},
"logos-nix_202": {
"inputs": {
- "nixpkgs": "nixpkgs_203"
+ "nixpkgs": "nixpkgs_204"
},
"locked": {
"lastModified": 1774455309,
@@ -6851,7 +6881,7 @@
},
"logos-nix_203": {
"inputs": {
- "nixpkgs": "nixpkgs_204"
+ "nixpkgs": "nixpkgs_205"
},
"locked": {
"lastModified": 1773955630,
@@ -6869,7 +6899,7 @@
},
"logos-nix_204": {
"inputs": {
- "nixpkgs": "nixpkgs_205"
+ "nixpkgs": "nixpkgs_206"
},
"locked": {
"lastModified": 1773955630,
@@ -6887,7 +6917,7 @@
},
"logos-nix_205": {
"inputs": {
- "nixpkgs": "nixpkgs_206"
+ "nixpkgs": "nixpkgs_207"
},
"locked": {
"lastModified": 1773955630,
@@ -6905,7 +6935,7 @@
},
"logos-nix_206": {
"inputs": {
- "nixpkgs": "nixpkgs_207"
+ "nixpkgs": "nixpkgs_208"
},
"locked": {
"lastModified": 1773955630,
@@ -6923,7 +6953,7 @@
},
"logos-nix_207": {
"inputs": {
- "nixpkgs": "nixpkgs_208"
+ "nixpkgs": "nixpkgs_209"
},
"locked": {
"lastModified": 1773955630,
@@ -6941,7 +6971,7 @@
},
"logos-nix_208": {
"inputs": {
- "nixpkgs": "nixpkgs_209"
+ "nixpkgs": "nixpkgs_210"
},
"locked": {
"lastModified": 1774455309,
@@ -6959,7 +6989,7 @@
},
"logos-nix_209": {
"inputs": {
- "nixpkgs": "nixpkgs_210"
+ "nixpkgs": "nixpkgs_211"
},
"locked": {
"lastModified": 1773955630,
@@ -6977,7 +7007,7 @@
},
"logos-nix_21": {
"inputs": {
- "nixpkgs": "nixpkgs_21"
+ "nixpkgs": "nixpkgs_22"
},
"locked": {
"lastModified": 1773955630,
@@ -6995,7 +7025,7 @@
},
"logos-nix_210": {
"inputs": {
- "nixpkgs": "nixpkgs_211"
+ "nixpkgs": "nixpkgs_212"
},
"locked": {
"lastModified": 1774455309,
@@ -7013,7 +7043,7 @@
},
"logos-nix_211": {
"inputs": {
- "nixpkgs": "nixpkgs_212"
+ "nixpkgs": "nixpkgs_213"
},
"locked": {
"lastModified": 1774455309,
@@ -7031,7 +7061,7 @@
},
"logos-nix_212": {
"inputs": {
- "nixpkgs": "nixpkgs_213"
+ "nixpkgs": "nixpkgs_214"
},
"locked": {
"lastModified": 1773955630,
@@ -7049,7 +7079,7 @@
},
"logos-nix_213": {
"inputs": {
- "nixpkgs": "nixpkgs_214"
+ "nixpkgs": "nixpkgs_215"
},
"locked": {
"lastModified": 1773955630,
@@ -7067,7 +7097,7 @@
},
"logos-nix_214": {
"inputs": {
- "nixpkgs": "nixpkgs_215"
+ "nixpkgs": "nixpkgs_216"
},
"locked": {
"lastModified": 1774455309,
@@ -7085,7 +7115,7 @@
},
"logos-nix_215": {
"inputs": {
- "nixpkgs": "nixpkgs_216"
+ "nixpkgs": "nixpkgs_217"
},
"locked": {
"lastModified": 1774455309,
@@ -7103,7 +7133,7 @@
},
"logos-nix_216": {
"inputs": {
- "nixpkgs": "nixpkgs_217"
+ "nixpkgs": "nixpkgs_218"
},
"locked": {
"lastModified": 1773955630,
@@ -7121,7 +7151,7 @@
},
"logos-nix_217": {
"inputs": {
- "nixpkgs": "nixpkgs_218"
+ "nixpkgs": "nixpkgs_219"
},
"locked": {
"lastModified": 1773955630,
@@ -7139,7 +7169,7 @@
},
"logos-nix_218": {
"inputs": {
- "nixpkgs": "nixpkgs_219"
+ "nixpkgs": "nixpkgs_220"
},
"locked": {
"lastModified": 1774455309,
@@ -7157,7 +7187,7 @@
},
"logos-nix_219": {
"inputs": {
- "nixpkgs": "nixpkgs_220"
+ "nixpkgs": "nixpkgs_221"
},
"locked": {
"lastModified": 1774455309,
@@ -7175,7 +7205,7 @@
},
"logos-nix_22": {
"inputs": {
- "nixpkgs": "nixpkgs_22"
+ "nixpkgs": "nixpkgs_23"
},
"locked": {
"lastModified": 1773955630,
@@ -7193,7 +7223,7 @@
},
"logos-nix_220": {
"inputs": {
- "nixpkgs": "nixpkgs_221"
+ "nixpkgs": "nixpkgs_222"
},
"locked": {
"lastModified": 1773955630,
@@ -7211,7 +7241,7 @@
},
"logos-nix_221": {
"inputs": {
- "nixpkgs": "nixpkgs_222"
+ "nixpkgs": "nixpkgs_223"
},
"locked": {
"lastModified": 1773955630,
@@ -7229,7 +7259,7 @@
},
"logos-nix_222": {
"inputs": {
- "nixpkgs": "nixpkgs_223"
+ "nixpkgs": "nixpkgs_224"
},
"locked": {
"lastModified": 1773955630,
@@ -7247,7 +7277,7 @@
},
"logos-nix_223": {
"inputs": {
- "nixpkgs": "nixpkgs_224"
+ "nixpkgs": "nixpkgs_225"
},
"locked": {
"lastModified": 1773955630,
@@ -7265,7 +7295,7 @@
},
"logos-nix_224": {
"inputs": {
- "nixpkgs": "nixpkgs_225"
+ "nixpkgs": "nixpkgs_226"
},
"locked": {
"lastModified": 1774455309,
@@ -7283,7 +7313,7 @@
},
"logos-nix_225": {
"inputs": {
- "nixpkgs": "nixpkgs_226"
+ "nixpkgs": "nixpkgs_227"
},
"locked": {
"lastModified": 1773955630,
@@ -7301,7 +7331,7 @@
},
"logos-nix_226": {
"inputs": {
- "nixpkgs": "nixpkgs_227"
+ "nixpkgs": "nixpkgs_228"
},
"locked": {
"lastModified": 1773955630,
@@ -7319,7 +7349,7 @@
},
"logos-nix_227": {
"inputs": {
- "nixpkgs": "nixpkgs_228"
+ "nixpkgs": "nixpkgs_229"
},
"locked": {
"lastModified": 1774455309,
@@ -7337,7 +7367,7 @@
},
"logos-nix_228": {
"inputs": {
- "nixpkgs": "nixpkgs_229"
+ "nixpkgs": "nixpkgs_230"
},
"locked": {
"lastModified": 1773955630,
@@ -7355,7 +7385,7 @@
},
"logos-nix_229": {
"inputs": {
- "nixpkgs": "nixpkgs_230"
+ "nixpkgs": "nixpkgs_231"
},
"locked": {
"lastModified": 1774455309,
@@ -7373,7 +7403,7 @@
},
"logos-nix_23": {
"inputs": {
- "nixpkgs": "nixpkgs_23"
+ "nixpkgs": "nixpkgs_24"
},
"locked": {
"lastModified": 1773955630,
@@ -7391,7 +7421,7 @@
},
"logos-nix_230": {
"inputs": {
- "nixpkgs": "nixpkgs_231"
+ "nixpkgs": "nixpkgs_232"
},
"locked": {
"lastModified": 1774455309,
@@ -7409,7 +7439,7 @@
},
"logos-nix_231": {
"inputs": {
- "nixpkgs": "nixpkgs_232"
+ "nixpkgs": "nixpkgs_233"
},
"locked": {
"lastModified": 1773955630,
@@ -7427,7 +7457,7 @@
},
"logos-nix_232": {
"inputs": {
- "nixpkgs": "nixpkgs_233"
+ "nixpkgs": "nixpkgs_234"
},
"locked": {
"lastModified": 1773955630,
@@ -7445,7 +7475,7 @@
},
"logos-nix_233": {
"inputs": {
- "nixpkgs": "nixpkgs_234"
+ "nixpkgs": "nixpkgs_235"
},
"locked": {
"lastModified": 1773955630,
@@ -7463,7 +7493,7 @@
},
"logos-nix_234": {
"inputs": {
- "nixpkgs": "nixpkgs_235"
+ "nixpkgs": "nixpkgs_236"
},
"locked": {
"lastModified": 1773955630,
@@ -7481,7 +7511,7 @@
},
"logos-nix_235": {
"inputs": {
- "nixpkgs": "nixpkgs_236"
+ "nixpkgs": "nixpkgs_237"
},
"locked": {
"lastModified": 1773955630,
@@ -7499,7 +7529,7 @@
},
"logos-nix_236": {
"inputs": {
- "nixpkgs": "nixpkgs_237"
+ "nixpkgs": "nixpkgs_238"
},
"locked": {
"lastModified": 1774455309,
@@ -7517,7 +7547,7 @@
},
"logos-nix_237": {
"inputs": {
- "nixpkgs": "nixpkgs_238"
+ "nixpkgs": "nixpkgs_239"
},
"locked": {
"lastModified": 1773955630,
@@ -7535,7 +7565,7 @@
},
"logos-nix_238": {
"inputs": {
- "nixpkgs": "nixpkgs_239"
+ "nixpkgs": "nixpkgs_240"
},
"locked": {
"lastModified": 1774455309,
@@ -7553,7 +7583,7 @@
},
"logos-nix_239": {
"inputs": {
- "nixpkgs": "nixpkgs_240"
+ "nixpkgs": "nixpkgs_241"
},
"locked": {
"lastModified": 1774455309,
@@ -7571,7 +7601,7 @@
},
"logos-nix_24": {
"inputs": {
- "nixpkgs": "nixpkgs_24"
+ "nixpkgs": "nixpkgs_25"
},
"locked": {
"lastModified": 1774455309,
@@ -7589,7 +7619,7 @@
},
"logos-nix_240": {
"inputs": {
- "nixpkgs": "nixpkgs_241"
+ "nixpkgs": "nixpkgs_242"
},
"locked": {
"lastModified": 1773955630,
@@ -7607,7 +7637,7 @@
},
"logos-nix_241": {
"inputs": {
- "nixpkgs": "nixpkgs_242"
+ "nixpkgs": "nixpkgs_243"
},
"locked": {
"lastModified": 1773955630,
@@ -7625,7 +7655,7 @@
},
"logos-nix_242": {
"inputs": {
- "nixpkgs": "nixpkgs_243"
+ "nixpkgs": "nixpkgs_244"
},
"locked": {
"lastModified": 1774455309,
@@ -7643,7 +7673,7 @@
},
"logos-nix_243": {
"inputs": {
- "nixpkgs": "nixpkgs_244"
+ "nixpkgs": "nixpkgs_245"
},
"locked": {
"lastModified": 1774455309,
@@ -7661,7 +7691,7 @@
},
"logos-nix_244": {
"inputs": {
- "nixpkgs": "nixpkgs_245"
+ "nixpkgs": "nixpkgs_246"
},
"locked": {
"lastModified": 1773955630,
@@ -7679,7 +7709,7 @@
},
"logos-nix_245": {
"inputs": {
- "nixpkgs": "nixpkgs_246"
+ "nixpkgs": "nixpkgs_247"
},
"locked": {
"lastModified": 1773955630,
@@ -7697,7 +7727,7 @@
},
"logos-nix_246": {
"inputs": {
- "nixpkgs": "nixpkgs_247"
+ "nixpkgs": "nixpkgs_248"
},
"locked": {
"lastModified": 1774455309,
@@ -7715,7 +7745,7 @@
},
"logos-nix_247": {
"inputs": {
- "nixpkgs": "nixpkgs_248"
+ "nixpkgs": "nixpkgs_249"
},
"locked": {
"lastModified": 1774455309,
@@ -7733,7 +7763,7 @@
},
"logos-nix_248": {
"inputs": {
- "nixpkgs": "nixpkgs_249"
+ "nixpkgs": "nixpkgs_250"
},
"locked": {
"lastModified": 1773955630,
@@ -7751,7 +7781,7 @@
},
"logos-nix_249": {
"inputs": {
- "nixpkgs": "nixpkgs_250"
+ "nixpkgs": "nixpkgs_251"
},
"locked": {
"lastModified": 1773955630,
@@ -7769,7 +7799,7 @@
},
"logos-nix_25": {
"inputs": {
- "nixpkgs": "nixpkgs_25"
+ "nixpkgs": "nixpkgs_26"
},
"locked": {
"lastModified": 1773955630,
@@ -7787,7 +7817,7 @@
},
"logos-nix_250": {
"inputs": {
- "nixpkgs": "nixpkgs_251"
+ "nixpkgs": "nixpkgs_252"
},
"locked": {
"lastModified": 1773955630,
@@ -7805,7 +7835,7 @@
},
"logos-nix_251": {
"inputs": {
- "nixpkgs": "nixpkgs_252"
+ "nixpkgs": "nixpkgs_253"
},
"locked": {
"lastModified": 1773955630,
@@ -7823,7 +7853,7 @@
},
"logos-nix_252": {
"inputs": {
- "nixpkgs": "nixpkgs_253"
+ "nixpkgs": "nixpkgs_254"
},
"locked": {
"lastModified": 1774455309,
@@ -7841,7 +7871,7 @@
},
"logos-nix_253": {
"inputs": {
- "nixpkgs": "nixpkgs_254"
+ "nixpkgs": "nixpkgs_255"
},
"locked": {
"lastModified": 1773955630,
@@ -7859,7 +7889,7 @@
},
"logos-nix_254": {
"inputs": {
- "nixpkgs": "nixpkgs_255"
+ "nixpkgs": "nixpkgs_256"
},
"locked": {
"lastModified": 1773955630,
@@ -7877,7 +7907,7 @@
},
"logos-nix_255": {
"inputs": {
- "nixpkgs": "nixpkgs_256"
+ "nixpkgs": "nixpkgs_257"
},
"locked": {
"lastModified": 1774455309,
@@ -7895,7 +7925,7 @@
},
"logos-nix_256": {
"inputs": {
- "nixpkgs": "nixpkgs_257"
+ "nixpkgs": "nixpkgs_258"
},
"locked": {
"lastModified": 1774455309,
@@ -7913,7 +7943,7 @@
},
"logos-nix_257": {
"inputs": {
- "nixpkgs": "nixpkgs_258"
+ "nixpkgs": "nixpkgs_259"
},
"locked": {
"lastModified": 1773955630,
@@ -7931,7 +7961,7 @@
},
"logos-nix_258": {
"inputs": {
- "nixpkgs": "nixpkgs_259"
+ "nixpkgs": "nixpkgs_260"
},
"locked": {
"lastModified": 1774455309,
@@ -7949,7 +7979,7 @@
},
"logos-nix_259": {
"inputs": {
- "nixpkgs": "nixpkgs_260"
+ "nixpkgs": "nixpkgs_261"
},
"locked": {
"lastModified": 1774455309,
@@ -7967,7 +7997,7 @@
},
"logos-nix_26": {
"inputs": {
- "nixpkgs": "nixpkgs_26"
+ "nixpkgs": "nixpkgs_27"
},
"locked": {
"lastModified": 1774455309,
@@ -7985,7 +8015,7 @@
},
"logos-nix_260": {
"inputs": {
- "nixpkgs": "nixpkgs_261"
+ "nixpkgs": "nixpkgs_262"
},
"locked": {
"lastModified": 1774455309,
@@ -8003,7 +8033,7 @@
},
"logos-nix_261": {
"inputs": {
- "nixpkgs": "nixpkgs_262"
+ "nixpkgs": "nixpkgs_263"
},
"locked": {
"lastModified": 1774455309,
@@ -8021,7 +8051,7 @@
},
"logos-nix_262": {
"inputs": {
- "nixpkgs": "nixpkgs_263"
+ "nixpkgs": "nixpkgs_264"
},
"locked": {
"lastModified": 1773955630,
@@ -8039,7 +8069,7 @@
},
"logos-nix_263": {
"inputs": {
- "nixpkgs": "nixpkgs_264"
+ "nixpkgs": "nixpkgs_265"
},
"locked": {
"lastModified": 1773955630,
@@ -8057,7 +8087,7 @@
},
"logos-nix_264": {
"inputs": {
- "nixpkgs": "nixpkgs_265"
+ "nixpkgs": "nixpkgs_266"
},
"locked": {
"lastModified": 1773955630,
@@ -8075,7 +8105,7 @@
},
"logos-nix_265": {
"inputs": {
- "nixpkgs": "nixpkgs_266"
+ "nixpkgs": "nixpkgs_267"
},
"locked": {
"lastModified": 1773955630,
@@ -8093,7 +8123,7 @@
},
"logos-nix_266": {
"inputs": {
- "nixpkgs": "nixpkgs_267"
+ "nixpkgs": "nixpkgs_268"
},
"locked": {
"lastModified": 1774455309,
@@ -8111,7 +8141,7 @@
},
"logos-nix_267": {
"inputs": {
- "nixpkgs": "nixpkgs_268"
+ "nixpkgs": "nixpkgs_269"
},
"locked": {
"lastModified": 1774455309,
@@ -8129,7 +8159,7 @@
},
"logos-nix_268": {
"inputs": {
- "nixpkgs": "nixpkgs_269"
+ "nixpkgs": "nixpkgs_270"
},
"locked": {
"lastModified": 1773955630,
@@ -8147,7 +8177,7 @@
},
"logos-nix_269": {
"inputs": {
- "nixpkgs": "nixpkgs_271"
+ "nixpkgs": "nixpkgs_272"
},
"locked": {
"lastModified": 1774455309,
@@ -8165,7 +8195,7 @@
},
"logos-nix_27": {
"inputs": {
- "nixpkgs": "nixpkgs_27"
+ "nixpkgs": "nixpkgs_28"
},
"locked": {
"lastModified": 1774455309,
@@ -8183,7 +8213,7 @@
},
"logos-nix_270": {
"inputs": {
- "nixpkgs": "nixpkgs_272"
+ "nixpkgs": "nixpkgs_273"
},
"locked": {
"lastModified": 1773955630,
@@ -8201,7 +8231,7 @@
},
"logos-nix_271": {
"inputs": {
- "nixpkgs": "nixpkgs_273"
+ "nixpkgs": "nixpkgs_274"
},
"locked": {
"lastModified": 1774455309,
@@ -8219,7 +8249,7 @@
},
"logos-nix_272": {
"inputs": {
- "nixpkgs": "nixpkgs_274"
+ "nixpkgs": "nixpkgs_275"
},
"locked": {
"lastModified": 1773955630,
@@ -8237,7 +8267,7 @@
},
"logos-nix_273": {
"inputs": {
- "nixpkgs": "nixpkgs_275"
+ "nixpkgs": "nixpkgs_276"
},
"locked": {
"lastModified": 1774455309,
@@ -8255,7 +8285,7 @@
},
"logos-nix_274": {
"inputs": {
- "nixpkgs": "nixpkgs_276"
+ "nixpkgs": "nixpkgs_277"
},
"locked": {
"lastModified": 1773955630,
@@ -8273,7 +8303,7 @@
},
"logos-nix_275": {
"inputs": {
- "nixpkgs": "nixpkgs_277"
+ "nixpkgs": "nixpkgs_278"
},
"locked": {
"lastModified": 1774455309,
@@ -8291,7 +8321,7 @@
},
"logos-nix_276": {
"inputs": {
- "nixpkgs": "nixpkgs_278"
+ "nixpkgs": "nixpkgs_279"
},
"locked": {
"lastModified": 1774455309,
@@ -8309,7 +8339,7 @@
},
"logos-nix_277": {
"inputs": {
- "nixpkgs": "nixpkgs_279"
+ "nixpkgs": "nixpkgs_280"
},
"locked": {
"lastModified": 1774455309,
@@ -8327,7 +8357,7 @@
},
"logos-nix_278": {
"inputs": {
- "nixpkgs": "nixpkgs_280"
+ "nixpkgs": "nixpkgs_281"
},
"locked": {
"lastModified": 1774455309,
@@ -8345,7 +8375,7 @@
},
"logos-nix_279": {
"inputs": {
- "nixpkgs": "nixpkgs_281"
+ "nixpkgs": "nixpkgs_282"
},
"locked": {
"lastModified": 1773955630,
@@ -8363,7 +8393,7 @@
},
"logos-nix_28": {
"inputs": {
- "nixpkgs": "nixpkgs_28"
+ "nixpkgs": "nixpkgs_29"
},
"locked": {
"lastModified": 1773955630,
@@ -8381,7 +8411,7 @@
},
"logos-nix_280": {
"inputs": {
- "nixpkgs": "nixpkgs_282"
+ "nixpkgs": "nixpkgs_283"
},
"locked": {
"lastModified": 1774455309,
@@ -8399,7 +8429,7 @@
},
"logos-nix_281": {
"inputs": {
- "nixpkgs": "nixpkgs_283"
+ "nixpkgs": "nixpkgs_284"
},
"locked": {
"lastModified": 1773955630,
@@ -8417,7 +8447,7 @@
},
"logos-nix_282": {
"inputs": {
- "nixpkgs": "nixpkgs_284"
+ "nixpkgs": "nixpkgs_285"
},
"locked": {
"lastModified": 1774455309,
@@ -8435,7 +8465,7 @@
},
"logos-nix_283": {
"inputs": {
- "nixpkgs": "nixpkgs_285"
+ "nixpkgs": "nixpkgs_286"
},
"locked": {
"lastModified": 1773955630,
@@ -8453,7 +8483,7 @@
},
"logos-nix_284": {
"inputs": {
- "nixpkgs": "nixpkgs_286"
+ "nixpkgs": "nixpkgs_287"
},
"locked": {
"lastModified": 1774455309,
@@ -8471,7 +8501,7 @@
},
"logos-nix_285": {
"inputs": {
- "nixpkgs": "nixpkgs_287"
+ "nixpkgs": "nixpkgs_288"
},
"locked": {
"lastModified": 1773955630,
@@ -8489,7 +8519,7 @@
},
"logos-nix_286": {
"inputs": {
- "nixpkgs": "nixpkgs_288"
+ "nixpkgs": "nixpkgs_289"
},
"locked": {
"lastModified": 1773955630,
@@ -8507,7 +8537,7 @@
},
"logos-nix_287": {
"inputs": {
- "nixpkgs": "nixpkgs_289"
+ "nixpkgs": "nixpkgs_290"
},
"locked": {
"lastModified": 1774455309,
@@ -8525,7 +8555,7 @@
},
"logos-nix_288": {
"inputs": {
- "nixpkgs": "nixpkgs_290"
+ "nixpkgs": "nixpkgs_291"
},
"locked": {
"lastModified": 1773955630,
@@ -8543,7 +8573,7 @@
},
"logos-nix_289": {
"inputs": {
- "nixpkgs": "nixpkgs_291"
+ "nixpkgs": "nixpkgs_292"
},
"locked": {
"lastModified": 1773955630,
@@ -8561,7 +8591,7 @@
},
"logos-nix_29": {
"inputs": {
- "nixpkgs": "nixpkgs_29"
+ "nixpkgs": "nixpkgs_30"
},
"locked": {
"lastModified": 1773955630,
@@ -8579,7 +8609,7 @@
},
"logos-nix_290": {
"inputs": {
- "nixpkgs": "nixpkgs_292"
+ "nixpkgs": "nixpkgs_293"
},
"locked": {
"lastModified": 1773955630,
@@ -8597,7 +8627,7 @@
},
"logos-nix_291": {
"inputs": {
- "nixpkgs": "nixpkgs_293"
+ "nixpkgs": "nixpkgs_294"
},
"locked": {
"lastModified": 1773955630,
@@ -8615,7 +8645,7 @@
},
"logos-nix_292": {
"inputs": {
- "nixpkgs": "nixpkgs_294"
+ "nixpkgs": "nixpkgs_295"
},
"locked": {
"lastModified": 1774455309,
@@ -8633,7 +8663,7 @@
},
"logos-nix_293": {
"inputs": {
- "nixpkgs": "nixpkgs_295"
+ "nixpkgs": "nixpkgs_296"
},
"locked": {
"lastModified": 1773955630,
@@ -8651,7 +8681,7 @@
},
"logos-nix_294": {
"inputs": {
- "nixpkgs": "nixpkgs_296"
+ "nixpkgs": "nixpkgs_297"
},
"locked": {
"lastModified": 1774455309,
@@ -8669,7 +8699,7 @@
},
"logos-nix_295": {
"inputs": {
- "nixpkgs": "nixpkgs_297"
+ "nixpkgs": "nixpkgs_298"
},
"locked": {
"lastModified": 1774455309,
@@ -8687,7 +8717,7 @@
},
"logos-nix_296": {
"inputs": {
- "nixpkgs": "nixpkgs_298"
+ "nixpkgs": "nixpkgs_299"
},
"locked": {
"lastModified": 1773955630,
@@ -8705,7 +8735,7 @@
},
"logos-nix_297": {
"inputs": {
- "nixpkgs": "nixpkgs_299"
+ "nixpkgs": "nixpkgs_300"
},
"locked": {
"lastModified": 1773955630,
@@ -8723,7 +8753,7 @@
},
"logos-nix_298": {
"inputs": {
- "nixpkgs": "nixpkgs_300"
+ "nixpkgs": "nixpkgs_301"
},
"locked": {
"lastModified": 1773955630,
@@ -8741,7 +8771,7 @@
},
"logos-nix_299": {
"inputs": {
- "nixpkgs": "nixpkgs_301"
+ "nixpkgs": "nixpkgs_302"
},
"locked": {
"lastModified": 1773955630,
@@ -8759,7 +8789,7 @@
},
"logos-nix_3": {
"inputs": {
- "nixpkgs": "nixpkgs_3"
+ "nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1774455309,
@@ -8777,7 +8807,7 @@
},
"logos-nix_30": {
"inputs": {
- "nixpkgs": "nixpkgs_30"
+ "nixpkgs": "nixpkgs_31"
},
"locked": {
"lastModified": 1773955630,
@@ -8795,7 +8825,7 @@
},
"logos-nix_300": {
"inputs": {
- "nixpkgs": "nixpkgs_302"
+ "nixpkgs": "nixpkgs_303"
},
"locked": {
"lastModified": 1773955630,
@@ -8813,7 +8843,7 @@
},
"logos-nix_301": {
"inputs": {
- "nixpkgs": "nixpkgs_303"
+ "nixpkgs": "nixpkgs_304"
},
"locked": {
"lastModified": 1774455309,
@@ -8831,7 +8861,7 @@
},
"logos-nix_302": {
"inputs": {
- "nixpkgs": "nixpkgs_304"
+ "nixpkgs": "nixpkgs_305"
},
"locked": {
"lastModified": 1773955630,
@@ -8849,7 +8879,7 @@
},
"logos-nix_303": {
"inputs": {
- "nixpkgs": "nixpkgs_305"
+ "nixpkgs": "nixpkgs_306"
},
"locked": {
"lastModified": 1774455309,
@@ -8867,7 +8897,7 @@
},
"logos-nix_304": {
"inputs": {
- "nixpkgs": "nixpkgs_306"
+ "nixpkgs": "nixpkgs_307"
},
"locked": {
"lastModified": 1774455309,
@@ -8885,7 +8915,7 @@
},
"logos-nix_305": {
"inputs": {
- "nixpkgs": "nixpkgs_307"
+ "nixpkgs": "nixpkgs_308"
},
"locked": {
"lastModified": 1773955630,
@@ -8903,7 +8933,7 @@
},
"logos-nix_306": {
"inputs": {
- "nixpkgs": "nixpkgs_308"
+ "nixpkgs": "nixpkgs_309"
},
"locked": {
"lastModified": 1773955630,
@@ -8921,7 +8951,7 @@
},
"logos-nix_307": {
"inputs": {
- "nixpkgs": "nixpkgs_309"
+ "nixpkgs": "nixpkgs_310"
},
"locked": {
"lastModified": 1774455309,
@@ -8939,7 +8969,7 @@
},
"logos-nix_308": {
"inputs": {
- "nixpkgs": "nixpkgs_310"
+ "nixpkgs": "nixpkgs_311"
},
"locked": {
"lastModified": 1774455309,
@@ -8957,7 +8987,7 @@
},
"logos-nix_309": {
"inputs": {
- "nixpkgs": "nixpkgs_311"
+ "nixpkgs": "nixpkgs_312"
},
"locked": {
"lastModified": 1773955630,
@@ -8975,7 +9005,7 @@
},
"logos-nix_31": {
"inputs": {
- "nixpkgs": "nixpkgs_31"
+ "nixpkgs": "nixpkgs_32"
},
"locked": {
"lastModified": 1773955630,
@@ -8993,7 +9023,7 @@
},
"logos-nix_310": {
"inputs": {
- "nixpkgs": "nixpkgs_312"
+ "nixpkgs": "nixpkgs_313"
},
"locked": {
"lastModified": 1773955630,
@@ -9011,7 +9041,7 @@
},
"logos-nix_311": {
"inputs": {
- "nixpkgs": "nixpkgs_313"
+ "nixpkgs": "nixpkgs_314"
},
"locked": {
"lastModified": 1774455309,
@@ -9029,7 +9059,7 @@
},
"logos-nix_312": {
"inputs": {
- "nixpkgs": "nixpkgs_314"
+ "nixpkgs": "nixpkgs_315"
},
"locked": {
"lastModified": 1774455309,
@@ -9047,7 +9077,7 @@
},
"logos-nix_313": {
"inputs": {
- "nixpkgs": "nixpkgs_315"
+ "nixpkgs": "nixpkgs_316"
},
"locked": {
"lastModified": 1773955630,
@@ -9065,7 +9095,7 @@
},
"logos-nix_314": {
"inputs": {
- "nixpkgs": "nixpkgs_316"
+ "nixpkgs": "nixpkgs_317"
},
"locked": {
"lastModified": 1773955630,
@@ -9083,7 +9113,7 @@
},
"logos-nix_315": {
"inputs": {
- "nixpkgs": "nixpkgs_317"
+ "nixpkgs": "nixpkgs_318"
},
"locked": {
"lastModified": 1773955630,
@@ -9101,7 +9131,7 @@
},
"logos-nix_316": {
"inputs": {
- "nixpkgs": "nixpkgs_318"
+ "nixpkgs": "nixpkgs_319"
},
"locked": {
"lastModified": 1773955630,
@@ -9119,7 +9149,7 @@
},
"logos-nix_317": {
"inputs": {
- "nixpkgs": "nixpkgs_319"
+ "nixpkgs": "nixpkgs_320"
},
"locked": {
"lastModified": 1774455309,
@@ -9137,7 +9167,7 @@
},
"logos-nix_318": {
"inputs": {
- "nixpkgs": "nixpkgs_320"
+ "nixpkgs": "nixpkgs_321"
},
"locked": {
"lastModified": 1773955630,
@@ -9155,7 +9185,7 @@
},
"logos-nix_319": {
"inputs": {
- "nixpkgs": "nixpkgs_321"
+ "nixpkgs": "nixpkgs_322"
},
"locked": {
"lastModified": 1773955630,
@@ -9173,7 +9203,7 @@
},
"logos-nix_32": {
"inputs": {
- "nixpkgs": "nixpkgs_32"
+ "nixpkgs": "nixpkgs_33"
},
"locked": {
"lastModified": 1773955630,
@@ -9191,7 +9221,7 @@
},
"logos-nix_320": {
"inputs": {
- "nixpkgs": "nixpkgs_322"
+ "nixpkgs": "nixpkgs_323"
},
"locked": {
"lastModified": 1774455309,
@@ -9209,7 +9239,7 @@
},
"logos-nix_321": {
"inputs": {
- "nixpkgs": "nixpkgs_323"
+ "nixpkgs": "nixpkgs_324"
},
"locked": {
"lastModified": 1773955630,
@@ -9227,7 +9257,7 @@
},
"logos-nix_322": {
"inputs": {
- "nixpkgs": "nixpkgs_324"
+ "nixpkgs": "nixpkgs_325"
},
"locked": {
"lastModified": 1774455309,
@@ -9245,7 +9275,7 @@
},
"logos-nix_323": {
"inputs": {
- "nixpkgs": "nixpkgs_325"
+ "nixpkgs": "nixpkgs_326"
},
"locked": {
"lastModified": 1773955630,
@@ -9263,7 +9293,7 @@
},
"logos-nix_324": {
"inputs": {
- "nixpkgs": "nixpkgs_326"
+ "nixpkgs": "nixpkgs_327"
},
"locked": {
"lastModified": 1774455309,
@@ -9281,7 +9311,7 @@
},
"logos-nix_325": {
"inputs": {
- "nixpkgs": "nixpkgs_327"
+ "nixpkgs": "nixpkgs_328"
},
"locked": {
"lastModified": 1773955630,
@@ -9299,7 +9329,7 @@
},
"logos-nix_326": {
"inputs": {
- "nixpkgs": "nixpkgs_328"
+ "nixpkgs": "nixpkgs_329"
},
"locked": {
"lastModified": 1774455309,
@@ -9317,7 +9347,7 @@
},
"logos-nix_327": {
"inputs": {
- "nixpkgs": "nixpkgs_329"
+ "nixpkgs": "nixpkgs_330"
},
"locked": {
"lastModified": 1773955630,
@@ -9335,7 +9365,7 @@
},
"logos-nix_328": {
"inputs": {
- "nixpkgs": "nixpkgs_330"
+ "nixpkgs": "nixpkgs_331"
},
"locked": {
"lastModified": 1774455309,
@@ -9353,7 +9383,7 @@
},
"logos-nix_329": {
"inputs": {
- "nixpkgs": "nixpkgs_331"
+ "nixpkgs": "nixpkgs_332"
},
"locked": {
"lastModified": 1773955630,
@@ -9371,7 +9401,7 @@
},
"logos-nix_33": {
"inputs": {
- "nixpkgs": "nixpkgs_33"
+ "nixpkgs": "nixpkgs_34"
},
"locked": {
"lastModified": 1774455309,
@@ -9389,7 +9419,7 @@
},
"logos-nix_330": {
"inputs": {
- "nixpkgs": "nixpkgs_332"
+ "nixpkgs": "nixpkgs_333"
},
"locked": {
"lastModified": 1773955630,
@@ -9407,7 +9437,7 @@
},
"logos-nix_331": {
"inputs": {
- "nixpkgs": "nixpkgs_333"
+ "nixpkgs": "nixpkgs_334"
},
"locked": {
"lastModified": 1774455309,
@@ -9425,7 +9455,7 @@
},
"logos-nix_332": {
"inputs": {
- "nixpkgs": "nixpkgs_334"
+ "nixpkgs": "nixpkgs_335"
},
"locked": {
"lastModified": 1773955630,
@@ -9443,7 +9473,7 @@
},
"logos-nix_333": {
"inputs": {
- "nixpkgs": "nixpkgs_335"
+ "nixpkgs": "nixpkgs_336"
},
"locked": {
"lastModified": 1773955630,
@@ -9461,7 +9491,7 @@
},
"logos-nix_334": {
"inputs": {
- "nixpkgs": "nixpkgs_336"
+ "nixpkgs": "nixpkgs_337"
},
"locked": {
"lastModified": 1773955630,
@@ -9479,7 +9509,7 @@
},
"logos-nix_335": {
"inputs": {
- "nixpkgs": "nixpkgs_337"
+ "nixpkgs": "nixpkgs_338"
},
"locked": {
"lastModified": 1773955630,
@@ -9497,7 +9527,7 @@
},
"logos-nix_336": {
"inputs": {
- "nixpkgs": "nixpkgs_338"
+ "nixpkgs": "nixpkgs_339"
},
"locked": {
"lastModified": 1774455309,
@@ -9515,7 +9545,7 @@
},
"logos-nix_337": {
"inputs": {
- "nixpkgs": "nixpkgs_339"
+ "nixpkgs": "nixpkgs_340"
},
"locked": {
"lastModified": 1773955630,
@@ -9533,7 +9563,7 @@
},
"logos-nix_338": {
"inputs": {
- "nixpkgs": "nixpkgs_340"
+ "nixpkgs": "nixpkgs_341"
},
"locked": {
"lastModified": 1774455309,
@@ -9551,7 +9581,7 @@
},
"logos-nix_339": {
"inputs": {
- "nixpkgs": "nixpkgs_341"
+ "nixpkgs": "nixpkgs_342"
},
"locked": {
"lastModified": 1774455309,
@@ -9569,7 +9599,7 @@
},
"logos-nix_34": {
"inputs": {
- "nixpkgs": "nixpkgs_34"
+ "nixpkgs": "nixpkgs_35"
},
"locked": {
"lastModified": 1773955630,
@@ -9587,7 +9617,7 @@
},
"logos-nix_340": {
"inputs": {
- "nixpkgs": "nixpkgs_342"
+ "nixpkgs": "nixpkgs_343"
},
"locked": {
"lastModified": 1773955630,
@@ -9605,7 +9635,7 @@
},
"logos-nix_341": {
"inputs": {
- "nixpkgs": "nixpkgs_343"
+ "nixpkgs": "nixpkgs_344"
},
"locked": {
"lastModified": 1773955630,
@@ -9623,7 +9653,7 @@
},
"logos-nix_342": {
"inputs": {
- "nixpkgs": "nixpkgs_344"
+ "nixpkgs": "nixpkgs_345"
},
"locked": {
"lastModified": 1773955630,
@@ -9641,7 +9671,7 @@
},
"logos-nix_343": {
"inputs": {
- "nixpkgs": "nixpkgs_345"
+ "nixpkgs": "nixpkgs_346"
},
"locked": {
"lastModified": 1773955630,
@@ -9659,7 +9689,7 @@
},
"logos-nix_344": {
"inputs": {
- "nixpkgs": "nixpkgs_346"
+ "nixpkgs": "nixpkgs_347"
},
"locked": {
"lastModified": 1773955630,
@@ -9677,7 +9707,7 @@
},
"logos-nix_345": {
"inputs": {
- "nixpkgs": "nixpkgs_347"
+ "nixpkgs": "nixpkgs_348"
},
"locked": {
"lastModified": 1774455309,
@@ -9695,7 +9725,7 @@
},
"logos-nix_346": {
"inputs": {
- "nixpkgs": "nixpkgs_348"
+ "nixpkgs": "nixpkgs_349"
},
"locked": {
"lastModified": 1773955630,
@@ -9713,7 +9743,7 @@
},
"logos-nix_347": {
"inputs": {
- "nixpkgs": "nixpkgs_349"
+ "nixpkgs": "nixpkgs_350"
},
"locked": {
"lastModified": 1774455309,
@@ -9731,7 +9761,7 @@
},
"logos-nix_348": {
"inputs": {
- "nixpkgs": "nixpkgs_350"
+ "nixpkgs": "nixpkgs_351"
},
"locked": {
"lastModified": 1774455309,
@@ -9749,7 +9779,7 @@
},
"logos-nix_349": {
"inputs": {
- "nixpkgs": "nixpkgs_351"
+ "nixpkgs": "nixpkgs_352"
},
"locked": {
"lastModified": 1773955630,
@@ -9767,7 +9797,7 @@
},
"logos-nix_35": {
"inputs": {
- "nixpkgs": "nixpkgs_35"
+ "nixpkgs": "nixpkgs_36"
},
"locked": {
"lastModified": 1774455309,
@@ -9785,7 +9815,7 @@
},
"logos-nix_350": {
"inputs": {
- "nixpkgs": "nixpkgs_352"
+ "nixpkgs": "nixpkgs_353"
},
"locked": {
"lastModified": 1773955630,
@@ -9803,7 +9833,7 @@
},
"logos-nix_351": {
"inputs": {
- "nixpkgs": "nixpkgs_353"
+ "nixpkgs": "nixpkgs_354"
},
"locked": {
"lastModified": 1774455309,
@@ -9821,7 +9851,7 @@
},
"logos-nix_352": {
"inputs": {
- "nixpkgs": "nixpkgs_354"
+ "nixpkgs": "nixpkgs_355"
},
"locked": {
"lastModified": 1774455309,
@@ -9839,7 +9869,7 @@
},
"logos-nix_353": {
"inputs": {
- "nixpkgs": "nixpkgs_355"
+ "nixpkgs": "nixpkgs_356"
},
"locked": {
"lastModified": 1773955630,
@@ -9857,7 +9887,7 @@
},
"logos-nix_354": {
"inputs": {
- "nixpkgs": "nixpkgs_356"
+ "nixpkgs": "nixpkgs_357"
},
"locked": {
"lastModified": 1773955630,
@@ -9875,7 +9905,7 @@
},
"logos-nix_355": {
"inputs": {
- "nixpkgs": "nixpkgs_357"
+ "nixpkgs": "nixpkgs_358"
},
"locked": {
"lastModified": 1774455309,
@@ -9893,7 +9923,7 @@
},
"logos-nix_356": {
"inputs": {
- "nixpkgs": "nixpkgs_358"
+ "nixpkgs": "nixpkgs_359"
},
"locked": {
"lastModified": 1774455309,
@@ -9911,7 +9941,7 @@
},
"logos-nix_357": {
"inputs": {
- "nixpkgs": "nixpkgs_359"
+ "nixpkgs": "nixpkgs_360"
},
"locked": {
"lastModified": 1773955630,
@@ -9929,7 +9959,7 @@
},
"logos-nix_358": {
"inputs": {
- "nixpkgs": "nixpkgs_360"
+ "nixpkgs": "nixpkgs_361"
},
"locked": {
"lastModified": 1773955630,
@@ -9947,7 +9977,7 @@
},
"logos-nix_359": {
"inputs": {
- "nixpkgs": "nixpkgs_361"
+ "nixpkgs": "nixpkgs_362"
},
"locked": {
"lastModified": 1773955630,
@@ -9965,7 +9995,7 @@
},
"logos-nix_36": {
"inputs": {
- "nixpkgs": "nixpkgs_36"
+ "nixpkgs": "nixpkgs_37"
},
"locked": {
"lastModified": 1774455309,
@@ -9983,7 +10013,7 @@
},
"logos-nix_360": {
"inputs": {
- "nixpkgs": "nixpkgs_362"
+ "nixpkgs": "nixpkgs_363"
},
"locked": {
"lastModified": 1773955630,
@@ -10001,7 +10031,7 @@
},
"logos-nix_361": {
"inputs": {
- "nixpkgs": "nixpkgs_363"
+ "nixpkgs": "nixpkgs_364"
},
"locked": {
"lastModified": 1774455309,
@@ -10019,7 +10049,7 @@
},
"logos-nix_362": {
"inputs": {
- "nixpkgs": "nixpkgs_364"
+ "nixpkgs": "nixpkgs_365"
},
"locked": {
"lastModified": 1773955630,
@@ -10037,7 +10067,7 @@
},
"logos-nix_363": {
"inputs": {
- "nixpkgs": "nixpkgs_365"
+ "nixpkgs": "nixpkgs_366"
},
"locked": {
"lastModified": 1773955630,
@@ -10055,7 +10085,7 @@
},
"logos-nix_364": {
"inputs": {
- "nixpkgs": "nixpkgs_366"
+ "nixpkgs": "nixpkgs_367"
},
"locked": {
"lastModified": 1774455309,
@@ -10073,7 +10103,7 @@
},
"logos-nix_365": {
"inputs": {
- "nixpkgs": "nixpkgs_367"
+ "nixpkgs": "nixpkgs_368"
},
"locked": {
"lastModified": 1773955630,
@@ -10091,7 +10121,7 @@
},
"logos-nix_366": {
"inputs": {
- "nixpkgs": "nixpkgs_368"
+ "nixpkgs": "nixpkgs_369"
},
"locked": {
"lastModified": 1774455309,
@@ -10109,7 +10139,7 @@
},
"logos-nix_367": {
"inputs": {
- "nixpkgs": "nixpkgs_369"
+ "nixpkgs": "nixpkgs_370"
},
"locked": {
"lastModified": 1774455309,
@@ -10127,7 +10157,7 @@
},
"logos-nix_368": {
"inputs": {
- "nixpkgs": "nixpkgs_370"
+ "nixpkgs": "nixpkgs_371"
},
"locked": {
"lastModified": 1773955630,
@@ -10145,7 +10175,7 @@
},
"logos-nix_369": {
"inputs": {
- "nixpkgs": "nixpkgs_371"
+ "nixpkgs": "nixpkgs_372"
},
"locked": {
"lastModified": 1773955630,
@@ -10163,7 +10193,7 @@
},
"logos-nix_37": {
"inputs": {
- "nixpkgs": "nixpkgs_37"
+ "nixpkgs": "nixpkgs_38"
},
"locked": {
"lastModified": 1773955630,
@@ -10181,7 +10211,7 @@
},
"logos-nix_370": {
"inputs": {
- "nixpkgs": "nixpkgs_372"
+ "nixpkgs": "nixpkgs_373"
},
"locked": {
"lastModified": 1773955630,
@@ -10199,7 +10229,7 @@
},
"logos-nix_371": {
"inputs": {
- "nixpkgs": "nixpkgs_373"
+ "nixpkgs": "nixpkgs_374"
},
"locked": {
"lastModified": 1773955630,
@@ -10217,7 +10247,7 @@
},
"logos-nix_372": {
"inputs": {
- "nixpkgs": "nixpkgs_374"
+ "nixpkgs": "nixpkgs_375"
},
"locked": {
"lastModified": 1774455309,
@@ -10235,7 +10265,7 @@
},
"logos-nix_373": {
"inputs": {
- "nixpkgs": "nixpkgs_375"
+ "nixpkgs": "nixpkgs_376"
},
"locked": {
"lastModified": 1774455309,
@@ -10253,7 +10283,7 @@
},
"logos-nix_374": {
"inputs": {
- "nixpkgs": "nixpkgs_376"
+ "nixpkgs": "nixpkgs_377"
},
"locked": {
"lastModified": 1773955630,
@@ -10271,7 +10301,7 @@
},
"logos-nix_375": {
"inputs": {
- "nixpkgs": "nixpkgs_377"
+ "nixpkgs": "nixpkgs_378"
},
"locked": {
"lastModified": 1774455309,
@@ -10289,7 +10319,7 @@
},
"logos-nix_376": {
"inputs": {
- "nixpkgs": "nixpkgs_378"
+ "nixpkgs": "nixpkgs_379"
},
"locked": {
"lastModified": 1773955630,
@@ -10307,7 +10337,7 @@
},
"logos-nix_377": {
"inputs": {
- "nixpkgs": "nixpkgs_379"
+ "nixpkgs": "nixpkgs_380"
},
"locked": {
"lastModified": 1774455309,
@@ -10325,7 +10355,7 @@
},
"logos-nix_378": {
"inputs": {
- "nixpkgs": "nixpkgs_380"
+ "nixpkgs": "nixpkgs_381"
},
"locked": {
"lastModified": 1774455309,
@@ -10343,7 +10373,7 @@
},
"logos-nix_379": {
"inputs": {
- "nixpkgs": "nixpkgs_381"
+ "nixpkgs": "nixpkgs_382"
},
"locked": {
"lastModified": 1773955630,
@@ -10361,7 +10391,7 @@
},
"logos-nix_38": {
"inputs": {
- "nixpkgs": "nixpkgs_38"
+ "nixpkgs": "nixpkgs_39"
},
"locked": {
"lastModified": 1773955630,
@@ -10379,7 +10409,7 @@
},
"logos-nix_380": {
"inputs": {
- "nixpkgs": "nixpkgs_382"
+ "nixpkgs": "nixpkgs_383"
},
"locked": {
"lastModified": 1773955630,
@@ -10397,7 +10427,7 @@
},
"logos-nix_381": {
"inputs": {
- "nixpkgs": "nixpkgs_383"
+ "nixpkgs": "nixpkgs_384"
},
"locked": {
"lastModified": 1774455309,
@@ -10415,7 +10445,7 @@
},
"logos-nix_382": {
"inputs": {
- "nixpkgs": "nixpkgs_384"
+ "nixpkgs": "nixpkgs_385"
},
"locked": {
"lastModified": 1774455309,
@@ -10433,7 +10463,7 @@
},
"logos-nix_383": {
"inputs": {
- "nixpkgs": "nixpkgs_385"
+ "nixpkgs": "nixpkgs_386"
},
"locked": {
"lastModified": 1773955630,
@@ -10451,7 +10481,7 @@
},
"logos-nix_384": {
"inputs": {
- "nixpkgs": "nixpkgs_386"
+ "nixpkgs": "nixpkgs_387"
},
"locked": {
"lastModified": 1773955630,
@@ -10469,7 +10499,7 @@
},
"logos-nix_385": {
"inputs": {
- "nixpkgs": "nixpkgs_387"
+ "nixpkgs": "nixpkgs_388"
},
"locked": {
"lastModified": 1774455309,
@@ -10487,7 +10517,7 @@
},
"logos-nix_386": {
"inputs": {
- "nixpkgs": "nixpkgs_388"
+ "nixpkgs": "nixpkgs_389"
},
"locked": {
"lastModified": 1774455309,
@@ -10505,7 +10535,7 @@
},
"logos-nix_387": {
"inputs": {
- "nixpkgs": "nixpkgs_389"
+ "nixpkgs": "nixpkgs_390"
},
"locked": {
"lastModified": 1773955630,
@@ -10523,7 +10553,7 @@
},
"logos-nix_388": {
"inputs": {
- "nixpkgs": "nixpkgs_390"
+ "nixpkgs": "nixpkgs_391"
},
"locked": {
"lastModified": 1773955630,
@@ -10541,7 +10571,7 @@
},
"logos-nix_389": {
"inputs": {
- "nixpkgs": "nixpkgs_391"
+ "nixpkgs": "nixpkgs_392"
},
"locked": {
"lastModified": 1773955630,
@@ -10559,7 +10589,7 @@
},
"logos-nix_39": {
"inputs": {
- "nixpkgs": "nixpkgs_39"
+ "nixpkgs": "nixpkgs_40"
},
"locked": {
"lastModified": 1774455309,
@@ -10577,7 +10607,7 @@
},
"logos-nix_390": {
"inputs": {
- "nixpkgs": "nixpkgs_392"
+ "nixpkgs": "nixpkgs_393"
},
"locked": {
"lastModified": 1773955630,
@@ -10595,7 +10625,7 @@
},
"logos-nix_391": {
"inputs": {
- "nixpkgs": "nixpkgs_393"
+ "nixpkgs": "nixpkgs_394"
},
"locked": {
"lastModified": 1774455309,
@@ -10613,7 +10643,7 @@
},
"logos-nix_392": {
"inputs": {
- "nixpkgs": "nixpkgs_394"
+ "nixpkgs": "nixpkgs_395"
},
"locked": {
"lastModified": 1773955630,
@@ -10631,7 +10661,7 @@
},
"logos-nix_393": {
"inputs": {
- "nixpkgs": "nixpkgs_395"
+ "nixpkgs": "nixpkgs_396"
},
"locked": {
"lastModified": 1773955630,
@@ -10649,7 +10679,7 @@
},
"logos-nix_394": {
"inputs": {
- "nixpkgs": "nixpkgs_396"
+ "nixpkgs": "nixpkgs_397"
},
"locked": {
"lastModified": 1774455309,
@@ -10667,7 +10697,7 @@
},
"logos-nix_395": {
"inputs": {
- "nixpkgs": "nixpkgs_397"
+ "nixpkgs": "nixpkgs_398"
},
"locked": {
"lastModified": 1773955630,
@@ -10685,7 +10715,7 @@
},
"logos-nix_396": {
"inputs": {
- "nixpkgs": "nixpkgs_398"
+ "nixpkgs": "nixpkgs_399"
},
"locked": {
"lastModified": 1773955630,
@@ -10703,7 +10733,7 @@
},
"logos-nix_4": {
"inputs": {
- "nixpkgs": "nixpkgs_4"
+ "nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1773955630,
@@ -10721,7 +10751,7 @@
},
"logos-nix_40": {
"inputs": {
- "nixpkgs": "nixpkgs_40"
+ "nixpkgs": "nixpkgs_41"
},
"locked": {
"lastModified": 1774455309,
@@ -10739,7 +10769,7 @@
},
"logos-nix_41": {
"inputs": {
- "nixpkgs": "nixpkgs_41"
+ "nixpkgs": "nixpkgs_42"
},
"locked": {
"lastModified": 1773955630,
@@ -10757,7 +10787,7 @@
},
"logos-nix_42": {
"inputs": {
- "nixpkgs": "nixpkgs_42"
+ "nixpkgs": "nixpkgs_43"
},
"locked": {
"lastModified": 1773955630,
@@ -10775,7 +10805,7 @@
},
"logos-nix_43": {
"inputs": {
- "nixpkgs": "nixpkgs_43"
+ "nixpkgs": "nixpkgs_44"
},
"locked": {
"lastModified": 1774455309,
@@ -10793,7 +10823,7 @@
},
"logos-nix_44": {
"inputs": {
- "nixpkgs": "nixpkgs_44"
+ "nixpkgs": "nixpkgs_45"
},
"locked": {
"lastModified": 1774455309,
@@ -10811,7 +10841,7 @@
},
"logos-nix_45": {
"inputs": {
- "nixpkgs": "nixpkgs_45"
+ "nixpkgs": "nixpkgs_46"
},
"locked": {
"lastModified": 1773955630,
@@ -10829,7 +10859,7 @@
},
"logos-nix_46": {
"inputs": {
- "nixpkgs": "nixpkgs_46"
+ "nixpkgs": "nixpkgs_47"
},
"locked": {
"lastModified": 1773955630,
@@ -10847,7 +10877,7 @@
},
"logos-nix_47": {
"inputs": {
- "nixpkgs": "nixpkgs_47"
+ "nixpkgs": "nixpkgs_48"
},
"locked": {
"lastModified": 1773955630,
@@ -10865,7 +10895,7 @@
},
"logos-nix_48": {
"inputs": {
- "nixpkgs": "nixpkgs_48"
+ "nixpkgs": "nixpkgs_49"
},
"locked": {
"lastModified": 1773955630,
@@ -10883,7 +10913,7 @@
},
"logos-nix_49": {
"inputs": {
- "nixpkgs": "nixpkgs_49"
+ "nixpkgs": "nixpkgs_50"
},
"locked": {
"lastModified": 1774455309,
@@ -10901,7 +10931,7 @@
},
"logos-nix_5": {
"inputs": {
- "nixpkgs": "nixpkgs_5"
+ "nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1774455309,
@@ -10919,7 +10949,7 @@
},
"logos-nix_50": {
"inputs": {
- "nixpkgs": "nixpkgs_50"
+ "nixpkgs": "nixpkgs_51"
},
"locked": {
"lastModified": 1773955630,
@@ -10937,7 +10967,7 @@
},
"logos-nix_51": {
"inputs": {
- "nixpkgs": "nixpkgs_51"
+ "nixpkgs": "nixpkgs_52"
},
"locked": {
"lastModified": 1773955630,
@@ -10955,7 +10985,7 @@
},
"logos-nix_52": {
"inputs": {
- "nixpkgs": "nixpkgs_52"
+ "nixpkgs": "nixpkgs_53"
},
"locked": {
"lastModified": 1774455309,
@@ -10973,7 +11003,7 @@
},
"logos-nix_53": {
"inputs": {
- "nixpkgs": "nixpkgs_53"
+ "nixpkgs": "nixpkgs_54"
},
"locked": {
"lastModified": 1773955630,
@@ -10991,7 +11021,7 @@
},
"logos-nix_54": {
"inputs": {
- "nixpkgs": "nixpkgs_54"
+ "nixpkgs": "nixpkgs_55"
},
"locked": {
"lastModified": 1774455309,
@@ -11009,7 +11039,7 @@
},
"logos-nix_55": {
"inputs": {
- "nixpkgs": "nixpkgs_55"
+ "nixpkgs": "nixpkgs_56"
},
"locked": {
"lastModified": 1773955630,
@@ -11027,7 +11057,7 @@
},
"logos-nix_56": {
"inputs": {
- "nixpkgs": "nixpkgs_56"
+ "nixpkgs": "nixpkgs_57"
},
"locked": {
"lastModified": 1774455309,
@@ -11045,7 +11075,7 @@
},
"logos-nix_57": {
"inputs": {
- "nixpkgs": "nixpkgs_57"
+ "nixpkgs": "nixpkgs_58"
},
"locked": {
"lastModified": 1773955630,
@@ -11063,7 +11093,7 @@
},
"logos-nix_58": {
"inputs": {
- "nixpkgs": "nixpkgs_58"
+ "nixpkgs": "nixpkgs_59"
},
"locked": {
"lastModified": 1774455309,
@@ -11081,7 +11111,7 @@
},
"logos-nix_59": {
"inputs": {
- "nixpkgs": "nixpkgs_59"
+ "nixpkgs": "nixpkgs_60"
},
"locked": {
"lastModified": 1773955630,
@@ -11099,7 +11129,7 @@
},
"logos-nix_6": {
"inputs": {
- "nixpkgs": "nixpkgs_6"
+ "nixpkgs": "nixpkgs_7"
},
"locked": {
"lastModified": 1773955630,
@@ -11117,7 +11147,7 @@
},
"logos-nix_60": {
"inputs": {
- "nixpkgs": "nixpkgs_60"
+ "nixpkgs": "nixpkgs_61"
},
"locked": {
"lastModified": 1774455309,
@@ -11135,7 +11165,7 @@
},
"logos-nix_61": {
"inputs": {
- "nixpkgs": "nixpkgs_61"
+ "nixpkgs": "nixpkgs_62"
},
"locked": {
"lastModified": 1773955630,
@@ -11153,7 +11183,7 @@
},
"logos-nix_62": {
"inputs": {
- "nixpkgs": "nixpkgs_62"
+ "nixpkgs": "nixpkgs_63"
},
"locked": {
"lastModified": 1773955630,
@@ -11171,7 +11201,7 @@
},
"logos-nix_63": {
"inputs": {
- "nixpkgs": "nixpkgs_63"
+ "nixpkgs": "nixpkgs_64"
},
"locked": {
"lastModified": 1774455309,
@@ -11189,7 +11219,7 @@
},
"logos-nix_64": {
"inputs": {
- "nixpkgs": "nixpkgs_64"
+ "nixpkgs": "nixpkgs_65"
},
"locked": {
"lastModified": 1773955630,
@@ -11207,7 +11237,7 @@
},
"logos-nix_65": {
"inputs": {
- "nixpkgs": "nixpkgs_65"
+ "nixpkgs": "nixpkgs_66"
},
"locked": {
"lastModified": 1773955630,
@@ -11225,7 +11255,7 @@
},
"logos-nix_66": {
"inputs": {
- "nixpkgs": "nixpkgs_66"
+ "nixpkgs": "nixpkgs_67"
},
"locked": {
"lastModified": 1773955630,
@@ -11243,7 +11273,7 @@
},
"logos-nix_67": {
"inputs": {
- "nixpkgs": "nixpkgs_67"
+ "nixpkgs": "nixpkgs_68"
},
"locked": {
"lastModified": 1773955630,
@@ -11261,7 +11291,7 @@
},
"logos-nix_68": {
"inputs": {
- "nixpkgs": "nixpkgs_68"
+ "nixpkgs": "nixpkgs_69"
},
"locked": {
"lastModified": 1774455309,
@@ -11279,7 +11309,7 @@
},
"logos-nix_69": {
"inputs": {
- "nixpkgs": "nixpkgs_69"
+ "nixpkgs": "nixpkgs_70"
},
"locked": {
"lastModified": 1773955630,
@@ -11297,7 +11327,7 @@
},
"logos-nix_7": {
"inputs": {
- "nixpkgs": "nixpkgs_7"
+ "nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1774455309,
@@ -11315,7 +11345,7 @@
},
"logos-nix_70": {
"inputs": {
- "nixpkgs": "nixpkgs_70"
+ "nixpkgs": "nixpkgs_71"
},
"locked": {
"lastModified": 1774455309,
@@ -11333,7 +11363,7 @@
},
"logos-nix_71": {
"inputs": {
- "nixpkgs": "nixpkgs_71"
+ "nixpkgs": "nixpkgs_72"
},
"locked": {
"lastModified": 1774455309,
@@ -11351,7 +11381,7 @@
},
"logos-nix_72": {
"inputs": {
- "nixpkgs": "nixpkgs_72"
+ "nixpkgs": "nixpkgs_73"
},
"locked": {
"lastModified": 1773955630,
@@ -11369,7 +11399,7 @@
},
"logos-nix_73": {
"inputs": {
- "nixpkgs": "nixpkgs_73"
+ "nixpkgs": "nixpkgs_74"
},
"locked": {
"lastModified": 1773955630,
@@ -11387,7 +11417,7 @@
},
"logos-nix_74": {
"inputs": {
- "nixpkgs": "nixpkgs_74"
+ "nixpkgs": "nixpkgs_75"
},
"locked": {
"lastModified": 1773955630,
@@ -11405,7 +11435,7 @@
},
"logos-nix_75": {
"inputs": {
- "nixpkgs": "nixpkgs_75"
+ "nixpkgs": "nixpkgs_76"
},
"locked": {
"lastModified": 1773955630,
@@ -11423,7 +11453,7 @@
},
"logos-nix_76": {
"inputs": {
- "nixpkgs": "nixpkgs_76"
+ "nixpkgs": "nixpkgs_77"
},
"locked": {
"lastModified": 1773955630,
@@ -11441,7 +11471,7 @@
},
"logos-nix_77": {
"inputs": {
- "nixpkgs": "nixpkgs_77"
+ "nixpkgs": "nixpkgs_78"
},
"locked": {
"lastModified": 1774455309,
@@ -11459,7 +11489,7 @@
},
"logos-nix_78": {
"inputs": {
- "nixpkgs": "nixpkgs_78"
+ "nixpkgs": "nixpkgs_79"
},
"locked": {
"lastModified": 1773955630,
@@ -11477,7 +11507,7 @@
},
"logos-nix_79": {
"inputs": {
- "nixpkgs": "nixpkgs_79"
+ "nixpkgs": "nixpkgs_80"
},
"locked": {
"lastModified": 1774455309,
@@ -11495,7 +11525,7 @@
},
"logos-nix_8": {
"inputs": {
- "nixpkgs": "nixpkgs_8"
+ "nixpkgs": "nixpkgs_9"
},
"locked": {
"lastModified": 1774455309,
@@ -11513,7 +11543,7 @@
},
"logos-nix_80": {
"inputs": {
- "nixpkgs": "nixpkgs_80"
+ "nixpkgs": "nixpkgs_81"
},
"locked": {
"lastModified": 1774455309,
@@ -11531,7 +11561,7 @@
},
"logos-nix_81": {
"inputs": {
- "nixpkgs": "nixpkgs_81"
+ "nixpkgs": "nixpkgs_82"
},
"locked": {
"lastModified": 1773955630,
@@ -11549,7 +11579,7 @@
},
"logos-nix_82": {
"inputs": {
- "nixpkgs": "nixpkgs_82"
+ "nixpkgs": "nixpkgs_83"
},
"locked": {
"lastModified": 1773955630,
@@ -11567,7 +11597,7 @@
},
"logos-nix_83": {
"inputs": {
- "nixpkgs": "nixpkgs_83"
+ "nixpkgs": "nixpkgs_84"
},
"locked": {
"lastModified": 1774455309,
@@ -11585,7 +11615,7 @@
},
"logos-nix_84": {
"inputs": {
- "nixpkgs": "nixpkgs_84"
+ "nixpkgs": "nixpkgs_85"
},
"locked": {
"lastModified": 1774455309,
@@ -11603,7 +11633,7 @@
},
"logos-nix_85": {
"inputs": {
- "nixpkgs": "nixpkgs_85"
+ "nixpkgs": "nixpkgs_86"
},
"locked": {
"lastModified": 1773955630,
@@ -11621,7 +11651,7 @@
},
"logos-nix_86": {
"inputs": {
- "nixpkgs": "nixpkgs_86"
+ "nixpkgs": "nixpkgs_87"
},
"locked": {
"lastModified": 1773955630,
@@ -11639,7 +11669,7 @@
},
"logos-nix_87": {
"inputs": {
- "nixpkgs": "nixpkgs_87"
+ "nixpkgs": "nixpkgs_88"
},
"locked": {
"lastModified": 1774455309,
@@ -11657,7 +11687,7 @@
},
"logos-nix_88": {
"inputs": {
- "nixpkgs": "nixpkgs_88"
+ "nixpkgs": "nixpkgs_89"
},
"locked": {
"lastModified": 1774455309,
@@ -11675,7 +11705,7 @@
},
"logos-nix_89": {
"inputs": {
- "nixpkgs": "nixpkgs_89"
+ "nixpkgs": "nixpkgs_90"
},
"locked": {
"lastModified": 1773955630,
@@ -11693,7 +11723,7 @@
},
"logos-nix_9": {
"inputs": {
- "nixpkgs": "nixpkgs_9"
+ "nixpkgs": "nixpkgs_10"
},
"locked": {
"lastModified": 1774455309,
@@ -11711,7 +11741,7 @@
},
"logos-nix_90": {
"inputs": {
- "nixpkgs": "nixpkgs_90"
+ "nixpkgs": "nixpkgs_91"
},
"locked": {
"lastModified": 1773955630,
@@ -11729,7 +11759,7 @@
},
"logos-nix_91": {
"inputs": {
- "nixpkgs": "nixpkgs_91"
+ "nixpkgs": "nixpkgs_92"
},
"locked": {
"lastModified": 1773955630,
@@ -11747,7 +11777,7 @@
},
"logos-nix_92": {
"inputs": {
- "nixpkgs": "nixpkgs_92"
+ "nixpkgs": "nixpkgs_93"
},
"locked": {
"lastModified": 1773955630,
@@ -11765,7 +11795,7 @@
},
"logos-nix_93": {
"inputs": {
- "nixpkgs": "nixpkgs_93"
+ "nixpkgs": "nixpkgs_94"
},
"locked": {
"lastModified": 1774455309,
@@ -11783,7 +11813,7 @@
},
"logos-nix_94": {
"inputs": {
- "nixpkgs": "nixpkgs_94"
+ "nixpkgs": "nixpkgs_95"
},
"locked": {
"lastModified": 1773955630,
@@ -11801,7 +11831,7 @@
},
"logos-nix_95": {
"inputs": {
- "nixpkgs": "nixpkgs_95"
+ "nixpkgs": "nixpkgs_96"
},
"locked": {
"lastModified": 1773955630,
@@ -11819,7 +11849,7 @@
},
"logos-nix_96": {
"inputs": {
- "nixpkgs": "nixpkgs_96"
+ "nixpkgs": "nixpkgs_97"
},
"locked": {
"lastModified": 1774455309,
@@ -11837,7 +11867,7 @@
},
"logos-nix_97": {
"inputs": {
- "nixpkgs": "nixpkgs_97"
+ "nixpkgs": "nixpkgs_98"
},
"locked": {
"lastModified": 1773955630,
@@ -11855,7 +11885,7 @@
},
"logos-nix_98": {
"inputs": {
- "nixpkgs": "nixpkgs_98"
+ "nixpkgs": "nixpkgs_99"
},
"locked": {
"lastModified": 1774455309,
@@ -11873,7 +11903,7 @@
},
"logos-nix_99": {
"inputs": {
- "nixpkgs": "nixpkgs_99"
+ "nixpkgs": "nixpkgs_100"
},
"locked": {
"lastModified": 1774455309,
@@ -20098,11 +20128,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1759036355,
- "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
+ "lastModified": 1783776592,
+ "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
+ "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
"type": "github"
},
"original": {
@@ -20546,32 +20576,32 @@
},
"nixpkgs_124": {
"locked": {
- "lastModified": 1767313136,
- "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
+ "lastModified": 1759036355,
+ "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
+ "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-25.05",
+ "ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_125": {
"locked": {
- "lastModified": 1759036355,
- "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
+ "lastModified": 1767313136,
+ "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
+ "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-unstable",
+ "ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
@@ -23138,32 +23168,32 @@
},
"nixpkgs_270": {
"locked": {
- "lastModified": 1767313136,
- "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
+ "lastModified": 1759036355,
+ "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
+ "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-25.05",
+ "ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_271": {
"locked": {
- "lastModified": 1759036355,
- "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
+ "lastModified": 1767313136,
+ "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
+ "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-unstable",
+ "ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
@@ -25408,6 +25438,22 @@
"type": "github"
}
},
+ "nixpkgs_399": {
+ "locked": {
+ "lastModified": 1759036355,
+ "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"nixpkgs_4": {
"locked": {
"lastModified": 1759036355,
@@ -26762,6 +26808,7 @@
},
"root": {
"inputs": {
+ "amm_client": "amm_client",
"logos-module-builder": "logos-module-builder",
"logos_execution_zone": "logos_execution_zone",
"shared_wallet": "shared_wallet"
@@ -26834,7 +26881,7 @@
},
"rust-rapidsnark": {
"inputs": {
- "nixpkgs": "nixpkgs_270"
+ "nixpkgs": "nixpkgs_271"
},
"locked": {
"lastModified": 1781090841,
diff --git a/apps/amm/flake.nix b/apps/amm/flake.nix
index 44121c52..5a2990dc 100644
--- a/apps/amm/flake.nix
+++ b/apps/amm/flake.nix
@@ -26,6 +26,8 @@
inputs.logos-execution-zone.url =
"github:logos-blockchain/logos-execution-zone?rev=a7e06a660940a00093b1760560d37ff84aff5a05";
};
+
+ amm_client.url = "path:../..";
};
# NOTE: this flake is no longer built standalone. The amm_client_ffi crate
@@ -47,6 +49,12 @@
preConfigure = ''
cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${shared_wallet}")
'';
+ externalLibInputs = {
+ amm_client = {
+ input = inputs.amm_client;
+ packages.default = "amm_client";
+ };
+ };
postInstall = ''
# The builder installs the view under lib/qml after this hook. Its
# import descriptor points back to this compiled shared QML module.
diff --git a/apps/amm/metadata.json b/apps/amm/metadata.json
index e5e84dca..bc2cce38 100644
--- a/apps/amm/metadata.json
+++ b/apps/amm/metadata.json
@@ -11,11 +11,12 @@
"nix": {
"packages": {
- "build": [],
- "runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp"]
+ "build": ["pkg-config"],
+ "runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"]
},
"external_libraries": [
- { "name": "amm_client_ffi" }
+ { "name": "amm_client_ffi" },
+ { "name": "amm_client" }
],
"cmake": {
"find_packages": [],
diff --git a/apps/amm/qml/Main.qml b/apps/amm/qml/Main.qml
index 1703e33d..b7000439 100644
--- a/apps/amm/qml/Main.qml
+++ b/apps/amm/qml/Main.qml
@@ -45,7 +45,7 @@ Item {
height: show ? 32 : 0
visible: height > 0
clip: true
- color: Theme.palette.error
+ color: Theme.palette.warning
Behavior on height { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
@@ -56,7 +56,7 @@ Item {
elide: Text.ElideMiddle
font.pixelSize: 12
font.weight: Font.Medium
- color: Theme.palette.text
+ color: Theme.palette.background
text: qsTr("Unable to connect to network")
}
}
@@ -88,6 +88,8 @@ Item {
LiquidityPage {
anchors.fill: parent
+ backend: root.ready ? root.backend : null
+ runtime: logos
visible: navbar.currentIndex === 1
}
}
diff --git a/apps/amm/qml/components/liquidity/AddLiquidityForm.qml b/apps/amm/qml/components/liquidity/AddLiquidityForm.qml
deleted file mode 100644
index db8c59b3..00000000
--- a/apps/amm/qml/components/liquidity/AddLiquidityForm.qml
+++ /dev/null
@@ -1,222 +0,0 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
-import "../shared"
-import "../../state"
-
-Rectangle {
- id: root
-
- required property DummyPoolState poolState
-
- property real slippageTolerancePercent: 0.5
- property string amountA: ""
- property string amountB: ""
- property string lastEditedToken: "A"
- readonly property real parsedA: root.poolState.parseAmount(root.amountA)
- readonly property real parsedB: root.poolState.parseAmount(root.amountB)
- readonly property var preview: root.poolState.addLiquidityPreview(root.parsedA, root.parsedB)
- readonly property int minLpReceived: root.poolState.minReceivedAmount(root.preview.deltaLp, root.slippageTolerancePercent)
- readonly property bool hasAnyAmount: root.parsedA > 0 || root.parsedB > 0
- readonly property bool amountAOverBalance: root.parsedA > root.poolState.walletBalanceA
- readonly property bool amountBOverBalance: root.parsedB > root.poolState.walletBalanceB
- readonly property bool minReceivedIsZero: root.hasAnyAmount && root.minLpReceived === 0
- readonly property bool zeroTokenDeposit: root.hasAnyAmount && (root.preview.actualA === 0 || root.preview.actualB === 0)
- readonly property bool zeroLpDeposit: root.preview.actualA > 0 && root.preview.actualB > 0 && root.preview.deltaLp === 0
- readonly property bool 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 warningText: root.zeroTokenDeposit ? qsTr("Deposit would be rejected because one token amount rounds to zero") : root.zeroLpDeposit ? qsTr("Deposit would mint 0 LP tokens") : ""
-
- signal slippageToleranceChangeRequested(real tolerancePercent)
- signal addLiquidityRequested(var snapshot)
-
- color: "#00000000"
- implicitHeight: content.implicitHeight
- radius: 0
- border.width: 0
-
- ColumnLayout {
- id: content
-
- anchors.fill: parent
- spacing: 10
-
- TokenAmountInput {
- balance: root.poolState.formatTokenAmount(root.poolState.walletBalanceA, root.poolState.tokenA)
- errorText: root.amountAOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenA) : ""
- helperText: root.lastEditedToken === "B" && root.amountA.length > 0 ? qsTr("Calculated from current pool ratio") : ""
- label: qsTr("Token A amount")
- token: root.poolState.tokenA
- text: root.amountA
-
- Layout.fillWidth: true
-
- onEditingChanged: function (value) {
- root.updateFromTokenA(value);
- }
- onMaxClicked: root.useMax("A")
- }
-
- TokenAmountInput {
- balance: root.poolState.formatTokenAmount(root.poolState.walletBalanceB, root.poolState.tokenB)
- errorText: root.amountBOverBalance ? qsTr("Insufficient %1 balance").arg(root.poolState.tokenB) : ""
- helperText: root.lastEditedToken === "A" && root.amountB.length > 0 ? qsTr("Calculated from current pool ratio") : ""
- label: qsTr("Token B amount")
- token: root.poolState.tokenB
- text: root.amountB
-
- Layout.fillWidth: true
-
- onEditingChanged: function (value) {
- root.updateFromTokenB(value);
- }
- onMaxClicked: root.useMax("B")
- }
-
- SummaryRow {
- label: qsTr("Current price")
- value: qsTr("1 %1 = %2 %3").arg(root.poolState.tokenB).arg(root.poolState.formatInteger(root.poolState.tokenAPerTokenB)).arg(root.poolState.tokenA)
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- estimated: true
- estimateHelp: qsTr("Estimated with the same integer floor math used by the add-liquidity contract path.")
- label: qsTr("Estimated LP tokens")
- value: root.poolState.formatLpAmount(root.preview.deltaLp)
- visible: root.hasAnyAmount
-
- Layout.fillWidth: true
- }
-
- SlippageToleranceControl {
- tolerancePercent: root.slippageTolerancePercent
-
- Layout.fillWidth: true
-
- onToleranceChangeRequested: function (tolerancePercent) {
- root.slippageToleranceChangeRequested(tolerancePercent);
- }
- }
-
- SummaryRow {
- label: qsTr("Min LP received")
- value: root.poolState.formatLpAmount(root.minLpReceived)
- visible: root.hasAnyAmount
-
- Layout.fillWidth: true
- }
-
- Text {
- color: "#F08A76"
- font.pixelSize: 12
- lineHeight: 1.25
- text: qsTr("Minimum received is 0. Increase amount or lower slippage.")
- visible: root.minReceivedIsZero
- wrapMode: Text.WordWrap
-
- Layout.fillWidth: true
- }
-
- Text {
- color: "#F08A76"
- font.pixelSize: 12
- lineHeight: 1.25
- text: root.warningText
- visible: root.warningText.length > 0
- wrapMode: Text.WordWrap
-
- Layout.fillWidth: true
- }
-
- Button {
- id: submitButton
-
- activeFocusOnTab: true
- enabled: root.canSubmit
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: root.submitButtonText
-
- Accessible.name: submitButton.text
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
- Layout.preferredHeight: 44
-
- onClicked: root.addLiquidityRequested(root.submitSnapshot())
-
- contentItem: Text {
- color: submitButton.enabled ? "#151515" : "#7D756E"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 13
- horizontalAlignment: Text.AlignHCenter
- text: submitButton.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: submitButton.enabled ? "#F26A21" : "#343434"
- border.width: 1
- color: submitButton.enabled ? submitButton.pressed ? "#D95C1E" : submitButton.hovered || submitButton.activeFocus ? "#FF8A3D" : "#F26A21" : "#181818"
- radius: 6
- }
- }
- }
-
- function setAmounts(nextA, nextB, intentToken, showZero) {
- root.lastEditedToken = intentToken;
- root.amountA = nextA > 0 || showZero ? root.poolState.formatInputAmount(nextA) : "";
- root.amountB = nextB > 0 || showZero ? root.poolState.formatInputAmount(nextB) : "";
- }
-
- function updateFromTokenA(value) {
- if (value.length === 0) {
- setAmounts(0, 0, "A", false);
- return;
- }
-
- const nextA = root.poolState.parseAmount(value);
- setAmounts(nextA, root.poolState.amountBForA(nextA), "A", true);
- }
-
- function updateFromTokenB(value) {
- if (value.length === 0) {
- setAmounts(0, 0, "B", false);
- return;
- }
-
- const nextB = root.poolState.parseAmount(value);
- setAmounts(root.poolState.amountAForB(nextB), nextB, "B", true);
- }
-
- function useMax(intentToken) {
- const capped = root.poolState.maxAddLiquidityForBalances();
- setAmounts(capped.actualA, capped.actualB, intentToken, false);
- }
-
- function resetForm() {
- root.amountA = "";
- root.amountB = "";
- root.lastEditedToken = "A";
- }
-
- function submitSnapshot() {
- return {
- "action": "add",
- "actualA": root.preview.actualA,
- "actualB": 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),
- "slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
- "tokenA": root.poolState.tokenA,
- "tokenB": root.poolState.tokenB
- };
- }
-}
diff --git a/apps/amm/qml/components/liquidity/AmmActionCard.qml b/apps/amm/qml/components/liquidity/AmmActionCard.qml
new file mode 100644
index 00000000..d8f3151d
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmmActionCard.qml
@@ -0,0 +1,15 @@
+import QtQuick
+
+Rectangle {
+ required property var theme
+
+ implicitWidth: 480
+ radius: 24
+ color: theme.colors.cardBg
+ border.color: theme.colors.border
+ border.width: 1
+
+ Behavior on color {
+ ColorAnimation { duration: 180 }
+ }
+}
diff --git a/apps/amm/qml/components/liquidity/AmmPairSeparator.qml b/apps/amm/qml/components/liquidity/AmmPairSeparator.qml
new file mode 100644
index 00000000..8c578cac
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmmPairSeparator.qml
@@ -0,0 +1,61 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ id: root
+
+ required property var theme
+ property string symbol: "\u2193"
+ property string accessibleName: qsTr("Swap token order")
+
+ signal clicked
+
+ implicitHeight: 40
+
+ Rectangle {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 1
+ color: root.theme.colors.divider
+ }
+
+ Button {
+ id: button
+
+ anchors.centerIn: parent
+ width: 36
+ height: 36
+ hoverEnabled: true
+ enabled: root.enabled
+
+ Accessible.name: root.accessibleName
+ ToolTip.visible: hovered
+ ToolTip.text: Accessible.name
+
+ onClicked: root.clicked()
+
+ contentItem: Text {
+ text: root.symbol
+ color: button.enabled
+ ? root.theme.colors.textPrimary
+ : root.theme.colors.textPlaceholder
+ font.pixelSize: 16
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ radius: 18
+ color: button.hovered || button.activeFocus
+ ? root.theme.colors.panelHoverBg
+ : root.theme.colors.panelBg
+ border.color: root.theme.colors.borderStrong
+ border.width: 1
+
+ Behavior on color {
+ ColorAnimation { duration: 120 }
+ }
+ }
+ }
+}
diff --git a/apps/amm/qml/components/liquidity/AmmPrimaryButton.qml b/apps/amm/qml/components/liquidity/AmmPrimaryButton.qml
new file mode 100644
index 00000000..f75f3d18
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmmPrimaryButton.qml
@@ -0,0 +1,43 @@
+import QtQuick
+import QtQuick.Controls
+
+Button {
+ id: root
+
+ required property var theme
+
+ activeFocusOnTab: true
+ focusPolicy: Qt.StrongFocus
+ hoverEnabled: true
+ implicitHeight: 56
+
+ Accessible.name: text
+
+ contentItem: Text {
+ text: root.text
+ color: root.enabled ? "#FFFFFF" : root.theme.colors.textSecondary
+ font.pixelSize: 17
+ font.weight: Font.Medium
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ }
+
+ background: Rectangle {
+ radius: 20
+ color: !root.enabled
+ ? root.theme.colors.panelBg
+ : root.pressed
+ ? root.theme.colors.ctaPressedBg
+ : root.hovered || root.activeFocus
+ ? root.theme.colors.ctaHoverBg
+ : root.theme.colors.ctaBg
+ border.color: root.activeFocus && root.enabled
+ ? root.theme.colors.textPrimary : "transparent"
+ border.width: 1
+
+ Behavior on color {
+ ColorAnimation { duration: 120 }
+ }
+ }
+}
diff --git a/apps/amm/qml/components/liquidity/AmmTheme.qml b/apps/amm/qml/components/liquidity/AmmTheme.qml
new file mode 100644
index 00000000..06a847b8
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmmTheme.qml
@@ -0,0 +1,50 @@
+import QtQml
+
+QtObject {
+ property bool isDark: true
+ readonly property var colors: isDark ? dark : light
+
+ readonly property var light: ({
+ "background": "#F4EDE3",
+ "cardBg": "#FFFFFF",
+ "inputBg": "#EFE7DB",
+ "panelBg": "#E7E1D8",
+ "panelHoverBg": "#D9D0C2",
+ "textPrimary": "#151515",
+ "textSecondary": "#7D756E",
+ "textPlaceholder": "#A9A098",
+ "border": Qt.rgba(0, 0, 0, 0.08),
+ "borderStrong": Qt.rgba(0, 0, 0, 0.10),
+ "divider": Qt.rgba(0, 0, 0, 0.06),
+ "ctaBg": "#F26A21",
+ "ctaHoverBg": "#D95C1E",
+ "ctaPressedBg": "#C85018",
+ "selection": "#F2D8C7",
+ "noTokenCircle": "#A9A098",
+ "success": "#4F9B64",
+ "warning": "#B8732A",
+ "error": "#D85F4B"
+ })
+
+ readonly property var dark: ({
+ "background": "#151515",
+ "cardBg": "#1B1B1B",
+ "inputBg": "#101010",
+ "panelBg": "#181818",
+ "panelHoverBg": "#202020",
+ "textPrimary": "#E7E1D8",
+ "textSecondary": "#A9A098",
+ "textPlaceholder": "#8E8780",
+ "border": Qt.rgba(1, 1, 1, 0.08),
+ "borderStrong": Qt.rgba(1, 1, 1, 0.10),
+ "divider": Qt.rgba(1, 1, 1, 0.06),
+ "ctaBg": "#F26A21",
+ "ctaHoverBg": "#FF8A3D",
+ "ctaPressedBg": "#D95C1E",
+ "selection": "#211914",
+ "noTokenCircle": "#343434",
+ "success": "#78C88D",
+ "warning": "#F2B366",
+ "error": "#F08A76"
+ })
+}
diff --git a/apps/amm/qml/components/liquidity/AmmTokenAccessory.qml b/apps/amm/qml/components/liquidity/AmmTokenAccessory.qml
new file mode 100644
index 00000000..df3919e3
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmmTokenAccessory.qml
@@ -0,0 +1,46 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ required property var theme
+ property bool hasToken: false
+ property string tokenColor: root.theme.colors.noTokenCircle
+ property string tokenLetter: ""
+ property string tokenText: qsTr("Select token")
+ property string balance: ""
+ property string accessibleName: qsTr("Select token")
+ property bool invalid: false
+
+ signal clicked
+
+ spacing: 2
+
+ Text {
+ Layout.fillWidth: true
+ visible: root.balance.length > 0
+ text: qsTr("Balance %1").arg(root.balance)
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 10
+ horizontalAlignment: Text.AlignRight
+ elide: Text.ElideRight
+ }
+
+ AmmTokenSelectButton {
+ objectName: "tokenSelectButton"
+ Layout.fillWidth: true
+ theme: root.theme
+ enabled: root.enabled
+ invalid: root.invalid
+ hasToken: root.hasToken
+ tokenColor: root.tokenColor
+ tokenLetter: root.tokenLetter
+ text: root.tokenText
+ maximumTextWidth: 112
+ Accessible.name: root.accessibleName
+ onClicked: root.clicked()
+ }
+}
diff --git a/apps/amm/qml/components/liquidity/AmmTokenAmountSurface.qml b/apps/amm/qml/components/liquidity/AmmTokenAmountSurface.qml
new file mode 100644
index 00000000..39ccd5d7
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmmTokenAmountSurface.qml
@@ -0,0 +1,195 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Rectangle {
+ id: root
+
+ required property var theme
+ property string label: ""
+ property string amount: ""
+ property string supportingText: ""
+ property string errorText: ""
+ property string supportingActionText: ""
+ property bool readOnly: false
+ property bool muted: false
+ property bool invalid: root.errorText.length > 0
+ property Component accessory
+ property real accessoryWidth: 0
+ property real accessoryHeight: 0
+ property Component adjustment
+ property real adjustmentWidth: 0
+ property real adjustmentHeight: 0
+
+ signal amountEdited(string value)
+ signal amountEditingFinished(string value)
+ signal supportingActionClicked
+
+ implicitHeight: Math.max(110, contentRow.implicitHeight + 24)
+ radius: 16
+ color: root.muted ? root.theme.colors.panelBg : root.theme.colors.inputBg
+ border.color: root.invalid
+ ? root.theme.colors.error
+ : amountInput.activeFocus
+ ? root.theme.colors.ctaBg
+ : "transparent"
+ border.width: 1
+
+ Binding {
+ target: amountInput
+ property: "text"
+ value: root.amount
+ }
+
+ RowLayout {
+ id: contentRow
+
+ anchors.fill: parent
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+ anchors.topMargin: 12
+ anchors.bottomMargin: 12
+ spacing: 10
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: root.label
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 13
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: root.errorText.length > 0
+ text: root.errorText
+ color: root.theme.colors.error
+ font.pixelSize: 11
+ wrapMode: Text.Wrap
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 44
+
+ TextInput {
+ id: amountInput
+
+ anchors.fill: parent
+ readOnly: root.readOnly
+ color: root.readOnly || root.muted
+ ? root.theme.colors.textSecondary
+ : root.theme.colors.textPrimary
+ font.pixelSize: {
+ var length = Math.max(1, String(root.amount || "0").length)
+ return Math.max(14, Math.min(34, Math.floor(width / (length * 0.68))))
+ }
+ font.weight: Font.Bold
+ horizontalAlignment: Text.AlignLeft
+ selectionColor: root.theme.colors.selection
+ selectedTextColor: root.theme.colors.textPrimary
+ clip: true
+ inputMethodHints: Qt.ImhFormattedNumbersOnly
+ maximumLength: 80
+ validator: RegularExpressionValidator {
+ regularExpression: /^[0-9]*\.?[0-9]*$/
+ }
+
+ Accessible.name: root.label
+
+ onTextEdited: {
+ if (!root.readOnly)
+ root.amountEdited(text)
+ }
+ onEditingFinished: {
+ if (!root.readOnly)
+ root.amountEditingFinished(text)
+ }
+ }
+
+ Text {
+ anchors.fill: parent
+ text: "0"
+ color: root.theme.colors.textPlaceholder
+ font: amountInput.font
+ visible: amountInput.text === "" && !amountInput.activeFocus
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 6
+
+ Loader {
+ active: root.adjustment !== null
+ visible: active
+ sourceComponent: root.adjustment
+ Layout.preferredWidth: active ? root.adjustmentWidth : 0
+ Layout.preferredHeight: active ? root.adjustmentHeight : 0
+ }
+
+ Button {
+ id: supportingAction
+
+ visible: root.supportingActionText.length > 0
+ enabled: visible && !root.readOnly
+ implicitWidth: 40
+ implicitHeight: 24
+ text: root.supportingActionText
+ hoverEnabled: true
+ Accessible.name: text
+ onClicked: root.supportingActionClicked()
+
+ contentItem: Text {
+ text: supportingAction.text
+ color: supportingAction.enabled
+ ? root.theme.colors.ctaBg
+ : root.theme.colors.textPlaceholder
+ font.pixelSize: 11
+ font.weight: Font.Bold
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ radius: 6
+ color: supportingAction.hovered || supportingAction.activeFocus
+ ? root.theme.colors.selection : "transparent"
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: root.supportingText
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 11
+ horizontalAlignment: root.adjustment !== null
+ || root.supportingActionText.length > 0
+ ? Text.AlignRight : Text.AlignLeft
+ elide: Text.ElideRight
+ visible: text.length > 0
+ }
+ }
+ }
+
+ Loader {
+ sourceComponent: root.accessory
+ visible: status === Loader.Ready
+ Layout.alignment: Qt.AlignTop
+ Layout.topMargin: 17
+ Layout.preferredWidth: root.accessoryWidth
+ Layout.preferredHeight: root.accessoryHeight
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation { duration: 180 }
+ }
+}
diff --git a/apps/amm/qml/components/liquidity/AmmTokenSelectButton.qml b/apps/amm/qml/components/liquidity/AmmTokenSelectButton.qml
new file mode 100644
index 00000000..859af85c
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmmTokenSelectButton.qml
@@ -0,0 +1,80 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Button {
+ id: root
+
+ required property var theme
+ property bool hasToken: false
+ property string tokenColor: root.theme.colors.noTokenCircle
+ property string tokenLetter: ""
+ property bool showIndicator: true
+ property bool invalid: false
+ property real maximumTextWidth: 112
+
+ implicitWidth: contentRow.implicitWidth + 24
+ implicitHeight: 40
+ leftPadding: 12
+ rightPadding: 12
+ hoverEnabled: true
+
+ contentItem: RowLayout {
+ id: contentRow
+
+ spacing: 6
+
+ Rectangle {
+ Layout.preferredWidth: 24
+ Layout.preferredHeight: 24
+ visible: root.hasToken
+ radius: 12
+ color: root.tokenColor
+
+ Text {
+ anchors.centerIn: parent
+ text: root.tokenLetter
+ color: "#FFFFFF"
+ font.pixelSize: 10
+ font.weight: Font.Bold
+ }
+ }
+
+ Text {
+ Layout.maximumWidth: root.maximumTextWidth
+ text: root.text
+ color: root.enabled
+ ? root.theme.colors.textPrimary
+ : root.theme.colors.textPlaceholder
+ font.pixelSize: 15
+ font.weight: root.hasToken ? Font.Medium : Font.Normal
+ elide: Text.ElideRight
+ }
+
+ Text {
+ visible: root.showIndicator
+ text: "\u25BE"
+ color: root.enabled
+ ? root.theme.colors.textSecondary
+ : root.theme.colors.textPlaceholder
+ font.pixelSize: 10
+ }
+ }
+
+ background: Rectangle {
+ radius: 20
+ color: root.pressed
+ ? root.theme.colors.selection
+ : root.hovered || root.activeFocus
+ ? root.theme.colors.panelHoverBg
+ : root.theme.colors.panelBg
+ border.width: root.invalid || root.activeFocus ? 1 : 0
+ border.color: root.invalid ? root.theme.colors.error : root.theme.colors.ctaBg
+
+ Behavior on color {
+ ColorAnimation { duration: 120 }
+ }
+ }
+}
diff --git a/apps/amm/qml/components/liquidity/AmountMath.js b/apps/amm/qml/components/liquidity/AmountMath.js
new file mode 100644
index 00000000..11309831
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/AmountMath.js
@@ -0,0 +1,295 @@
+.pragma library
+
+var base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
+var U128_MAX = "340282366920938463463374607431768211455"
+var U32_MAX = "4294967295"
+var Q64 = "18446744073709551616"
+
+function normalize(value) {
+ var text = String(value === undefined || value === null ? "" : value)
+ var index = 0
+ while (index + 1 < text.length && text.charAt(index) === "0")
+ ++index
+ return text.length > 0 ? text.slice(index) : "0"
+}
+
+function isUnsigned(value) {
+ return /^[0-9]+$/.test(String(value))
+}
+
+function compare(left, right) {
+ var a = normalize(left)
+ var b = normalize(right)
+ if (a.length !== b.length)
+ return a.length < b.length ? -1 : 1
+ if (a === b)
+ return 0
+ return a < b ? -1 : 1
+}
+
+function compareBase58Ids(left, right) {
+ var leftStart = 0
+ var rightStart = 0
+ while (leftStart < left.length && left[leftStart] === "1")
+ ++leftStart
+ while (rightStart < right.length && right[rightStart] === "1")
+ ++rightStart
+
+ var leftLength = left.length - leftStart
+ var rightLength = right.length - rightStart
+ if (leftLength !== rightLength)
+ return leftLength < rightLength ? -1 : 1
+
+ for (var i = 0; i < leftLength; ++i) {
+ var leftDigit = base58Alphabet.indexOf(left[leftStart + i])
+ var rightDigit = base58Alphabet.indexOf(right[rightStart + i])
+ if (leftDigit < 0 || rightDigit < 0)
+ return 0
+ if (leftDigit !== rightDigit)
+ return leftDigit < rightDigit ? -1 : 1
+ }
+ return 0
+}
+
+function subtract(left, right) {
+ var a = normalize(left)
+ var b = normalize(right)
+ var result = []
+ var borrow = 0
+ var j = b.length - 1
+ for (var i = a.length - 1; i >= 0; --i) {
+ var digit = Number(a.charAt(i)) - borrow - (j >= 0 ? Number(b.charAt(j)) : 0)
+ if (digit < 0) {
+ digit += 10
+ borrow = 1
+ } else {
+ borrow = 0
+ }
+ result.unshift(String(digit))
+ --j
+ }
+ return normalize(result.join(""))
+}
+
+function multiply(left, right) {
+ var a = normalize(left)
+ var b = normalize(right)
+ if (a === "0" || b === "0")
+ return "0"
+
+ var result = []
+ for (var z = 0; z < a.length + b.length; ++z)
+ result.push(0)
+
+ for (var i = a.length - 1; i >= 0; --i) {
+ for (var j = b.length - 1; j >= 0; --j) {
+ var low = i + j + 1
+ var high = i + j
+ var product = Number(a.charAt(i)) * Number(b.charAt(j)) + result[low]
+ result[low] = product % 10
+ result[high] += Math.floor(product / 10)
+ }
+ }
+ return normalize(result.join(""))
+}
+
+function divide(left, right) {
+ var numerator = normalize(left)
+ var denominator = normalize(right)
+ if (denominator === "0")
+ return { "quotient": "0", "remainder": numerator, "valid": false }
+
+ var quotient = ""
+ var remainder = "0"
+ for (var i = 0; i < numerator.length; ++i) {
+ remainder = normalize((remainder === "0" ? "" : remainder) + numerator.charAt(i))
+ var digit = 0
+ while (compare(remainder, denominator) >= 0) {
+ remainder = subtract(remainder, denominator)
+ ++digit
+ }
+ quotient += String(digit)
+ }
+ return { "quotient": normalize(quotient), "remainder": remainder, "valid": true }
+}
+
+function increment(value) {
+ var digits = normalize(value).split("")
+ for (var i = digits.length - 1; i >= 0; --i) {
+ if (digits[i] !== "9") {
+ digits[i] = String(Number(digits[i]) + 1)
+ return digits.join("")
+ }
+ digits[i] = "0"
+ }
+ digits.unshift("1")
+ return digits.join("")
+}
+
+function pow10(exponent) {
+ var value = "1"
+ for (var i = 0; i < exponent; ++i)
+ value += "0"
+ return value
+}
+
+function parseHuman(text, decimals) {
+ var value = String(text)
+ if (value.length === 0)
+ return { "ok": false, "code": "amount_required", "raw": "" }
+ var match = /^(0|[1-9][0-9]*)(?:\.([0-9]*))?$/.exec(value)
+ if (!match)
+ return { "ok": false, "code": "invalid_amount_format", "raw": "" }
+
+ var fraction = match[2] || ""
+ if (fraction.length > decimals)
+ return { "ok": false, "code": "invalid_amount_precision", "raw": "" }
+ var raw = normalize(match[1] + fraction + pow10(decimals - fraction.length).slice(1))
+ if (compare(raw, U128_MAX) > 0)
+ return { "ok": false, "code": "invalid_raw_amount", "raw": "" }
+ return { "ok": true, "code": "", "raw": raw }
+}
+
+function formatRaw(rawValue, decimals) {
+ if (!isUnsigned(rawValue))
+ return ""
+ var raw = normalize(rawValue)
+ if (decimals === 0)
+ return raw
+ while (raw.length <= decimals)
+ raw = "0" + raw
+ var whole = raw.slice(0, raw.length - decimals)
+ var fraction = raw.slice(raw.length - decimals).replace(/0+$/, "")
+ return fraction.length > 0 ? whole + "." + fraction : whole
+}
+
+function parsePrice(text) {
+ var value = String(text)
+ if (value.length === 0)
+ return { "ok": false, "code": "amount_required" }
+ var match = /^(0|[1-9][0-9]*)(?:\.([0-9]*))?$/.exec(value)
+ if (!match)
+ return { "ok": false, "code": "invalid_amount_format" }
+ var fraction = match[2] || ""
+ var numerator = normalize(match[1] + fraction)
+ if (numerator === "0")
+ return { "ok": false, "code": "amount_must_be_positive" }
+ return {
+ "ok": true,
+ "code": "",
+ "numerator": numerator,
+ "scale": fraction.length
+ }
+}
+
+function parseRatio(amountA, amountB) {
+ var parsedA = parsePrice(amountA)
+ if (!parsedA.ok)
+ return { "ok": false, "code": parsedA.code }
+ var parsedB = parsePrice(amountB)
+ if (!parsedB.ok)
+ return { "ok": false, "code": parsedB.code }
+
+ return {
+ "ok": true,
+ "code": "",
+ "numerator": multiply(parsedB.numerator, pow10(parsedA.scale)),
+ "denominator": multiply(parsedA.numerator, pow10(parsedB.scale))
+ }
+}
+
+function ratioToQ64(amountA, amountB, canonicalDecimalsA, canonicalDecimalsB,
+ displayIsCanonical) {
+ var ratio = parseRatio(amountA, amountB)
+ if (!ratio.ok)
+ return { "ok": false, "code": ratio.code, "raw": "" }
+
+ var numerator
+ var denominator
+ if (displayIsCanonical) {
+ numerator = multiply(multiply(ratio.numerator, Q64), pow10(canonicalDecimalsB))
+ denominator = multiply(ratio.denominator, pow10(canonicalDecimalsA))
+ } else {
+ numerator = multiply(multiply(ratio.denominator, Q64), pow10(canonicalDecimalsB))
+ denominator = multiply(ratio.numerator, pow10(canonicalDecimalsA))
+ }
+ var raw = divide(numerator, denominator).quotient
+ if (raw === "0")
+ return { "ok": false, "code": "amount_too_low", "raw": "" }
+ if (compare(raw, U128_MAX) > 0)
+ return { "ok": false, "code": "invalid_raw_amount", "raw": "" }
+ return { "ok": true, "code": "", "raw": raw }
+}
+
+function pairAmount(raw, fromA, decimalsA, decimalsB, priceAmountA, priceAmountB) {
+ if (!isUnsigned(raw))
+ return { "ok": false, "code": "invalid_raw_amount", "raw": "" }
+ var ratio = parseRatio(priceAmountA, priceAmountB)
+ if (!ratio.ok)
+ return { "ok": false, "code": ratio.code, "raw": "" }
+
+ var numerator
+ var denominator
+ if (fromA) {
+ numerator = multiply(multiply(raw, ratio.numerator), pow10(decimalsB))
+ denominator = multiply(ratio.denominator, pow10(decimalsA))
+ } else {
+ numerator = multiply(multiply(raw, ratio.denominator), pow10(decimalsA))
+ denominator = multiply(ratio.numerator, pow10(decimalsB))
+ }
+ var result = divide(numerator, denominator)
+ if (!result.valid)
+ return { "ok": false, "code": "invalid_raw_amount", "raw": "" }
+ var rounded = compare(multiply(result.remainder, "2"), denominator) >= 0
+ ? increment(result.quotient) : result.quotient
+ if (rounded === "0")
+ return { "ok": false, "code": "amount_too_low", "raw": "" }
+ if (compare(rounded, U128_MAX) > 0)
+ return { "ok": false, "code": "invalid_raw_amount", "raw": "" }
+ return { "ok": true, "code": "", "raw": rounded }
+}
+
+function ratioValue(amountA, amountB, precision) {
+ var ratio = parseRatio(amountA, amountB)
+ return ratio.ok ? formatRatio(ratio.numerator, ratio.denominator, precision) : ""
+}
+
+function formatRatio(numerator, denominator, precision) {
+ var result = divide(numerator, denominator)
+ if (!result.valid)
+ return ""
+ var fraction = ""
+ var remainder = result.remainder
+ for (var i = 0; i < precision && remainder !== "0"; ++i) {
+ var digit = divide(multiply(remainder, "10"), denominator)
+ fraction += digit.quotient
+ remainder = digit.remainder
+ }
+ fraction = fraction.replace(/0+$/, "")
+ return fraction.length > 0 ? result.quotient + "." + fraction : result.quotient
+}
+
+function priceFromQ64(rawValue, canonicalDecimalsA, canonicalDecimalsB, displayIsCanonical) {
+ if (!isUnsigned(rawValue) || normalize(rawValue) === "0")
+ return ""
+ var numerator
+ var denominator
+ if (displayIsCanonical) {
+ numerator = multiply(rawValue, pow10(canonicalDecimalsA))
+ denominator = multiply(Q64, pow10(canonicalDecimalsB))
+ } else {
+ numerator = multiply(Q64, pow10(canonicalDecimalsB))
+ denominator = multiply(rawValue, pow10(canonicalDecimalsA))
+ }
+ return formatRatio(numerator, denominator, 12)
+}
+
+function mulDivFloor(left, right, denominator) {
+ return divide(multiply(left, right), denominator).quotient
+}
+
+function toU32(value) {
+ if (!isUnsigned(value) || compare(value, U32_MAX) > 0)
+ return -1
+ return Number(normalize(value))
+}
diff --git a/apps/amm/qml/components/liquidity/LiquidityActionTabs.qml b/apps/amm/qml/components/liquidity/LiquidityActionTabs.qml
deleted file mode 100644
index 5a7c1d1f..00000000
--- a/apps/amm/qml/components/liquidity/LiquidityActionTabs.qml
+++ /dev/null
@@ -1,91 +0,0 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
-
-Rectangle {
- id: root
-
- property int currentIndex: 0
-
- signal tabRequested(int index)
-
- color: "#181818"
- implicitHeight: 42
- radius: 8
- border.color: "#303030"
- border.width: 1
-
- RowLayout {
- anchors.fill: parent
- anchors.margins: 4
- spacing: 4
-
- Button {
- id: addTab
-
- activeFocusOnTab: true
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("Add liquidity")
-
- Accessible.name: addTab.text
- Accessible.role: Accessible.PageTab
-
- Layout.fillHeight: true
- Layout.fillWidth: true
-
- onClicked: root.tabRequested(0)
-
- contentItem: Text {
- color: root.currentIndex === 0 ? "#F2D8C7" : addTab.hovered || addTab.activeFocus ? "#E7E1D8" : "#8E8780"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 12
- horizontalAlignment: Text.AlignHCenter
- text: addTab.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: addTab.activeFocus || root.currentIndex === 0 ? "#F26A21" : "#181818"
- border.width: 1
- color: addTab.pressed ? "#2A1D16" : root.currentIndex === 0 ? "#211914" : addTab.hovered || addTab.activeFocus ? "#202020" : "#121212"
- radius: 6
- }
- }
-
- Button {
- id: removeTab
-
- activeFocusOnTab: true
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("Remove liquidity")
-
- Accessible.name: removeTab.text
- Accessible.role: Accessible.PageTab
-
- Layout.fillHeight: true
- Layout.fillWidth: true
-
- onClicked: root.tabRequested(1)
-
- contentItem: Text {
- color: root.currentIndex === 1 ? "#F2D8C7" : removeTab.hovered || removeTab.activeFocus ? "#E7E1D8" : "#8E8780"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 12
- horizontalAlignment: Text.AlignHCenter
- text: removeTab.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: removeTab.activeFocus || root.currentIndex === 1 ? "#F26A21" : "#181818"
- border.width: 1
- color: removeTab.pressed ? "#2A1D16" : root.currentIndex === 1 ? "#211914" : removeTab.hovered || removeTab.activeFocus ? "#202020" : "#121212"
- radius: 6
- }
- }
- }
-}
diff --git a/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml
index 68af40da..cbcc3b68 100644
--- a/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml
+++ b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml
@@ -5,87 +5,47 @@ ColumnLayout {
id: root
property var snapshot: ({})
- readonly property bool isAdd: root.snapshot.action === "add"
spacing: 8
- ColumnLayout {
- Layout.fillWidth: true
- spacing: 8
- visible: root.isAdd
-
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Deposit %1").arg(root.snapshot.tokenA || "")
- value: root.snapshot.depositA || ""
- }
-
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Deposit %1").arg(root.snapshot.tokenB || "")
- value: root.snapshot.depositB || ""
- }
-
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Receive at least")
- value: root.snapshot.minLpReceived || ""
- }
-
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Current ratio")
- value: root.snapshot.currentRatio || ""
- }
-
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Fee tier")
- value: root.snapshot.feeTier || ""
- }
-
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Slippage tolerance")
- value: root.snapshot.slippageTolerance || ""
- }
+ function actionText(instruction) {
+ if (instruction === "NewDefinition")
+ return qsTr("Create pool")
+ if (instruction === "AddLiquidity")
+ return qsTr("Add liquidity")
+ return instruction || "-"
}
- ColumnLayout {
+ SummaryRow {
Layout.fillWidth: true
- spacing: 8
- visible: !root.isAdd
-
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Burn LP")
- value: qsTr("%1 (%2)")
- .arg(root.snapshot.burnText || "")
- .arg(root.snapshot.burnPercent || "")
- }
+ label: qsTr("Pair")
+ value: root.snapshot.pairText || "-"
+ }
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Receive at least %1").arg(root.snapshot.tokenA || "")
- value: root.snapshot.minTokenAReceived || ""
- }
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Action")
+ value: root.actionText(root.snapshot.instruction)
+ }
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Receive at least %1").arg(root.snapshot.tokenB || "")
- value: root.snapshot.minTokenBReceived || ""
- }
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Fee")
+ value: root.snapshot.feeText || "-"
+ }
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Slippage tolerance")
- value: root.snapshot.slippageTolerance || ""
- }
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Deposit")
+ value: qsTr("%1 + %2")
+ .arg(root.snapshot.depositAText || "-")
+ .arg(root.snapshot.depositBText || "-")
+ valueWrapAnywhere: true
+ }
- SummaryRow {
- Layout.fillWidth: true
- label: qsTr("Post-removal share")
- value: root.snapshot.postRemovalShare || ""
- }
+ SummaryRow {
+ Layout.fillWidth: true
+ label: qsTr("Expected LP")
+ value: root.snapshot.expectedLpText || "-"
}
}
diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml
new file mode 100644
index 00000000..c70000fc
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml
@@ -0,0 +1,1542 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import Logos.Controls
+import Logos.Icons
+import Logos.Wallet
+
+import "AmountMath.js" as AmountMath
+
+AmmActionCard {
+ id: root
+
+ theme: fallbackTheme
+
+ AmmTheme {
+ id: fallbackTheme
+ }
+
+ property var newPositionContext: ({})
+ property var flowState: ({})
+ property string selectedTokenAId: ""
+ property string selectedTokenBId: ""
+ property int selectedFeeBps: 30
+ property int slippageBps: 50
+ property string amountA: ""
+ property string amountB: ""
+ property string priceAmountA: "1"
+ property string priceAmountB: "1"
+ property string minimumAmountARaw: ""
+ property string minimumAmountBRaw: ""
+ property var localErrors: []
+ property string resolvingTokenId: ""
+ property string resolvingTokenSide: ""
+ property string tokenResolutionError: ""
+ property string tokenResolutionErrorSide: ""
+ property string tokenResolutionMessage: ""
+ property string confirmedPoolStatus: ""
+ property var activePoolQuote: ({})
+ property string headingText: qsTr("New position")
+ property string headingDetail: ""
+ property bool showRefreshAction: true
+
+ readonly property var quotePayload: root.flowState.quote || ({})
+ readonly property bool contextLoading: root.flowState.contextLoading === true
+ readonly property bool quoteLoading: root.flowState.quoteLoading === true
+ readonly property bool submitting: root.flowState.submitting === true
+ readonly property bool quoteStale: root.flowState.quoteStale === true
+ readonly property bool poolCreationPending: root.flowState.poolCreationPending === true
+ readonly property string submitError: root.flowState.errorCode
+ ? root.issueText(root.flowState.errorCode) : ""
+ readonly property string transactionId: String(root.flowState.transactionId || "")
+ readonly property var emptyToken: ({
+ "definitionId": "",
+ "name": "",
+ "totalSupplyRaw": "0",
+ "balanceRaw": "0",
+ "selectable": false
+ })
+ readonly property var tokens: root.newPositionContext && root.newPositionContext.tokens
+ ? root.newPositionContext.tokens : []
+ readonly property var feeTiers: root.newPositionContext && root.newPositionContext.feeTiers
+ ? root.newPositionContext.feeTiers : []
+ readonly property var tokenA: root.tokenById(root.selectedTokenAId)
+ readonly property var tokenB: root.tokenById(root.selectedTokenBId)
+ readonly property int decimalsA: 0
+ readonly property int decimalsB: 0
+ readonly property bool displayIsCanonical: root.selectedTokenAId.length > 0
+ && AmountMath.compareBase58Ids(root.selectedTokenAId,
+ root.selectedTokenBId) > 0
+ readonly property int canonicalDecimalsA: root.displayIsCanonical ? root.decimalsA : root.decimalsB
+ readonly property int canonicalDecimalsB: root.displayIsCanonical ? root.decimalsB : root.decimalsA
+ readonly property string initialPrice: AmountMath.ratioValue(root.priceAmountA,
+ root.priceAmountB,
+ 12)
+ readonly property string inverseInitialPrice: AmountMath.ratioValue(root.priceAmountB,
+ root.priceAmountA,
+ 12)
+ readonly property string poolStatus: root.effectivePoolStatus()
+ readonly property bool activePool: root.poolStatus === "active_pool"
+ readonly property bool missingPool: root.poolStatus === "missing_pool"
+ readonly property int poolFeeBps: root.knownPoolFeeBps()
+ readonly property bool compact: root.width < 420
+ readonly property bool hasPair: root.selectedTokenAId.length > 0
+ && root.selectedTokenBId.length > 0
+ && root.selectedTokenAId !== root.selectedTokenBId
+ readonly property bool resolvingToken: root.resolvingTokenId.length > 0
+ readonly property bool canConfirm: root.quotePayload.schema === "new-position.v1"
+ && root.quotePayload.status === "ok"
+ && root.quotePayload.canSubmit === true
+ && root.quoteMatchesPair()
+ && String(root.quotePayload.quoteHash || "").length > 0
+ && !root.contextLoading
+ && !root.quoteLoading
+ && !root.quoteStale
+ && !root.submitting
+ && !root.poolCreationPending
+
+ signal quoteRequested(bool immediate, var quoteRequest)
+ signal confirmationRequested(var snapshot)
+ signal tokenResolveRequested(string tokenId)
+ signal draftChanged
+ signal refreshRequested
+
+ readonly property int contentPadding: width >= 600 ? 24 : 16
+
+ implicitHeight: content.implicitHeight + root.contentPadding * 2
+ implicitWidth: 480
+
+ Component.onCompleted: Qt.callLater(root.ensurePair)
+ onNewPositionContextChanged: Qt.callLater(root.applyContextChange)
+ function applyContextChange() {
+ if (root.resolvingToken)
+ root.finishTokenResolution()
+ else
+ root.ensurePair()
+ }
+ onQuotePayloadChanged: {
+ if (root.quoteStale)
+ return
+ root.rememberPoolStatus()
+ root.rememberActivePoolQuote()
+ Qt.callLater(root.applyQuoteSideEffects)
+ }
+
+ Component {
+ id: priceAmountAAdjustment
+
+ PriceRatioAdjustment {
+ amount: root.priceAmountA
+ enabled: !root.submitting
+ fieldName: "priceAmountAField"
+ invalid: root.fieldHasError("initialPrice")
+ onEdited: function(value) { root.editPrice("A", value) }
+ }
+ }
+
+ Component {
+ id: priceAmountBAdjustment
+
+ PriceRatioAdjustment {
+ amount: root.priceAmountB
+ enabled: !root.submitting
+ fieldName: "priceAmountBField"
+ invalid: root.fieldHasError("initialPrice")
+ onEdited: function(value) { root.editPrice("B", value) }
+ }
+ }
+
+ ColumnLayout {
+ id: content
+
+ anchors.fill: parent
+ anchors.margins: root.contentPadding
+ spacing: 16
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 12
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 3
+
+ Text {
+ text: root.headingText
+ color: root.theme.colors.textPrimary
+ font.pixelSize: 18
+ font.weight: Font.DemiBold
+ font.letterSpacing: 0
+ }
+
+ Text {
+ text: root.headingDetail.length > 0
+ ? root.headingDetail : root.contextStatusText()
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 12
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
+
+ BusyIndicator {
+ running: root.contextLoading || root.quoteLoading
+ visible: running
+ implicitWidth: 24
+ implicitHeight: 24
+ }
+
+ LogosIconButton {
+ iconSource: LogosIcons.refresh
+ iconColor: root.theme.colors.textSecondary
+ iconSize: 18
+ Layout.preferredWidth: 36
+ Layout.preferredHeight: 36
+ enabled: !root.contextLoading && !root.submitting
+ visible: root.showRefreshAction
+ Accessible.name: qsTr("Refresh position data")
+ ToolTip.visible: hovered
+ ToolTip.text: Accessible.name
+ onClicked: root.refreshRequested()
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ implicitHeight: networkMessage.implicitHeight + 20
+ radius: 6
+ color: root.theme.colors.panelBg
+ border.color: root.theme.colors.error
+ visible: root.contextBlocksForm()
+
+ Text {
+ id: networkMessage
+ anchors.fill: parent
+ anchors.margins: 10
+ text: root.contextErrorText()
+ color: root.theme.colors.textPrimary
+ font.pixelSize: 12
+ wrapMode: Text.Wrap
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ TokenAmountInput {
+ id: tokenAInput
+
+ objectName: "tokenAAmountInput"
+ Layout.fillWidth: true
+ theme: root.theme
+ text: root.amountA
+ label: qsTr("Token A amount")
+ balance: root.contextLoading ? "" : root.balanceText(root.tokenA, root.decimalsA)
+ helperText: root.missingPool && !root.compact
+ ? root.minimumAmountText("A") : ""
+ errorText: root.formErrorText()
+ invalid: root.fieldHasError("amountA")
+ readOnly: root.submitting || (!root.activePool && !root.missingPool)
+ showMaxButton: root.activePool
+ tokenData: root.tokenA.definitionId ? root.tokenA : null
+ tokens: root.tokens
+ selectedTokenId: root.selectedTokenAId
+ tokenInvalid: root.tokenHasError("A")
+ tokenSelectionEnabled: !root.contextLoading && !root.submitting
+ adjustment: root.missingPool ? priceAmountAAdjustment : null
+ adjustmentWidth: root.missingPool ? (root.compact ? 100 : 142) : 0
+ adjustmentHeight: root.missingPool ? 24 : 0
+ disabledReasonForCode: function(code) { return root.issueText(code) }
+ detailForToken: function(token) { return root.tokenBalanceDetail(token) }
+ onEditingChanged: function(value) {
+ if (root.activePool)
+ root.editActiveAmount("A", value)
+ else
+ root.editMissingAmount("A", value)
+ }
+ onEditingCommitted: function(value) {
+ if (root.activePool)
+ root.finishActiveAmount("A", value)
+ else
+ root.finishMissingAmount("A", value)
+ }
+ onMaxClicked: root.useMaximum()
+ onTokenSelected: function(tokenId) { root.resolveToken("A", tokenId) }
+ onTokenEntered: function(value) { root.resolveToken("A", value) }
+ }
+
+ AmmPairSeparator {
+ Layout.fillWidth: true
+ theme: root.theme
+ enabled: root.hasPair && !root.submitting
+ onClicked: root.swapTokens()
+ }
+
+ TokenAmountInput {
+ id: tokenBInput
+
+ objectName: "tokenBAmountInput"
+ Layout.fillWidth: true
+ theme: root.theme
+ text: root.amountB
+ label: qsTr("Token B amount")
+ balance: root.contextLoading ? "" : root.balanceText(root.tokenB, root.decimalsB)
+ helperText: root.missingPool && !root.compact
+ ? root.minimumAmountText("B") : ""
+ invalid: root.fieldHasError("amountB")
+ readOnly: root.submitting || (!root.activePool && !root.missingPool)
+ showMaxButton: root.activePool
+ tokenData: root.tokenB.definitionId ? root.tokenB : null
+ tokens: root.tokens
+ selectedTokenId: root.selectedTokenBId
+ tokenInvalid: root.tokenHasError("B")
+ tokenSelectionEnabled: !root.contextLoading && !root.submitting
+ adjustment: root.missingPool ? priceAmountBAdjustment : null
+ adjustmentWidth: root.missingPool ? (root.compact ? 100 : 142) : 0
+ adjustmentHeight: root.missingPool ? 24 : 0
+ disabledReasonForCode: function(code) { return root.issueText(code) }
+ detailForToken: function(token) { return root.tokenBalanceDetail(token) }
+ onEditingChanged: function(value) {
+ if (root.activePool)
+ root.editActiveAmount("B", value)
+ else
+ root.editMissingAmount("B", value)
+ }
+ onEditingCommitted: function(value) {
+ if (root.activePool)
+ root.finishActiveAmount("B", value)
+ else
+ root.finishMissingAmount("B", value)
+ }
+ onMaxClicked: root.useMaximum()
+ onTokenSelected: function(tokenId) { root.resolveToken("B", tokenId) }
+ onTokenEntered: function(value) { root.resolveToken("B", value) }
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: root.resolvingToken || root.tokenResolutionMessage.length > 0
+ text: root.resolvingToken
+ ? qsTr("Resolving TokenDefinition...")
+ : root.tokenResolutionMessage
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 11
+ wrapMode: Text.Wrap
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: text.length > 0
+ text: root.activePool ? root.activePriceText()
+ : root.missingPool ? root.depositMultiplierText() : ""
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 12
+ horizontalAlignment: Text.AlignRight
+ wrapMode: Text.Wrap
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ implicitHeight: 1
+ color: root.theme.colors.divider
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 8
+ visible: !root.contextLoading
+
+ Text {
+ text: qsTr("Fee tier")
+ color: root.theme.colors.textPrimary
+ font.pixelSize: 13
+ font.weight: Font.Medium
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: root.compact ? 2 : 4
+ columnSpacing: 8
+ rowSpacing: 8
+
+ Repeater {
+ model: root.feeTiers
+
+ Item {
+ id: feeTierOption
+
+ required property var modelData
+ readonly property string disabledReason: root.feeDisabledReason(modelData)
+ readonly property bool invalid: root.fieldHasError("feeBps")
+ && feeTierButton.checked
+ Layout.fillWidth: true
+ implicitHeight: 40
+
+ Button {
+ id: feeTierButton
+
+ anchors.fill: parent
+ text: parent.modelData.label || root.feeLabel(parent.modelData.feeBps)
+ checkable: true
+ checked: root.selectedFeeBps === parent.modelData.feeBps
+ enabled: parent.disabledReason.length === 0 && !root.submitting
+ onClicked: root.selectFee(parent.modelData.feeBps)
+
+ contentItem: Text {
+ text: feeTierButton.text
+ color: feeTierButton.enabled
+ ? root.theme.colors.textPrimary
+ : root.theme.colors.textPlaceholder
+ font.pixelSize: 12
+ font.weight: Font.Medium
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ radius: 6
+ color: feeTierButton.checked
+ ? root.theme.colors.selection
+ : root.theme.colors.inputBg
+ border.color: feeTierOption.invalid
+ ? root.theme.colors.error
+ : feeTierButton.checked
+ ? root.theme.colors.ctaBg
+ : root.theme.colors.borderStrong
+ border.width: 1
+ }
+ }
+
+ MouseArea {
+ id: disabledFeeHover
+ anchors.fill: parent
+ enabled: parent.disabledReason.length > 0
+ hoverEnabled: true
+ acceptedButtons: Qt.NoButton
+ }
+
+ ToolTip.visible: disabledFeeHover.containsMouse
+ ToolTip.text: disabledReason
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 10
+ visible: root.activePool
+
+ Text {
+ text: qsTr("Slippage")
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 12
+ Layout.fillWidth: true
+ }
+
+ Item {
+ Layout.preferredWidth: slippageControl.implicitWidth
+ Layout.preferredHeight: slippageControl.implicitHeight
+
+ LogosSpinBox {
+ id: slippageControl
+
+ anchors.fill: parent
+ from: 0
+ to: 5000
+ stepSize: 10
+ editable: true
+ value: root.slippageBps
+ enabled: !root.submitting
+ textFromValue: function(value) { return root.formatBps(value) }
+ valueFromText: function(text) {
+ var parsed = AmountMath.parseHuman(String(text).replace("%", "").trim(), 2)
+ return parsed.ok ? AmountMath.toU32(parsed.raw) : root.slippageBps
+ }
+ onValueModified: {
+ root.slippageBps = value
+ root.noteDraftChanged()
+ root.requestQuote(true)
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ visible: root.fieldHasError("slippageBps")
+ color: "transparent"
+ radius: 6
+ border.color: root.theme.colors.error
+ border.width: 1
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 9
+ visible: root.quotePayload.status === "ok"
+ && root.quoteMatchesPair()
+ && !root.quoteStale
+
+ Rectangle {
+ Layout.fillWidth: true
+ implicitHeight: 1
+ color: root.theme.colors.divider
+ }
+
+ LabelValueRow {
+ label: root.activePool ? qsTr("Expected spend") : qsTr("Opening deposit")
+ value: root.depositSummary()
+ }
+
+ LabelValueRow {
+ visible: root.missingPool
+ label: qsTr("Initial price")
+ value: root.initialPriceText(false)
+ }
+
+ LabelValueRow {
+ visible: root.missingPool
+ label: qsTr("Inverse price")
+ value: root.initialPriceText(true)
+ }
+
+ LabelValueRow {
+ visible: root.missingPool
+ label: qsTr("Deposit multiplier")
+ value: root.depositMultiplierValue()
+ }
+
+ LabelValueRow {
+ visible: root.missingPool
+ label: qsTr("Deposit scale")
+ value: root.depositBasisPointsText()
+ }
+
+ LabelValueRow {
+ label: qsTr("Expected LP")
+ value: root.rawLpText(root.quotePayload.expectedLpRaw)
+ }
+
+ LabelValueRow {
+ label: root.activePool ? qsTr("Minimum LP") : qsTr("Locked LP")
+ value: root.rawLpText(root.activePool
+ ? root.quotePayload.minimumLpRaw
+ : root.quotePayload.lockedLpRaw)
+ }
+
+ LabelValueRow {
+ label: qsTr("Pool")
+ value: String(root.quotePayload.poolId || "")
+ valueWrapAnywhere: true
+ }
+
+ LogosButton {
+ id: accountPlanButton
+ text: qsTr("Account plan (%1)").arg(root.accountPreview().length)
+ enabled: root.accountPreview().length > 0
+ property bool checked: false
+ implicitWidth: 150
+ implicitHeight: 36
+ radius: 6
+ Layout.alignment: Qt.AlignLeft
+ onClicked: checked = !checked
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 5
+ visible: accountPlanButton.checked
+
+ Repeater {
+ model: root.accountPreview()
+
+ LabelValueRow {
+ required property var modelData
+ label: qsTr("%1. %2 · %3").arg(modelData.order + 1).arg(modelData.role).arg(modelData.action)
+ value: modelData.accountId ? modelData.accountId : qsTr("Assigned by wallet")
+ valueWrapAnywhere: true
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ implicitHeight: warningTextItem.implicitHeight + 20
+ radius: 6
+ color: root.theme.colors.panelBg
+ border.color: root.theme.colors.ctaBg
+ visible: root.warningText().length > 0
+
+ Text {
+ id: warningTextItem
+ anchors.fill: parent
+ anchors.margins: 10
+ text: root.warningText()
+ color: root.theme.colors.textPrimary
+ font.pixelSize: 12
+ wrapMode: Text.Wrap
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ SubmittedTransaction {
+ Layout.fillWidth: true
+ title: qsTr("Position submitted")
+ transactionId: root.transactionId
+ visible: root.transactionId.length > 0
+ }
+
+ AmmPrimaryButton {
+ Layout.fillWidth: true
+ Layout.minimumHeight: 56
+ theme: root.theme
+ text: root.submitting
+ ? qsTr("Submitting…")
+ : root.contextLoading ? qsTr("Loading…")
+ : root.poolCreationPending ? qsTr("Waiting for pool")
+ : root.missingPool ? qsTr("Create pool") : qsTr("Add liquidity")
+ enabled: root.canConfirm
+ onClicked: root.confirmationRequested(root.submissionSnapshot())
+ }
+ }
+
+ component LabelValueRow: RowLayout {
+ required property string label
+ required property string value
+ property bool valueWrapAnywhere: false
+ Layout.fillWidth: true
+ spacing: 12
+
+ Text {
+ text: parent.label
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 12
+ Layout.fillWidth: true
+ wrapMode: Text.Wrap
+ }
+
+ Text {
+ text: parent.value
+ color: root.theme.colors.textPrimary
+ font.pixelSize: 12
+ font.weight: Font.Medium
+ horizontalAlignment: Text.AlignRight
+ wrapMode: parent.valueWrapAnywhere ? Text.WrapAnywhere : Text.Wrap
+ Layout.maximumWidth: root.compact ? 190 : 280
+ }
+ }
+
+ component PriceRatioAdjustment: RowLayout {
+ id: ratioAdjustment
+
+ required property string amount
+ required property string fieldName
+ required property bool invalid
+
+ signal edited(string value)
+
+ implicitWidth: 142
+ implicitHeight: 24
+ spacing: 6
+
+ Text {
+ text: qsTr("Price")
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 11
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 24
+ radius: 6
+ color: root.theme.colors.panelBg
+ border.color: ratioAdjustment.invalid
+ ? root.theme.colors.error
+ : ratioInput.activeFocus
+ ? root.theme.colors.ctaBg
+ : root.theme.colors.borderStrong
+ border.width: 1
+
+ TextInput {
+ id: ratioInput
+
+ objectName: ratioAdjustment.fieldName
+ anchors.fill: parent
+ anchors.leftMargin: 8
+ anchors.rightMargin: 8
+ text: ratioAdjustment.amount
+ enabled: ratioAdjustment.enabled
+ color: enabled ? root.theme.colors.textPrimary
+ : root.theme.colors.textSecondary
+ font.pixelSize: 12
+ selectionColor: root.theme.colors.selection
+ selectedTextColor: root.theme.colors.textPrimary
+ horizontalAlignment: Text.AlignRight
+ verticalAlignment: Text.AlignVCenter
+ inputMethodHints: Qt.ImhFormattedNumbersOnly
+ maximumLength: 80
+ validator: RegularExpressionValidator {
+ regularExpression: /[0-9]*([.][0-9]*)?/
+ }
+ Accessible.name: qsTr("Initial price ratio amount")
+ onTextEdited: ratioAdjustment.edited(text)
+ }
+ }
+ }
+
+ function tokenById(tokenId) {
+ for (var i = 0; i < root.tokens.length; ++i) {
+ if (root.tokens[i].definitionId === tokenId)
+ return root.tokens[i]
+ }
+ return root.emptyToken
+ }
+
+ function selectableTokenIds() {
+ var result = []
+ for (var i = 0; i < root.tokens.length; ++i) {
+ if (root.tokens[i].selectable === true)
+ result.push(root.tokens[i].definitionId)
+ }
+ return result
+ }
+
+ function isBase58TokenId(tokenId) {
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(String(tokenId || ""))
+ }
+
+ function resolveToken(side, value) {
+ var tokenId = String(value || "").trim()
+ root.tokenResolutionError = ""
+ root.tokenResolutionErrorSide = ""
+ root.tokenResolutionMessage = ""
+ if (!root.isBase58TokenId(tokenId)) {
+ root.tokenResolutionError = root.issueText("invalid_token_id")
+ root.tokenResolutionErrorSide = side
+ return
+ }
+
+ var current = root.tokenById(tokenId)
+ if (current.definitionId === tokenId) {
+ if (current.selectable === true)
+ root.selectToken(side, tokenId)
+ else {
+ root.tokenResolutionError = root.issueText(current.code || current.status)
+ root.tokenResolutionErrorSide = side
+ }
+ return
+ }
+ root.resolvingTokenId = tokenId
+ root.resolvingTokenSide = side
+ root.tokenResolveRequested(tokenId)
+ }
+
+ function finishTokenResolution(finalResponse) {
+ if (!root.resolvingToken)
+ return
+ var token = root.tokenById(root.resolvingTokenId)
+ if (!token || !token.definitionId) {
+ if (finalResponse === true) {
+ var currentStatus = String(root.newPositionContext.status || "")
+ var code = currentStatus !== "ready" && currentStatus !== "no_wallet"
+ && currentStatus !== "loading"
+ ? root.newPositionContext.code || currentStatus
+ : "token_definition_unreadable"
+ root.failTokenResolution(code)
+ }
+ return
+ }
+ var status = String(root.newPositionContext.status || "")
+ if (status === "loading")
+ return
+ if (status !== "ready" && status !== "no_wallet") {
+ root.failTokenResolution(root.newPositionContext.code || status)
+ return
+ }
+ var side = root.resolvingTokenSide
+ root.resolvingTokenId = ""
+ root.resolvingTokenSide = ""
+ if (token.selectable !== true) {
+ root.tokenResolutionError = root.issueText(token.code || token.status)
+ root.tokenResolutionErrorSide = side
+ return
+ }
+
+ root.tokenResolutionMessage = qsTr("%1 - raw supply %2")
+ .arg(token.name || root.shortId(token.definitionId))
+ .arg(AmountMath.formatRaw(token.totalSupplyRaw || "0", 0))
+ root.selectToken(side, token.definitionId)
+ }
+
+ function failTokenResolution(code) {
+ if (!root.resolvingToken)
+ return
+ var side = root.resolvingTokenSide
+ root.resolvingTokenId = ""
+ root.resolvingTokenSide = ""
+ root.tokenResolutionError = root.issueText(code)
+ root.tokenResolutionErrorSide = side
+ }
+
+ function ensurePair() {
+ var status = String(root.newPositionContext.status || "")
+ if (status !== "ready" && status !== "no_wallet")
+ return
+ var previousA = root.selectedTokenAId
+ var previousB = root.selectedTokenBId
+ var selectable = root.selectableTokenIds()
+ if (selectable.length === 0) {
+ root.selectedTokenAId = ""
+ root.selectedTokenBId = ""
+ if (previousA.length > 0 || previousB.length > 0)
+ root.resetPairDraft()
+ return
+ }
+ if (selectable.indexOf(root.selectedTokenAId) < 0)
+ root.selectedTokenAId = selectable[0]
+ if (selectable.indexOf(root.selectedTokenBId) < 0
+ || root.selectedTokenBId === root.selectedTokenAId) {
+ root.selectedTokenBId = selectable.length > 1 ? selectable[1] : ""
+ }
+ if (root.selectedTokenAId !== previousA || root.selectedTokenBId !== previousB)
+ root.resetPairDraft()
+ else if (root.hasPair)
+ root.requestQuote(true)
+ }
+
+ function selectToken(side, tokenId) {
+ if (tokenId.length === 0)
+ return
+ root.tokenResolutionError = ""
+ root.tokenResolutionErrorSide = ""
+ if (side === "A") {
+ if (tokenId === root.selectedTokenBId)
+ root.selectedTokenBId = root.selectedTokenAId
+ root.selectedTokenAId = tokenId
+ } else {
+ if (tokenId === root.selectedTokenAId)
+ root.selectedTokenAId = root.selectedTokenBId
+ root.selectedTokenBId = tokenId
+ }
+ root.resetPairDraft()
+ }
+
+ function swapTokens() {
+ var tokenId = root.selectedTokenAId
+ root.selectedTokenAId = root.selectedTokenBId
+ root.selectedTokenBId = tokenId
+ var amount = root.amountA
+ root.amountA = root.amountB
+ root.amountB = amount
+ var minimum = root.minimumAmountARaw
+ root.minimumAmountARaw = root.minimumAmountBRaw
+ root.minimumAmountBRaw = minimum
+ var priceAmount = root.priceAmountA
+ root.priceAmountA = root.priceAmountB
+ root.priceAmountB = priceAmount
+ root.noteDraftChanged()
+ root.requestQuote(true)
+ }
+
+ function resetPairDraft() {
+ root.confirmedPoolStatus = ""
+ root.activePoolQuote = ({})
+ root.amountA = ""
+ root.amountB = ""
+ root.priceAmountA = "1"
+ root.priceAmountB = "1"
+ root.minimumAmountARaw = ""
+ root.minimumAmountBRaw = ""
+ root.localErrors = []
+ root.noteDraftChanged()
+ root.requestQuote(true)
+ }
+
+ function acceptPoolActivation(quote) {
+ if (!quote || quote.schema !== "new-position.v1"
+ || quote.status !== "ok"
+ || quote.poolStatus !== "active_pool"
+ || !root.quoteMatchesSelectedPair(quote)) {
+ return false
+ }
+ root.confirmedPoolStatus = "active_pool"
+ root.activePoolQuote = quote
+ root.amountA = ""
+ root.amountB = ""
+ root.minimumAmountARaw = ""
+ root.minimumAmountBRaw = ""
+ root.localErrors = []
+ return true
+ }
+
+ function noteDraftChanged() {
+ root.draftChanged()
+ }
+
+ function effectivePoolStatus() {
+ if (root.quoteStale || !root.quoteMatchesPair())
+ return root.confirmedPoolStatus
+ var status = String(root.quotePayload.poolStatus || "")
+ if (status === "active_pool" || status === "missing_pool")
+ return status
+ if (root.quotePayload.code === "fee_tier_mismatch")
+ return "active_pool"
+ return root.confirmedPoolStatus
+ }
+
+ function rememberPoolStatus() {
+ if (!root.quoteMatchesPair())
+ return
+ var status = String(root.quotePayload.poolStatus || "")
+ if (status === "active_pool" || status === "missing_pool")
+ root.confirmedPoolStatus = status
+ else if (root.quotePayload.code === "fee_tier_mismatch")
+ root.confirmedPoolStatus = "active_pool"
+ }
+
+ function rememberActivePoolQuote() {
+ if (root.quotePayload.status !== "ok"
+ || root.quotePayload.poolStatus !== "active_pool"
+ || !root.quoteMatchesPair()) {
+ return
+ }
+ var reserveA = String(root.quotePayload.reserveARaw || "")
+ var reserveB = String(root.quotePayload.reserveBRaw || "")
+ if (AmountMath.isUnsigned(reserveA) && reserveA !== "0"
+ && AmountMath.isUnsigned(reserveB) && reserveB !== "0") {
+ root.activePoolQuote = root.quotePayload
+ }
+ }
+
+ function knownPoolFeeBps() {
+ var direct = root.feeBpsFromQuote(root.quotePayload)
+ if (direct > 0)
+ return direct
+ if (root.quoteMatchesSelectedPair(root.activePoolQuote))
+ return Number(root.activePoolQuote.poolFeeBps || 0)
+ return 0
+ }
+
+ function feeBpsFromQuote(quote) {
+ if (!root.quoteMatchesSelectedPair(quote))
+ return 0
+ var direct = Number(quote.poolFeeBps || 0)
+ if (direct > 0)
+ return direct
+ var errors = quote.errors || []
+ for (var i = 0; i < errors.length; ++i) {
+ var value = Number(errors[i].details ? errors[i].details.poolFeeBps : 0)
+ if (value > 0)
+ return value
+ }
+ return 0
+ }
+
+ function quoteMatchesPair() {
+ return root.quoteMatchesSelectedPair(root.quotePayload)
+ }
+
+ function quoteMatchesSelectedPair(quote) {
+ var tokenAId = String(quote.tokenAId || "")
+ var tokenBId = String(quote.tokenBId || "")
+ return root.hasPair
+ && ((tokenAId === root.selectedTokenAId && tokenBId === root.selectedTokenBId)
+ || (tokenAId === root.selectedTokenBId && tokenBId === root.selectedTokenAId))
+ }
+
+ function selectFee(feeBps) {
+ root.selectedFeeBps = feeBps
+ root.noteDraftChanged()
+ root.requestQuote(true)
+ }
+
+ function feeDisabledReason(tier) {
+ if (tier.enabled === false)
+ return tier.disabledReason || qsTr("This fee tier is unavailable.")
+ if (root.poolFeeBps > 0 && Number(tier.feeBps) !== root.poolFeeBps)
+ return qsTr("Existing pool uses %1. Fee tier is fixed for this pair.")
+ .arg(root.feeLabel(root.poolFeeBps))
+ return ""
+ }
+
+ function feeLabel(feeBps) {
+ if (feeBps === 1)
+ return "0.01%"
+ if (feeBps === 5)
+ return "0.05%"
+ if (feeBps === 30)
+ return "0.30%"
+ if (feeBps === 100)
+ return "1.00%"
+ return root.formatBps(feeBps)
+ }
+
+ function buildQuoteRequest() {
+ var errors = []
+ if (!root.hasPair) {
+ errors.push(root.localIssue("token_pair_required", ["tokenAId", "tokenBId"]))
+ root.localErrors = errors
+ return { "ok": false, "errors": errors, "request": ({}) }
+ }
+
+ var canonicalAId = root.displayIsCanonical ? root.selectedTokenAId : root.selectedTokenBId
+ var canonicalBId = root.displayIsCanonical ? root.selectedTokenBId : root.selectedTokenAId
+ var request = {
+ "schema": "new-position.v1",
+ "tokenAId": canonicalAId,
+ "tokenBId": canonicalBId,
+ "feeBps": root.selectedFeeBps
+ }
+
+ if (root.activePool) {
+ var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA)
+ var parsedB = AmountMath.parseHuman(root.amountB, root.decimalsB)
+ if (!parsedA.ok)
+ errors.push(root.localIssue(parsedA.code, ["amountA"]))
+ if (!parsedB.ok)
+ errors.push(root.localIssue(parsedB.code, ["amountB"]))
+ if (errors.length === 0) {
+ request.maxAmountARaw = root.displayIsCanonical ? parsedA.raw : parsedB.raw
+ request.maxAmountBRaw = root.displayIsCanonical ? parsedB.raw : parsedA.raw
+ request.slippageBps = root.slippageBps
+ }
+ } else {
+ var priceFromAmounts = false
+ var price = AmountMath.ratioToQ64(root.priceAmountA,
+ root.priceAmountB,
+ root.canonicalDecimalsA,
+ root.canonicalDecimalsB,
+ root.displayIsCanonical)
+ if (!price.ok)
+ errors.push(root.localIssue(price.code, ["initialPrice"]))
+ if (root.missingPool && root.minimumAmountARaw.length > 0) {
+ var parsedMissingA = AmountMath.parseHuman(root.amountA, root.decimalsA)
+ var parsedMissingB = AmountMath.parseHuman(root.amountB, root.decimalsB)
+ if (!parsedMissingA.ok)
+ errors.push(root.localIssue(parsedMissingA.code, ["amountA"]))
+ if (!parsedMissingB.ok)
+ errors.push(root.localIssue(parsedMissingB.code, ["amountB"]))
+ if (parsedMissingA.ok && parsedMissingB.ok) {
+ if (AmountMath.compare(parsedMissingA.raw, root.minimumAmountARaw) < 0)
+ errors.push(root.localIssue("amount_too_low", ["amountA"]))
+ if (AmountMath.compare(parsedMissingB.raw, root.minimumAmountBRaw) < 0)
+ errors.push(root.localIssue("amount_too_low", ["amountB"]))
+ var pairedB = AmountMath.pairAmount(parsedMissingA.raw,
+ true,
+ root.decimalsA,
+ root.decimalsB,
+ root.priceAmountA,
+ root.priceAmountB)
+ var pairedA = AmountMath.pairAmount(parsedMissingB.raw,
+ false,
+ root.decimalsA,
+ root.decimalsB,
+ root.priceAmountA,
+ root.priceAmountB)
+ var matchesA = pairedA.ok
+ && AmountMath.normalize(pairedA.raw)
+ === AmountMath.normalize(parsedMissingA.raw)
+ var matchesB = pairedB.ok
+ && AmountMath.normalize(pairedB.raw)
+ === AmountMath.normalize(parsedMissingB.raw)
+ if (!matchesA && !matchesB) {
+ errors.push(root.localIssue("deposit_ratio_mismatch", ["amountA", "amountB"]))
+ }
+ if (errors.length === 0) {
+ request.amountARaw = root.displayIsCanonical
+ ? parsedMissingA.raw : parsedMissingB.raw
+ request.amountBRaw = root.displayIsCanonical
+ ? parsedMissingB.raw : parsedMissingA.raw
+ var actualPrice = AmountMath.ratioToQ64(root.amountA,
+ root.amountB,
+ root.canonicalDecimalsA,
+ root.canonicalDecimalsB,
+ root.displayIsCanonical)
+ if (actualPrice.ok) {
+ request.initialPriceRealRaw = actualPrice.raw
+ priceFromAmounts = true
+ } else {
+ errors.push(root.localIssue(actualPrice.code, ["initialPrice"]))
+ }
+ }
+ }
+ }
+ if (price.ok && !priceFromAmounts)
+ request.initialPriceRealRaw = price.raw
+
+ if (!root.missingPool) {
+ var probeA = root.probeRaw(root.tokenA, root.decimalsA)
+ var probeB = root.probeRaw(root.tokenB, root.decimalsB)
+ request.maxAmountARaw = root.displayIsCanonical ? probeA : probeB
+ request.maxAmountBRaw = root.displayIsCanonical ? probeB : probeA
+ request.slippageBps = root.slippageBps
+ }
+ }
+
+ root.localErrors = errors
+ return { "ok": errors.length === 0, "errors": errors, "request": request }
+ }
+
+ function requestQuote(immediate) {
+ if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) {
+ root.localErrors = []
+ return
+ }
+ root.quoteRequested(immediate, root.buildQuoteRequest())
+ }
+
+ function probeRaw(token, decimals) {
+ var balance = String(token.balanceRaw || "0")
+ var simulated = AmountMath.multiply(AmountMath.pow10(decimals), "1000")
+ if (AmountMath.isUnsigned(balance) && AmountMath.compare(balance, simulated) > 0)
+ return balance
+ return simulated
+ }
+
+ function poolProbeRequest(request) {
+ var probe = {}
+ for (var field in request)
+ probe[field] = request[field]
+ var amountA = root.probeRaw(root.tokenA, root.decimalsA)
+ var amountB = root.probeRaw(root.tokenB, root.decimalsB)
+ probe.maxAmountARaw = root.displayIsCanonical ? amountA : amountB
+ probe.maxAmountBRaw = root.displayIsCanonical ? amountB : amountA
+ probe.slippageBps = root.slippageBps
+ return probe
+ }
+
+ function formatBps(value) {
+ return qsTr("%1%").arg(AmountMath.formatRaw(String(value), 2))
+ }
+
+ function localIssue(code, fields) {
+ return { "code": code, "blockingFields": fields, "details": ({}) }
+ }
+
+ function canonicalFieldToDisplay(field) {
+ if (field === "tokenAId")
+ return root.displayIsCanonical ? "tokenAId" : "tokenBId"
+ if (field === "tokenBId")
+ return root.displayIsCanonical ? "tokenBId" : "tokenAId"
+ if (field === "maxAmountARaw")
+ return root.displayIsCanonical ? "amountA" : "amountB"
+ if (field === "maxAmountBRaw")
+ return root.displayIsCanonical ? "amountB" : "amountA"
+ if (field === "amountARaw")
+ return root.displayIsCanonical ? "amountA" : "amountB"
+ if (field === "amountBRaw")
+ return root.displayIsCanonical ? "amountB" : "amountA"
+ if (field === "initialPriceRealRaw")
+ return "initialPrice"
+ return field
+ }
+
+ function fieldError(field) {
+ var collections = [root.localErrors, root.currentQuoteErrors()]
+ for (var c = 0; c < collections.length; ++c) {
+ for (var i = 0; i < collections[c].length; ++i) {
+ var fields = collections[c][i].blockingFields || []
+ for (var f = 0; f < fields.length; ++f) {
+ if (root.canonicalFieldToDisplay(fields[f]) === field)
+ return root.issueText(collections[c][i].code)
+ }
+ }
+ }
+ return ""
+ }
+
+ function fieldHasError(field) {
+ return root.fieldError(field).length > 0
+ }
+
+ function tokenHasError(side) {
+ if (root.tokenResolutionError.length > 0
+ && root.tokenResolutionErrorSide === side) {
+ return true
+ }
+ return root.fieldHasError(side === "A" ? "tokenAId" : "tokenBId")
+ }
+
+ function formErrorText() {
+ if (root.tokenResolutionError.length > 0)
+ return root.tokenResolutionError
+ if (root.submitError.length > 0)
+ return root.submitError
+ var collections = [root.localErrors, root.currentQuoteErrors()]
+ for (var c = 0; c < collections.length; ++c) {
+ for (var i = 0; i < collections[c].length; ++i) {
+ var code = String(collections[c][i].code || "")
+ if (code.length > 0)
+ return root.issueText(code)
+ }
+ }
+ return root.quoteError()
+ }
+
+ function currentQuoteErrors() {
+ return !root.quoteStale && root.quoteMatchesPair()
+ ? root.quotePayload.errors || [] : []
+ }
+
+ function issueText(code) {
+ var messages = {
+ "amount_required": qsTr("Enter a value."),
+ "amount_must_be_positive": qsTr("Value must be greater than zero."),
+ "invalid_amount_format": qsTr("Use plain dot-decimal format."),
+ "invalid_amount_precision": qsTr("Token amounts must use whole raw units."),
+ "invalid_raw_amount": qsTr("Value is outside the supported range."),
+ "amount_exceeds_balance": qsTr("Amount exceeds the selected holding balance."),
+ "amount_too_low": qsTr("Value is too low for this pool."),
+ "invalid_token_id": qsTr("Enter a valid base58 TokenDefinition ID."),
+ "deposit_ratio_mismatch": qsTr("Deposit amounts must match the initial price."),
+ "minimum_lp_zero": qsTr("Slippage leaves no minimum LP output."),
+ "invalid_slippage": qsTr("Slippage must be between 0% and 50%."),
+ "fee_tier_mismatch": qsTr("Select the existing pool fee tier."),
+ "no_wallet": qsTr("Connect a wallet to submit this position."),
+ "wallet_unavailable": qsTr("Wallet is unavailable."),
+ "wallet_submission_failed": qsTr("Wallet submission failed. Review and retry manually."),
+ "signature_rejected": qsTr("Wallet approval was rejected."),
+ "quote_changed": qsTr("Pool or wallet state changed. Review the refreshed quote."),
+ "quote_not_submittable": qsTr("Current quote cannot be submitted."),
+ "submit_in_progress": qsTr("A submission is already in progress."),
+ "transaction_deadline_expired": qsTr("Wallet approval expired. Retry to create a fresh request."),
+ "high_slippage": qsTr("High slippage tolerance."),
+ "token_definition_unreadable": qsTr("A token definition could not be read."),
+ "token_program_mismatch": qsTr("Token belongs to a different TokenProgram."),
+ "token_not_fungible": qsTr("Token is not a public fungible token."),
+ "backend_error": qsTr("Position backend failed. Refresh and retry."),
+ "network_unknown": qsTr("Network identity could not be verified. Refresh and retry."),
+ "network_mismatch": qsTr("Connected wallet uses a different network."),
+ "config_missing": qsTr("Network configuration is missing."),
+ "account_read_failed": qsTr("Required on-chain state could not be read."),
+ "pool_unavailable": qsTr("Pool state is unavailable."),
+ "config_unavailable": qsTr("AMM configuration is unavailable."),
+ "same_token_pair": qsTr("Select two different tokens."),
+ "token_pair_required": qsTr("Select two tokens.")
+ }
+ return messages[code] || qsTr("Position quote is unavailable (%1).").arg(code || "unknown")
+ }
+
+ function editActiveAmount(side, value) {
+ if (side === "A")
+ root.amountA = value
+ else
+ root.amountB = value
+
+ root.localErrors = []
+ root.noteDraftChanged()
+ }
+
+ function finishActiveAmount(side, value) {
+ var decimals = side === "A" ? root.decimalsA : root.decimalsB
+ if (side === "A")
+ root.amountA = value
+ else
+ root.amountB = value
+
+ var reserveA = root.poolReserve("A")
+ var reserveB = root.poolReserve("B")
+ var parsed = AmountMath.parseHuman(value, decimals)
+ if (parsed.ok && reserveA !== "" && reserveB !== ""
+ && reserveA !== "0" && reserveB !== "0") {
+ if (side === "A") {
+ var rawB = AmountMath.mulDivFloor(parsed.raw, reserveB, reserveA)
+ root.amountB = AmountMath.formatRaw(rawB, root.decimalsB)
+ } else {
+ var rawA = AmountMath.mulDivFloor(parsed.raw, reserveA, reserveB)
+ root.amountA = AmountMath.formatRaw(rawA, root.decimalsA)
+ }
+ }
+ root.requestQuote(true)
+ }
+
+ function useMaximum() {
+ var reserveA = root.poolReserve("A")
+ var reserveB = root.poolReserve("B")
+ if (!reserveA || !reserveB || reserveA === "0" || reserveB === "0")
+ return
+ var balanceA = String(root.tokenA.balanceRaw || "0")
+ var balanceB = String(root.tokenB.balanceRaw || "0")
+ var fitA = AmountMath.mulDivFloor(balanceB, reserveA, reserveB)
+ var rawA = AmountMath.compare(balanceA, fitA) < 0 ? balanceA : fitA
+ var rawB = AmountMath.mulDivFloor(rawA, reserveB, reserveA)
+ root.amountA = AmountMath.formatRaw(rawA, root.decimalsA)
+ root.amountB = AmountMath.formatRaw(rawB, root.decimalsB)
+ root.noteDraftChanged()
+ root.requestQuote(true)
+ }
+
+ function editPrice(side, value) {
+ if (side === "A")
+ root.priceAmountA = value
+ else
+ root.priceAmountB = value
+ root.minimumAmountARaw = ""
+ root.minimumAmountBRaw = ""
+ root.amountA = ""
+ root.amountB = ""
+ root.noteDraftChanged()
+ root.requestQuote(false)
+ }
+
+ function editMissingAmount(side, value) {
+ if (side === "A")
+ root.amountA = value
+ else
+ root.amountB = value
+
+ root.localErrors = []
+ root.noteDraftChanged()
+ }
+
+ function finishMissingAmount(side, value) {
+ var decimals = side === "A" ? root.decimalsA : root.decimalsB
+ if (side === "A")
+ root.amountA = value
+ else
+ root.amountB = value
+
+ var parsed = AmountMath.parseHuman(value, decimals)
+ if (parsed.ok) {
+ var paired = AmountMath.pairAmount(parsed.raw,
+ side === "A",
+ root.decimalsA,
+ root.decimalsB,
+ root.priceAmountA,
+ root.priceAmountB)
+ if (paired.ok) {
+ if (side === "A")
+ root.amountB = AmountMath.formatRaw(paired.raw, root.decimalsB)
+ else
+ root.amountA = AmountMath.formatRaw(paired.raw, root.decimalsA)
+ }
+ }
+ root.requestQuote(true)
+ }
+
+ function applyQuoteSideEffects() {
+ if (root.quoteStale)
+ return
+ if (root.poolFeeBps > 0 && root.selectedFeeBps !== root.poolFeeBps) {
+ root.selectedFeeBps = root.poolFeeBps
+ if (root.amountA.length === 0 && root.amountB.length === 0) {
+ root.amountA = AmountMath.formatRaw(
+ root.probeRaw(root.tokenA, root.decimalsA), root.decimalsA)
+ root.amountB = AmountMath.formatRaw(
+ root.probeRaw(root.tokenB, root.decimalsB), root.decimalsB)
+ }
+ root.requestQuote(true)
+ return
+ }
+
+ if (root.quotePayload.status !== "ok")
+ return
+
+ if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) {
+ root.amountA = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "A"), root.decimalsA)
+ root.amountB = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "B"), root.decimalsB)
+ }
+
+ if (root.missingPool) {
+ var rawA = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "A")
+ var rawB = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "B")
+ var minimumA = root.displayRaw("minimumAmountARaw", "minimumAmountBRaw", "A")
+ var minimumB = root.displayRaw("minimumAmountARaw", "minimumAmountBRaw", "B")
+ root.minimumAmountARaw = minimumA.length > 0 ? minimumA : rawA
+ root.minimumAmountBRaw = minimumB.length > 0 ? minimumB : rawB
+ if (rawA.length > 0 && rawB.length > 0) {
+ root.amountA = AmountMath.formatRaw(rawA, root.decimalsA)
+ root.amountB = AmountMath.formatRaw(rawB, root.decimalsB)
+ }
+ }
+ }
+
+ function displayRaw(canonicalAField, canonicalBField, side) {
+ return root.displayQuoteRaw(root.quotePayload, canonicalAField, canonicalBField, side)
+ }
+
+ function displayQuoteRaw(quote, canonicalAField, canonicalBField, side) {
+ if (!root.quoteMatchesSelectedPair(quote))
+ return ""
+ if (side === "A")
+ return String(quote[root.displayIsCanonical ? canonicalAField : canonicalBField] || "")
+ return String(quote[root.displayIsCanonical ? canonicalBField : canonicalAField] || "")
+ }
+
+ function poolReserve(side) {
+ var reserve = root.displayRaw("reserveARaw", "reserveBRaw", side)
+ if (AmountMath.isUnsigned(reserve) && reserve !== "0")
+ return reserve
+ if (!root.quoteMatchesSelectedPair(root.activePoolQuote))
+ return ""
+ return root.displayQuoteRaw(root.activePoolQuote, "reserveARaw", "reserveBRaw", side)
+ }
+
+ function quoteAmount(canonicalAField, canonicalBField, side) {
+ var token = side === "A" ? root.tokenA : root.tokenB
+ var decimals = side === "A" ? root.decimalsA : root.decimalsB
+ var raw = root.displayRaw(canonicalAField, canonicalBField, side)
+ return raw.length > 0
+ ? qsTr("%1 %2").arg(AmountMath.formatRaw(raw, decimals)).arg(root.shortTokenName(token))
+ : "—"
+ }
+
+ function depositSummary() {
+ var amountA = root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A")
+ var amountB = root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B")
+ return amountA + " + " + amountB
+ }
+
+ function initialPriceText(inverse) {
+ var price = inverse ? root.inverseInitialPrice : root.initialPrice
+ if (price.length === 0)
+ return "—"
+ var from = inverse ? root.tokenB : root.tokenA
+ var to = inverse ? root.tokenA : root.tokenB
+ return qsTr("1 %1 = %2 %3")
+ .arg(root.shortTokenName(from))
+ .arg(price)
+ .arg(root.shortTokenName(to))
+ }
+
+ function rawLpText(raw) {
+ return raw !== undefined && raw !== null && String(raw).length > 0
+ ? qsTr("%1 raw LP").arg(String(raw)) : "—"
+ }
+
+ function activePriceValue() {
+ var priceRaw = String(root.quotePayload.initialPriceRealRaw || "")
+ if (priceRaw.length === 0 && root.quoteMatchesSelectedPair(root.activePoolQuote))
+ priceRaw = String(root.activePoolQuote.initialPriceRealRaw || "")
+ return AmountMath.priceFromQ64(priceRaw,
+ root.canonicalDecimalsA,
+ root.canonicalDecimalsB,
+ root.displayIsCanonical)
+ }
+
+ function activePriceText() {
+ var price = root.activePriceValue()
+ if (price.length === 0)
+ return ""
+ return qsTr("1 %1 = %2 %3")
+ .arg(root.shortTokenName(root.tokenA))
+ .arg(price)
+ .arg(root.shortTokenName(root.tokenB))
+ }
+
+ function accountPreview() {
+ return !root.quoteStale && root.quoteMatchesPair()
+ ? root.quotePayload.accountPreview || [] : []
+ }
+
+ function quoteError() {
+ if (root.quoteLoading || root.quoteStale)
+ return ""
+ if (root.quotePayload.status === "error")
+ return root.issueText(root.quotePayload.code)
+ return ""
+ }
+
+ function warningText() {
+ var warnings = !root.quoteStale && root.quoteMatchesPair()
+ ? root.quotePayload.warnings || [] : []
+ if (warnings.length === 0)
+ warnings = root.newPositionContext.warnings || []
+ return warnings.length > 0 ? root.issueText(warnings[0].code) : ""
+ }
+
+ function submissionSnapshot() {
+ var built = root.buildQuoteRequest()
+ return {
+ "request": built.request,
+ "poolProbeRequest": root.poolProbeRequest(built.request),
+ "quoteHash": String(root.quotePayload.quoteHash || ""),
+ "pairText": qsTr("%1 / %2").arg(root.shortTokenName(root.tokenA)).arg(root.shortTokenName(root.tokenB)),
+ "feeText": root.feeLabel(root.selectedFeeBps),
+ "depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"),
+ "depositBText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B"),
+ "expectedLpText": root.rawLpText(root.quotePayload.expectedLpRaw),
+ "instruction": String(root.quotePayload.instruction || "")
+ }
+ }
+
+ function shortTokenName(token) {
+ if (token && token.name && token.name.length > 0)
+ return token.name
+ return token && token.definitionId ? root.shortId(token.definitionId) : "—"
+ }
+
+ function balanceText(token, decimals) {
+ return AmountMath.formatRaw(String(token.balanceRaw || "0"), decimals)
+ }
+
+ function tokenBalanceDetail(token) {
+ return qsTr("Available %1").arg(root.balanceText(token, 0))
+ }
+
+ function shortId(value) {
+ var text = String(value || "")
+ return text.length > 14 ? text.slice(0, 7) + "…" + text.slice(-5) : text
+ }
+
+ function depositMultiplierText() {
+ var multiplier = root.depositMultiplierValue()
+ var basisPoints = root.depositBasisPointsText()
+ return multiplier.length > 0 ? qsTr("%1 · %2").arg(multiplier).arg(basisPoints) : ""
+ }
+
+ function depositMultiplierValue() {
+ var scale = root.depositScaleValue()
+ return scale.length > 0
+ ? qsTr("%1x minimum").arg(AmountMath.formatRaw(scale, 4)) : ""
+ }
+
+ function depositBasisPointsText() {
+ var scale = root.depositScaleValue()
+ return scale.length > 0 ? qsTr("%1 basis points").arg(scale) : ""
+ }
+
+ function depositScaleValue() {
+ var parsed = AmountMath.parseHuman(root.amountA, root.decimalsA)
+ if (!parsed.ok || !AmountMath.isUnsigned(root.minimumAmountARaw)
+ || root.minimumAmountARaw === "0"
+ || AmountMath.compare(parsed.raw, root.minimumAmountARaw) < 0) {
+ return ""
+ }
+ return AmountMath.divide(AmountMath.multiply(parsed.raw, "10000"),
+ root.minimumAmountARaw).quotient
+ }
+
+ function minimumAmountText(side) {
+ var raw = side === "A" ? root.minimumAmountARaw : root.minimumAmountBRaw
+ var decimals = side === "A" ? root.decimalsA : root.decimalsB
+ return raw.length > 0
+ ? qsTr("Min %1").arg(AmountMath.formatRaw(raw, decimals)) : ""
+ }
+
+ function contextStatusText() {
+ var network = String(root.newPositionContext.networkId || "")
+ if (root.newPositionContext.status === "no_wallet")
+ return qsTr("%1 · simulation only").arg(network || qsTr("Wallet disconnected"))
+ if (root.newPositionContext.status === "ready")
+ return qsTr("%1 · wallet ready").arg(network)
+ return network.length > 0 ? network : qsTr("Loading network")
+ }
+
+ function contextBlocksForm() {
+ var status = String(root.newPositionContext.status || "")
+ return status !== "" && status !== "ready" && status !== "no_wallet" && status !== "loading"
+ }
+
+ function contextErrorText() {
+ return root.issueText(root.newPositionContext.code || root.newPositionContext.status)
+ }
+}
diff --git a/apps/amm/qml/components/liquidity/PoolPositionSummary.qml b/apps/amm/qml/components/liquidity/PoolPositionSummary.qml
deleted file mode 100644
index 2466ae7a..00000000
--- a/apps/amm/qml/components/liquidity/PoolPositionSummary.qml
+++ /dev/null
@@ -1,98 +0,0 @@
-import QtQuick 2.15
-import QtQuick.Layouts 1.15
-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.")
-
- color: "#151515"
- implicitHeight: content.implicitHeight + 20
- radius: 8
- border.color: "#303030"
- border.width: 1
-
- ColumnLayout {
- id: content
-
- anchors.fill: parent
- anchors.margins: 10
- spacing: 6
-
- RowLayout {
- spacing: 10
-
- Layout.fillWidth: true
-
- ColumnLayout {
- spacing: 2
-
- Layout.fillWidth: true
-
- Text {
- color: "#E7E1D8"
- font.bold: true
- font.pixelSize: 13
- text: root.poolState.userLpBalance > 0 ? qsTr("Your position") : qsTr("No position")
-
- Layout.fillWidth: true
- }
-
- Text {
- color: "#8E8780"
- font.pixelSize: 11
- text: qsTr("%1 LP tokens").arg(root.poolState.formatInteger(root.poolState.userLpBalance))
- visible: root.poolState.userLpBalance > 0
-
- Layout.fillWidth: true
- }
- }
-
- Rectangle {
- color: "#211914"
- radius: 10
- border.color: "#49301F"
- border.width: 1
-
- Layout.preferredHeight: 24
- Layout.preferredWidth: shareText.implicitWidth + 18
-
- Text {
- id: shareText
-
- anchors.centerIn: parent
- color: "#F2D8C7"
- font.bold: true
- font.pixelSize: 11
- text: root.poolState.userLpBalance > 0 ? root.poolState.formatPoolShare(root.poolState.poolShare) : root.poolState.feeTier
- }
- }
- }
-
- SummaryRow {
- estimated: true
- estimateHelp: root.estimateHelp
- label: qsTr("Owned")
- value: qsTr("%1 + %2").arg(root.poolState.formatCompactTokenAmount(root.poolState.userOwnedA, root.poolState.tokenA)).arg(root.poolState.formatCompactTokenAmount(root.poolState.userOwnedB, root.poolState.tokenB))
- visible: root.poolState.userLpBalance > 0
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Pool")
- value: qsTr("%1 / %2").arg(root.poolState.formatCompactTokenAmount(root.poolState.reserveA, root.poolState.tokenA)).arg(root.poolState.formatCompactTokenAmount(root.poolState.reserveB, root.poolState.tokenB))
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Fee")
- value: root.poolState.feeTier
-
- Layout.fillWidth: true
- }
- }
-}
diff --git a/apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml b/apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml
deleted file mode 100644
index b916a744..00000000
--- a/apps/amm/qml/components/liquidity/RemoveLiquidityForm.qml
+++ /dev/null
@@ -1,492 +0,0 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
-import "../shared"
-import "../../state"
-
-Rectangle {
- id: root
-
- required property DummyPoolState poolState
-
- property real slippageTolerancePercent: 0.5
- property int burnAmount: 0
- readonly property int maxBurnAmount: root.poolState.clampBurnAmount(root.poolState.userLpBalance)
- readonly property bool hasLpTokens: root.maxBurnAmount > 0
- readonly property int preset25Amount: root.poolState.burnAmountForPercent(25)
- readonly property int preset50Amount: root.poolState.burnAmountForPercent(50)
- readonly property int preset75Amount: root.poolState.burnAmountForPercent(75)
- readonly property real removePercent: root.maxBurnAmount > 0 ? root.burnAmount * 100 / root.maxBurnAmount : 0
- readonly property var preview: root.poolState.removeLiquidityPreview(root.burnAmount)
- readonly property int minTokenAReceived: root.poolState.minReceivedAmount(root.preview.withdrawA, root.slippageTolerancePercent)
- readonly property int minTokenBReceived: root.poolState.minReceivedAmount(root.preview.withdrawB, root.slippageTolerancePercent)
- 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")
-
- signal slippageToleranceChangeRequested(real tolerancePercent)
- signal removeLiquidityRequested(var snapshot)
-
- color: "#00000000"
- implicitHeight: content.implicitHeight
- radius: 0
- border.width: 0
-
- onMaxBurnAmountChanged: {
- if (root.burnAmount > root.maxBurnAmount) {
- root.setBurnAmount(root.maxBurnAmount);
- }
- }
-
- ColumnLayout {
- id: content
-
- anchors.fill: parent
- spacing: 10
-
- Text {
- color: "#F26A21"
- font.pixelSize: 12
- text: qsTr("No LP tokens")
- visible: !root.hasLpTokens
-
- Layout.fillWidth: true
- }
-
- Text {
- color: "#A9A098"
- font.pixelSize: 12
- lineHeight: 1.25
- text: qsTr("Add liquidity first to receive LP tokens before removing from this pool.")
- visible: !root.hasLpTokens
- wrapMode: Text.WordWrap
-
- Layout.fillWidth: true
- }
-
- Rectangle {
- color: root.hasLpTokens ? "#151515" : "#121212"
- radius: 8
- border.color: burnField.activeFocus ? "#F26A21" : "#343434"
- border.width: 1
-
- Layout.fillWidth: true
- Layout.preferredHeight: inputContent.implicitHeight + 20
-
- ColumnLayout {
- id: inputContent
-
- anchors.fill: parent
- anchors.margins: 10
- spacing: 8
-
- RowLayout {
- spacing: 8
-
- Layout.fillWidth: true
-
- Text {
- color: "#A9A098"
- elide: Text.ElideRight
- font.pixelSize: 12
- text: qsTr("LP tokens to burn")
-
- Layout.fillWidth: true
- }
-
- Text {
- color: "#A9A098"
- elide: Text.ElideRight
- font.pixelSize: 11
- horizontalAlignment: Text.AlignRight
- text: qsTr("Available LP: %1").arg(root.poolState.formatInteger(root.poolState.userLpBalance))
-
- Layout.maximumWidth: 170
- }
- }
-
- TextField {
- id: burnField
-
- activeFocusOnTab: root.hasLpTokens
- color: "#E7E1D8"
- enabled: root.hasLpTokens
- font.bold: true
- font.pixelSize: 18
- inputMethodHints: Qt.ImhDigitsOnly
- placeholderText: qsTr("0")
- selectByMouse: true
- selectedTextColor: "#151515"
- selectionColor: "#F26A21"
- text: root.burnAmount > 0 ? String(root.burnAmount) : ""
- validator: RegularExpressionValidator {
- regularExpression: /[0-9]*/
- }
-
- Accessible.name: qsTr("LP tokens to burn")
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onTextEdited: root.setBurnAmount(text)
-
- background: Rectangle {
- border.color: burnField.activeFocus ? "#F26A21" : "#343434"
- border.width: 1
- color: burnField.activeFocus ? "#1F1B18" : "#101010"
- radius: 6
- }
- }
- }
- }
-
- RowLayout {
- spacing: 6
-
- Layout.fillWidth: true
-
- Button {
- id: preset25
-
- activeFocusOnTab: root.hasLpTokens
- enabled: root.hasLpTokens
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("25%")
-
- Accessible.name: qsTr("Remove 25 percent")
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onClicked: root.setBurnPercent(25)
-
- contentItem: Text {
- color: preset25.enabled && (preset25.hovered || preset25.activeFocus || root.preset25Amount === root.burnAmount) ? "#151515" : "#A9A098"
- font.bold: true
- font.pixelSize: 11
- horizontalAlignment: Text.AlignHCenter
- text: preset25.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: preset25.activeFocus || root.preset25Amount === root.burnAmount ? "#F26A21" : "#343434"
- border.width: 1
- color: preset25.pressed ? "#D95C1E" : root.preset25Amount === root.burnAmount ? "#F26A21" : preset25.hovered || preset25.activeFocus ? "#E7E1D8" : "#151515"
- radius: 6
- }
- }
-
- Button {
- id: preset50
-
- activeFocusOnTab: root.hasLpTokens
- enabled: root.hasLpTokens
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("50%")
-
- Accessible.name: qsTr("Remove 50 percent")
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onClicked: root.setBurnPercent(50)
-
- contentItem: Text {
- color: preset50.enabled && (preset50.hovered || preset50.activeFocus || root.preset50Amount === root.burnAmount) ? "#151515" : "#A9A098"
- font.bold: true
- font.pixelSize: 11
- horizontalAlignment: Text.AlignHCenter
- text: preset50.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: preset50.activeFocus || root.preset50Amount === root.burnAmount ? "#F26A21" : "#343434"
- border.width: 1
- color: preset50.pressed ? "#D95C1E" : root.preset50Amount === root.burnAmount ? "#F26A21" : preset50.hovered || preset50.activeFocus ? "#E7E1D8" : "#151515"
- radius: 6
- }
- }
-
- Button {
- id: preset75
-
- activeFocusOnTab: root.hasLpTokens
- enabled: root.hasLpTokens
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("75%")
-
- Accessible.name: qsTr("Remove 75 percent")
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onClicked: root.setBurnPercent(75)
-
- contentItem: Text {
- color: preset75.enabled && (preset75.hovered || preset75.activeFocus || root.preset75Amount === root.burnAmount) ? "#151515" : "#A9A098"
- font.bold: true
- font.pixelSize: 11
- horizontalAlignment: Text.AlignHCenter
- text: preset75.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: preset75.activeFocus || root.preset75Amount === root.burnAmount ? "#F26A21" : "#343434"
- border.width: 1
- color: preset75.pressed ? "#D95C1E" : root.preset75Amount === root.burnAmount ? "#F26A21" : preset75.hovered || preset75.activeFocus ? "#E7E1D8" : "#151515"
- radius: 6
- }
- }
-
- Button {
- id: presetMax
-
- activeFocusOnTab: root.hasLpTokens
- enabled: root.hasLpTokens
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("MAX")
-
- Accessible.name: qsTr("Remove maximum LP balance")
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onClicked: root.setBurnAmount(root.maxBurnAmount)
-
- contentItem: Text {
- color: presetMax.enabled && (presetMax.hovered || presetMax.activeFocus || root.burnAmount === root.maxBurnAmount) ? "#151515" : "#A9A098"
- font.bold: true
- font.pixelSize: 11
- horizontalAlignment: Text.AlignHCenter
- text: presetMax.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: presetMax.activeFocus || root.burnAmount === root.maxBurnAmount ? "#F26A21" : "#343434"
- border.width: 1
- color: presetMax.pressed ? "#D95C1E" : root.burnAmount === root.maxBurnAmount ? "#F26A21" : presetMax.hovered || presetMax.activeFocus ? "#E7E1D8" : "#151515"
- radius: 6
- }
- }
- }
-
- ColumnLayout {
- spacing: 6
-
- Layout.fillWidth: true
-
- RowLayout {
- spacing: 8
-
- Layout.fillWidth: true
-
- Text {
- color: "#A9A098"
- font.pixelSize: 12
- text: qsTr("Pool share to remove")
-
- Layout.fillWidth: true
- }
-
- Text {
- color: "#E7E1D8"
- font.bold: true
- font.pixelSize: 12
- horizontalAlignment: Text.AlignRight
- text: root.poolState.formatPercent(root.removePercent)
-
- Layout.maximumWidth: 72
- }
- }
-
- Slider {
- id: burnSlider
-
- activeFocusOnTab: root.hasLpTokens
- enabled: root.hasLpTokens
- from: 0
- stepSize: 1
- to: 100
- value: root.removePercent
-
- Accessible.name: qsTr("Pool share to remove")
- Accessible.role: Accessible.Slider
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onMoved: root.setBurnPercent(Math.round(value))
-
- background: Rectangle {
- color: "#343434"
- implicitHeight: 4
- radius: 2
- x: burnSlider.leftPadding
- y: burnSlider.topPadding + burnSlider.availableHeight / 2 - height / 2
-
- width: burnSlider.availableWidth
-
- Rectangle {
- color: burnSlider.enabled ? "#F26A21" : "#56504A"
- height: parent.height
- radius: 2
- width: burnSlider.visualPosition * parent.width
- }
- }
-
- handle: Rectangle {
- border.color: burnSlider.activeFocus ? "#E7E1D8" : "#F26A21"
- border.width: 1
- color: burnSlider.enabled ? "#F26A21" : "#56504A"
- height: 18
- radius: 9
- width: 18
- x: burnSlider.leftPadding + burnSlider.visualPosition * (burnSlider.availableWidth - width)
- y: burnSlider.topPadding + burnSlider.availableHeight / 2 - height / 2
- }
- }
- }
-
- SummaryRow {
- estimated: true
- estimateHelp: root.estimateHelp
- label: qsTr("Withdraw %1").arg(root.poolState.tokenA)
- value: root.poolState.formatTokenAmount(root.preview.withdrawA, root.poolState.tokenA)
- visible: root.burnAmount > 0
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- estimated: true
- estimateHelp: root.estimateHelp
- label: qsTr("Withdraw %1").arg(root.poolState.tokenB)
- value: root.poolState.formatTokenAmount(root.preview.withdrawB, root.poolState.tokenB)
- visible: root.burnAmount > 0
-
- Layout.fillWidth: true
- }
-
- SlippageToleranceControl {
- tolerancePercent: root.slippageTolerancePercent
-
- Layout.fillWidth: true
-
- onToleranceChangeRequested: function (tolerancePercent) {
- root.slippageToleranceChangeRequested(tolerancePercent);
- }
- }
-
- SummaryRow {
- label: qsTr("Min %1 received").arg(root.poolState.tokenA)
- value: root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA)
- visible: root.burnAmount > 0
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- label: qsTr("Min %1 received").arg(root.poolState.tokenB)
- value: root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB)
- visible: root.burnAmount > 0
-
- Layout.fillWidth: true
- }
-
- Text {
- color: "#F08A76"
- font.pixelSize: 12
- lineHeight: 1.25
- text: qsTr("Minimum received is 0. Increase amount or lower slippage.")
- visible: root.minReceivedIsZero
- wrapMode: Text.WordWrap
-
- Layout.fillWidth: true
- }
-
- SummaryRow {
- estimated: true
- estimateHelp: root.estimateHelp
- label: qsTr("Position after")
- value: root.poolState.formatPoolShare(root.preview.newUserShare)
- visible: root.burnAmount > 0
-
- Layout.fillWidth: true
- }
-
- Button {
- id: submitButton
-
- activeFocusOnTab: root.hasLpTokens
- enabled: root.canSubmit
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: root.submitButtonText
-
- Accessible.name: submitButton.text
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
- Layout.preferredHeight: 44
-
- onClicked: root.removeLiquidityRequested(root.submitSnapshot())
-
- contentItem: Text {
- color: submitButton.enabled ? "#151515" : "#7D756E"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 13
- horizontalAlignment: Text.AlignHCenter
- text: submitButton.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: submitButton.enabled ? "#F26A21" : "#343434"
- border.width: 1
- color: submitButton.enabled ? submitButton.pressed ? "#D95C1E" : submitButton.hovered || submitButton.activeFocus ? "#FF8A3D" : "#F26A21" : "#181818"
- radius: 6
- }
- }
- }
-
- function setBurnAmount(value) {
- root.burnAmount = root.poolState.clampBurnAmount(value);
- }
-
- function setBurnPercent(percent) {
- root.setBurnAmount(root.poolState.burnAmountForPercent(percent));
- }
-
- function resetForm() {
- root.setBurnAmount(0);
- }
-
- function submitSnapshot() {
- return {
- "action": "remove",
- "burnAmount": root.preview.burnedLp,
- "burnPercent": root.poolState.formatPercent(root.removePercent),
- "burnText": root.poolState.formatLpAmount(root.preview.burnedLp),
- "minTokenAReceived": root.poolState.formatTokenAmount(root.minTokenAReceived, root.poolState.tokenA),
- "minTokenBReceived": root.poolState.formatTokenAmount(root.minTokenBReceived, root.poolState.tokenB),
- "postRemovalShare": root.poolState.formatPoolShare(root.preview.newUserShare),
- "slippageTolerance": root.poolState.formatPercent(root.slippageTolerancePercent),
- "tokenA": root.poolState.tokenA,
- "tokenB": root.poolState.tokenB,
- "withdrawA": root.preview.withdrawA,
- "withdrawB": root.preview.withdrawB,
- "withdrawAText": root.poolState.formatTokenAmount(root.preview.withdrawA, root.poolState.tokenA),
- "withdrawBText": root.poolState.formatTokenAmount(root.preview.withdrawB, root.poolState.tokenB)
- };
- }
-}
diff --git a/apps/amm/qml/components/liquidity/SummaryRow.qml b/apps/amm/qml/components/liquidity/SummaryRow.qml
index 0c340465..849ecb62 100644
--- a/apps/amm/qml/components/liquidity/SummaryRow.qml
+++ b/apps/amm/qml/components/liquidity/SummaryRow.qml
@@ -6,6 +6,7 @@ Item {
property string label: ""
property string value: ""
+ property bool valueWrapAnywhere: false
property bool estimated: false
property string estimateHelp: qsTr("This value is derived from your LP token balance, total LP supply, and current pool reserves.")
@@ -36,14 +37,16 @@ Item {
Text {
color: "#E7E1D8"
- elide: Text.ElideRight
+ elide: root.valueWrapAnywhere ? Text.ElideNone : Text.ElideRight
font.bold: true
font.pixelSize: 12
horizontalAlignment: Text.AlignRight
text: root.value
verticalAlignment: Text.AlignVCenter
+ wrapMode: root.valueWrapAnywhere ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
Layout.maximumWidth: Math.max(178, root.width * 0.55)
+ Layout.preferredWidth: Math.min(implicitWidth, Math.max(178, root.width * 0.55))
}
EstimateInfoButton {
diff --git a/apps/amm/qml/components/liquidity/TokenAmountInput.qml b/apps/amm/qml/components/liquidity/TokenAmountInput.qml
index 120be579..311276cf 100644
--- a/apps/amm/qml/components/liquidity/TokenAmountInput.qml
+++ b/apps/amm/qml/components/liquidity/TokenAmountInput.qml
@@ -1,156 +1,135 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
+pragma ComponentBehavior: Bound
-Rectangle {
+import QtQuick
+
+import "../shared"
+
+AmmTokenAmountSurface {
id: root
- property alias text: amountField.text
+ property string text: ""
property string balance: ""
- property string errorText: ""
property string helperText: ""
- property string label: ""
- property string token: ""
+ property bool showMaxButton: true
+ property var tokenData: null
+ property var tokens: []
+ property string selectedTokenId: ""
+ property bool tokenInvalid: false
+ property bool tokenSelectionEnabled: true
+ property bool editPending: false
+ property string pendingValue: ""
+ property var disabledReasonForCode: function(code) {
+ return qsTr("This token is unavailable (%1).").arg(code || "unknown")
+ }
+ property var detailForToken: function(token) { return "" }
+ property alias popup: tokenModal
+ property alias query: tokenModal.searchText
+ readonly property var rows: tokenModal.rows
signal editingChanged(string value)
+ signal editingCommitted(string value)
signal maxClicked
+ signal tokenSelected(string tokenId)
+ signal tokenEntered(string value)
+
+ amount: root.text
+ supportingText: root.helperText
+ supportingActionText: root.showMaxButton ? qsTr("MAX") : ""
+ accessory: tokenActions
+ accessoryWidth: width < 360 ? 132 : 180
+ accessoryHeight: root.balance.length > 0 ? 58 : 40
+
+ onAmountEdited: function(value) {
+ root.pendingValue = value
+ root.editPending = true
+ root.editingChanged(value)
+ commitTimer.restart()
+ }
+ onAmountEditingFinished: function(value) {
+ root.pendingValue = value
+ root.commitPendingEdit()
+ }
+ onSupportingActionClicked: root.maxClicked()
+ onTextChanged: {
+ if (root.editPending && root.text !== root.pendingValue) {
+ commitTimer.stop()
+ root.editPending = false
+ }
+ }
- color: "#151515"
- implicitHeight: content.implicitHeight + 20
- radius: 8
- border.color: root.errorText.length > 0 ? "#D85F4B" : amountField.activeFocus ? "#F26A21" : "#343434"
- border.width: 1
-
- Accessible.name: root.label
- Accessible.role: Accessible.EditableText
-
- ColumnLayout {
- id: content
-
- anchors.fill: parent
- anchors.margins: 10
- spacing: 8
-
- RowLayout {
- spacing: 8
-
- Layout.fillWidth: true
-
- Text {
- color: "#A9A098"
- elide: Text.ElideRight
- font.pixelSize: 12
- text: root.label
-
- Layout.fillWidth: true
- }
+ Timer {
+ id: commitTimer
- Text {
- color: "#E7E1D8"
- elide: Text.ElideRight
- font.bold: true
- font.pixelSize: 12
- horizontalAlignment: Text.AlignRight
- text: root.token
+ interval: 250
+ repeat: false
+ onTriggered: root.commitPendingEdit()
+ }
- Layout.maximumWidth: 76
- }
+ Component {
+ id: tokenActions
+
+ AmmTokenAccessory {
+ theme: root.theme
+ enabled: root.tokenSelectionEnabled
+ invalid: root.tokenInvalid
+ hasToken: root.tokenData !== null
+ tokenColor: root.tokenColor(root.tokenData)
+ tokenLetter: root.tokenLetter(root.tokenData)
+ tokenText: root.tokenText(root.tokenData)
+ balance: root.balance
+ accessibleName: qsTr("Select %1").arg(root.label)
+ onClicked: tokenModal.open()
}
+ }
- RowLayout {
- spacing: 8
-
- Layout.fillWidth: true
-
- TextField {
- id: amountField
-
- activeFocusOnTab: true
- color: "#E7E1D8"
- font.bold: true
- font.pixelSize: 18
- inputMethodHints: Qt.ImhFormattedNumbersOnly
- placeholderText: qsTr("0")
- selectByMouse: true
- selectedTextColor: "#151515"
- selectionColor: "#F26A21"
- validator: RegularExpressionValidator {
- regularExpression: /[0-9]*([.][0-9]*)?/
- }
-
- Accessible.name: root.label
-
- Layout.fillWidth: true
- Layout.minimumHeight: 44
-
- onTextEdited: root.editingChanged(text)
-
- background: Rectangle {
- border.color: amountField.activeFocus ? "#F26A21" : "#343434"
- border.width: 1
- color: amountField.activeFocus ? "#1F1B18" : "#101010"
- radius: 6
- }
- }
-
- Button {
- id: maxButton
-
- activeFocusOnTab: true
- focusPolicy: Qt.StrongFocus
- hoverEnabled: true
- text: qsTr("MAX")
-
- Accessible.name: qsTr("Use maximum %1 balance").arg(root.token)
-
- Layout.minimumHeight: 44
- Layout.preferredWidth: 58
-
- onClicked: root.maxClicked()
-
- contentItem: Text {
- color: maxButton.activeFocus || maxButton.hovered ? "#151515" : "#F26A21"
- font.bold: true
- font.pixelSize: 11
- horizontalAlignment: Text.AlignHCenter
- text: maxButton.text
- verticalAlignment: Text.AlignVCenter
- }
-
- background: Rectangle {
- border.color: "#F26A21"
- border.width: 1
- color: maxButton.pressed ? "#D95C1E" : maxButton.hovered || maxButton.activeFocus ? "#F26A21" : "#201712"
- radius: 6
- }
- }
+ TokenSelectorModal {
+ id: tokenModal
+
+ theme: root.theme
+ tokens: root.tokens
+ title: qsTr("Select a token")
+ searchPlaceholder: qsTr("Search name or address")
+ popularTitle: qsTr("Quick select")
+ listTitle: qsTr("All tokens")
+ allowCustomEntry: true
+ disabledReasonForCode: root.disabledReasonForCode
+ detailForToken: root.detailForToken
+
+ onTokenSelected: function(token) {
+ root.tokenSelected(String(token.definitionId || token.address || ""))
}
+ onTokenEntered: function(value) { root.tokenEntered(value) }
+ }
- RowLayout {
- spacing: 8
+ function acceptInput(value) {
+ tokenModal.acceptInput(value)
+ }
- Layout.fillWidth: true
+ function commitPendingEdit() {
+ if (!root.editPending)
+ return
+ commitTimer.stop()
+ root.editPending = false
+ root.editingCommitted(root.pendingValue)
+ }
- Text {
- color: root.errorText.length > 0 ? "#F08A76" : root.helperText.length > 0 ? "#F26A21" : "#A9A098"
- elide: Text.ElideRight
- font.pixelSize: 11
- text: root.errorText.length > 0 ? root.errorText : root.helperText
- visible: text.length > 0
+ function tokenText(token) {
+ if (!token)
+ return qsTr("Select token")
+ return String(token.symbol || token.name || root.shortId(root.selectedTokenId))
+ }
- Layout.fillWidth: true
- }
+ function tokenLetter(token) {
+ var text = root.tokenText(token)
+ return token ? String(token.letter || text.charAt(0).toUpperCase()) : ""
+ }
- Text {
- color: "#A9A098"
- elide: Text.ElideRight
- font.pixelSize: 11
- horizontalAlignment: Text.AlignRight
- text: qsTr("Balance %1").arg(root.balance)
+ function tokenColor(token) {
+ return token && token.color ? token.color : root.theme.colors.noTokenCircle
+ }
- Layout.alignment: Qt.AlignRight
- Layout.maximumWidth: 150
- }
- }
+ function shortId(value) {
+ var text = String(value || "")
+ return text.length > 14 ? text.slice(0, 7) + "..." + text.slice(-5) : text
}
}
diff --git a/apps/amm/qml/components/liquidity/TokenSelectorModal.qml b/apps/amm/qml/components/liquidity/TokenSelectorModal.qml
new file mode 100644
index 00000000..c07b7253
--- /dev/null
+++ b/apps/amm/qml/components/liquidity/TokenSelectorModal.qml
@@ -0,0 +1,489 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Popup {
+ id: root
+
+ required property var theme
+ property var tokens: []
+ property string searchText: ""
+ property string title: qsTr("Select a token")
+ property string searchPlaceholder: qsTr("Search tokens")
+ property string popularTitle: qsTr("Popular tokens")
+ property string listTitle: qsTr("Tokens by 24H volume")
+ property bool allowCustomEntry: false
+ property var disabledReasonForCode: function(code) {
+ return qsTr("This token is unavailable (%1).").arg(code || "unknown")
+ }
+ property var detailForToken: function(token) {
+ return token && token.balance !== undefined
+ ? qsTr("Balance %1").arg(token.balance) : ""
+ }
+ readonly property var rows: root.filteredTokens()
+ readonly property var popularTokens: root.selectableTokens().slice(0, 5)
+ readonly property bool compact: root.height < 360
+ readonly property bool showPopular: root.popularTokens.length > 0 && root.height >= 440
+
+ signal tokenSelected(var token)
+ signal tokenEntered(string value)
+
+ parent: Overlay.overlay
+ x: parent ? Math.round((parent.width - width) / 2) : 0
+ y: parent ? Math.round((parent.height - height) / 2) : 0
+ width: parent && parent.width > 32
+ ? Math.max(0, Math.min(480, parent.width - 32)) : 280
+ height: parent && parent.height > 32
+ ? Math.max(0, Math.min(600, parent.height - 32)) : 360
+ padding: root.compact ? 8 : 20
+ modal: true
+ focus: true
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ onOpened: {
+ root.searchText = ""
+ searchField.text = ""
+ Qt.callLater(searchField.forceActiveFocus)
+ }
+
+ Overlay.modal: Rectangle {
+ color: Qt.rgba(0, 0, 0, 0.4)
+ }
+
+ background: Rectangle {
+ radius: 24
+ color: root.theme.colors.cardBg
+ border.color: root.theme.colors.border
+ border.width: 1
+
+ Behavior on color {
+ ColorAnimation { duration: 180 }
+ }
+ }
+
+ contentItem: ColumnLayout {
+ spacing: root.compact ? 8 : 16
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ Layout.fillWidth: true
+ text: root.title
+ color: root.theme.colors.textPrimary
+ font.pixelSize: 18
+ font.weight: Font.Bold
+ }
+
+ Button {
+ id: closeButton
+
+ Layout.preferredWidth: 32
+ Layout.preferredHeight: 32
+ text: "\u00D7"
+ hoverEnabled: true
+ Accessible.name: qsTr("Close token selector")
+ ToolTip.visible: hovered
+ ToolTip.text: Accessible.name
+ onClicked: root.close()
+
+ contentItem: Text {
+ text: closeButton.text
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 16
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ radius: 16
+ color: closeButton.hovered || closeButton.activeFocus
+ ? root.theme.colors.panelHoverBg
+ : root.theme.colors.panelBg
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: root.compact ? 40 : 48
+ radius: 16
+ color: root.theme.colors.inputBg
+ border.color: searchField.activeFocus
+ ? root.theme.colors.ctaBg
+ : root.theme.colors.border
+ border.width: 1
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 14
+ anchors.rightMargin: 14
+ spacing: 8
+
+ Text {
+ text: "\u2315"
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 20
+ }
+
+ TextField {
+ id: searchField
+
+ objectName: "tokenSearchField"
+
+ Layout.fillWidth: true
+ color: root.theme.colors.textPrimary
+ placeholderText: root.searchPlaceholder
+ placeholderTextColor: root.theme.colors.textPlaceholder
+ selectionColor: root.theme.colors.selection
+ selectedTextColor: root.theme.colors.textPrimary
+ font.pixelSize: 15
+ selectByMouse: true
+ background: null
+
+ onTextEdited: root.searchText = text
+ onAccepted: root.acceptInput(text)
+ }
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: root.showPopular
+ text: root.popularTitle
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 13
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ visible: root.showPopular
+ spacing: 8
+
+ Repeater {
+ model: root.popularTokens
+
+ delegate: AmmTokenSelectButton {
+ required property var modelData
+
+ theme: root.theme
+ hasToken: true
+ tokenColor: root.tokenColor(modelData)
+ tokenLetter: root.tokenLetter(modelData)
+ showIndicator: false
+ text: root.tokenSymbol(modelData).length > 0
+ ? root.tokenSymbol(modelData) : root.tokenName(modelData)
+ maximumTextWidth: 84
+ width: Math.min(140, implicitWidth)
+ onClicked: root.chooseToken(modelData)
+ }
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: !root.compact
+ text: root.listTitle
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 13
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ListView {
+ id: tokenList
+
+ objectName: "tokenList"
+
+ anchors.fill: parent
+ clip: true
+ spacing: 2
+ model: root.rows
+ boundsBehavior: Flickable.StopAtBounds
+ ScrollIndicator.vertical: ScrollIndicator { }
+
+ delegate: Item {
+ id: tokenRow
+
+ required property var modelData
+ readonly property bool selectable: root.isSelectable(modelData)
+ readonly property string disabledReason: root.disabledReasonForCode(
+ modelData.code
+ || modelData.status)
+ readonly property bool pointerHovered: rowHover.containsMouse
+ readonly property bool disabledReasonVisible: (pointerHovered || activeFocus)
+ && !selectable
+
+ width: ListView.view.width
+ height: 56
+ activeFocusOnTab: true
+ Accessible.role: Accessible.Button
+ Accessible.name: root.tokenName(modelData)
+ Accessible.description: selectable
+ ? root.detailForToken(modelData)
+ : disabledReason
+ Accessible.onPressAction: tokenRow.activate()
+ Keys.onReturnPressed: function(event) {
+ tokenRow.activate()
+ event.accepted = true
+ }
+ Keys.onEnterPressed: function(event) {
+ tokenRow.activate()
+ event.accepted = true
+ }
+ Keys.onSpacePressed: function(event) {
+ tokenRow.activate()
+ event.accepted = true
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: 12
+ color: tokenRow.pointerHovered || tokenRow.activeFocus
+ ? root.theme.colors.panelBg : "transparent"
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 8
+ anchors.rightMargin: 8
+ spacing: 12
+
+ Rectangle {
+ Layout.preferredWidth: 36
+ Layout.preferredHeight: 36
+ radius: 18
+ color: root.tokenColor(tokenRow.modelData)
+
+ Text {
+ anchors.centerIn: parent
+ text: root.tokenLetter(tokenRow.modelData)
+ color: "#FFFFFF"
+ font.pixelSize: 14
+ font.weight: Font.Bold
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ Layout.fillWidth: true
+ text: root.tokenName(tokenRow.modelData)
+ color: tokenRow.selectable
+ ? root.theme.colors.textPrimary
+ : root.theme.colors.textPlaceholder
+ font.pixelSize: 15
+ elide: Text.ElideRight
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 6
+
+ Text {
+ visible: text.length > 0
+ text: root.tokenSymbol(tokenRow.modelData)
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 12
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: root.shortAddress(root.tokenAddress(tokenRow.modelData))
+ color: root.theme.colors.textPlaceholder
+ font.family: "monospace"
+ font.pixelSize: 11
+ elide: Text.ElideMiddle
+ }
+ }
+ }
+
+ Text {
+ Layout.maximumWidth: 118
+ text: root.detailForToken(tokenRow.modelData)
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 11
+ horizontalAlignment: Text.AlignRight
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ MouseArea {
+ id: rowHover
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: tokenRow.selectable
+ ? Qt.PointingHandCursor : Qt.ForbiddenCursor
+ onClicked: tokenRow.activate()
+ }
+
+ ToolTip.visible: tokenRow.disabledReasonVisible
+ ToolTip.text: tokenRow.disabledReason
+
+ function activate() {
+ if (tokenRow.selectable)
+ root.chooseToken(tokenRow.modelData)
+ }
+ }
+ }
+
+ Button {
+ id: customEntryButton
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ visible: root.rows.length === 0
+ && root.allowCustomEntry
+ && root.searchText.trim().length > 0
+ implicitHeight: 56
+ text: qsTr("Use TokenDefinition address")
+ onClicked: root.acceptInput(root.searchText)
+
+ contentItem: Column {
+ leftPadding: 8
+ spacing: 2
+
+ Text {
+ width: parent.width - 16
+ text: qsTr("Use TokenDefinition address")
+ color: root.theme.colors.textPrimary
+ font.pixelSize: 14
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ }
+
+ Text {
+ width: parent.width - 16
+ text: root.searchText
+ color: root.theme.colors.textSecondary
+ font.family: "monospace"
+ font.pixelSize: 11
+ elide: Text.ElideMiddle
+ }
+ }
+
+ background: Rectangle {
+ radius: 12
+ color: customEntryButton.hovered || customEntryButton.activeFocus
+ ? root.theme.colors.panelBg : "transparent"
+ }
+ }
+
+ Text {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 16
+ visible: root.rows.length === 0
+ && (!root.allowCustomEntry
+ || root.searchText.trim().length === 0)
+ text: qsTr("No tokens found")
+ color: root.theme.colors.textSecondary
+ font.pixelSize: 13
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ }
+
+ function filteredTokens() {
+ var needle = root.searchText.trim().toLowerCase()
+ if (needle.length === 0)
+ return root.tokens
+ var result = []
+ for (var i = 0; i < root.tokens.length; ++i) {
+ var token = root.tokens[i]
+ if (root.tokenName(token).toLowerCase().indexOf(needle) >= 0
+ || root.tokenSymbol(token).toLowerCase().indexOf(needle) >= 0
+ || root.tokenAddress(token).toLowerCase().indexOf(needle) >= 0)
+ result.push(token)
+ }
+ return result
+ }
+
+ function selectableTokens() {
+ var result = []
+ for (var i = 0; i < root.tokens.length; ++i) {
+ if (root.isSelectable(root.tokens[i]))
+ result.push(root.tokens[i])
+ }
+ return result
+ }
+
+ function acceptInput(value) {
+ var entered = String(value || "").trim()
+ if (entered.length === 0)
+ return
+ var lowered = entered.toLowerCase()
+ for (var i = 0; i < root.tokens.length; ++i) {
+ var token = root.tokens[i]
+ if (root.tokenAddress(token).toLowerCase() === lowered
+ || root.tokenName(token).toLowerCase() === lowered
+ || root.tokenSymbol(token).toLowerCase() === lowered) {
+ if (root.isSelectable(token))
+ root.chooseToken(token)
+ else {
+ root.tokenEntered(root.tokenAddress(token))
+ root.close()
+ }
+ return
+ }
+ }
+ if (root.rows.length === 1 && root.isSelectable(root.rows[0])) {
+ root.chooseToken(root.rows[0])
+ return
+ }
+ if (root.allowCustomEntry) {
+ root.tokenEntered(entered)
+ root.close()
+ }
+ }
+
+ function chooseToken(token) {
+ if (!root.isSelectable(token))
+ return
+ root.tokenSelected(token)
+ root.close()
+ }
+
+ function isSelectable(token) {
+ return token && token.selectable !== false
+ }
+
+ function tokenName(token) {
+ if (!token)
+ return qsTr("Unknown token")
+ return String(token.name || token.symbol || qsTr("Unknown token"))
+ }
+
+ function tokenSymbol(token) {
+ return token ? String(token.symbol || "") : ""
+ }
+
+ function tokenAddress(token) {
+ return token ? String(token.definitionId || token.address || "") : ""
+ }
+
+ function tokenColor(token) {
+ return token && token.color ? token.color : root.theme.colors.noTokenCircle
+ }
+
+ function tokenLetter(token) {
+ if (token && token.letter)
+ return String(token.letter)
+ var label = root.tokenSymbol(token) || root.tokenName(token)
+ return label.length > 0 ? label.charAt(0).toUpperCase() : ""
+ }
+
+ function shortAddress(value) {
+ var text = String(value || "")
+ return text.length > 14 ? text.slice(0, 7) + "..." + text.slice(-5) : text
+ }
+}
diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml
index 9dde13a6..297b6d9a 100644
--- a/apps/amm/qml/pages/LiquidityPage.qml
+++ b/apps/amm/qml/pages/LiquidityPage.qml
@@ -1,31 +1,43 @@
-import QtQuick 2.15
-import QtQuick.Layouts 1.15
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQml
+
+import Logos.Controls
+import Logos.Icons
import Logos.Wallet
-import "../components/shared"
+
import "../components/liquidity"
import "../state"
Item {
id: root
- property int activeLiquidityTab: 0
- property real slippageTolerancePercent: 0.5
- 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
+ property var backend: null
+ property var runtime: null
+ readonly property NewPositionFlow flow: newPositionFlow
+
+ readonly property int pageMargin: width < 640 ? 16 : 24
+ readonly property int contentMaxWidth: 1200
+ readonly property bool wideLayout: width >= 760
+ readonly property int stepRailWidth: Math.max(210, Math.min(330,
+ (width - pageMargin * 2) * 0.30))
+
+ AmmTheme {
+ id: theme
+ }
- width: parent ? parent.width : implicitWidth
- height: parent ? parent.height : implicitHeight
- implicitWidth: root.preferredCardWidth + root.pageMargin * 2
- implicitHeight: pageCard.implicitHeight + root.pageMargin * 2
+ NewPositionFlow {
+ id: newPositionFlow
- DummyPoolState {
- id: poolState
+ backend: root.backend
+ runtime: root.runtime
+ active: root.visible
}
Rectangle {
anchors.fill: parent
- color: "#151515"
+ color: theme.colors.background
}
Flickable {
@@ -33,133 +45,215 @@ Item {
anchors.fill: parent
clip: true
- contentHeight: Math.max(height, pageCard.y + pageCard.implicitHeight + root.pageMargin)
contentWidth: width
- enabled: !confirmationDialog.visible
+ contentHeight: Math.max(height, pageLayout.y + pageLayout.implicitHeight + root.pageMargin)
+ enabled: !confirmationDialog.opened
flickableDirection: Flickable.VerticalFlick
+ boundsBehavior: Flickable.StopAtBounds
- Rectangle {
- id: pageCard
-
- color: "#1B1B1B"
- implicitHeight: shellContent.implicitHeight + 24
- radius: 16
- border.color: "#303030"
- border.width: 1
- width: Math.max(0, Math.min(scroll.width - root.pageMargin * 2, root.preferredCardWidth))
- x: Math.max(root.pageMargin, (scroll.width - width) / 2)
- y: root.pageCardY
+ ScrollBar.vertical: ScrollBar {
+ policy: ScrollBar.AsNeeded
+ }
- ColumnLayout {
- id: shellContent
+ ColumnLayout {
+ id: pageLayout
- anchors.fill: parent
- anchors.margins: 12
- spacing: 10
+ x: Math.max(root.pageMargin, (scroll.width - width) / 2)
+ y: root.wideLayout ? 32 : 16
+ width: Math.max(0, Math.min(root.contentMaxWidth,
+ scroll.width - root.pageMargin * 2))
+ spacing: 24
- RowLayout {
- spacing: 10
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 16
+ ColumnLayout {
Layout.fillWidth: true
+ spacing: 4
Text {
- color: "#E7E1D8"
- font.bold: true
- font.pixelSize: 18
- text: qsTr("Liquidity")
-
- Layout.fillWidth: true
+ text: qsTr("New position")
+ color: theme.colors.textPrimary
+ font.pixelSize: 30
+ font.weight: Font.Bold
+ font.letterSpacing: 0
}
- Rectangle {
- color: "#211914"
- radius: 12
- border.color: "#49301F"
- border.width: 1
-
- Layout.preferredHeight: 26
- Layout.preferredWidth: pairText.implicitWidth + 20
-
- Text {
- id: pairText
-
- anchors.centerIn: parent
- color: "#F2D8C7"
- font.bold: true
- font.pixelSize: 12
- text: qsTr("%1 / %2").arg(poolState.tokenA).arg(poolState.tokenB)
- }
+ Text {
+ Layout.fillWidth: true
+ text: form.contextStatusText()
+ color: theme.colors.textSecondary
+ font.pixelSize: 12
+ elide: Text.ElideRight
}
}
- LiquidityActionTabs {
- currentIndex: root.activeLiquidityTab
-
- Layout.fillWidth: true
- Layout.preferredHeight: implicitHeight
-
- onTabRequested: function (index) {
- root.activeLiquidityTab = index;
- }
+ LogosIconButton {
+ objectName: "refreshPositionButton"
+ iconSource: LogosIcons.refresh
+ iconColor: theme.colors.textSecondary
+ iconSize: 18
+ Layout.preferredWidth: 40
+ Layout.preferredHeight: 40
+ enabled: !newPositionFlow.contextLoading && !newPositionFlow.submitting
+ Accessible.name: qsTr("Refresh position data")
+ ToolTip.visible: hovered
+ ToolTip.text: Accessible.name
+ onClicked: newPositionFlow.refreshContext(true)
}
+ }
- PoolPositionSummary {
- poolState: poolState
+ Rectangle {
+ id: compactSteps
- Layout.fillWidth: true
- Layout.preferredHeight: implicitHeight
- }
+ objectName: "compactPositionSteps"
+ Layout.fillWidth: true
+ implicitHeight: compactStepRow.implicitHeight + 32
+ visible: !root.wideLayout
+ radius: 16
+ color: theme.colors.cardBg
+ border.color: theme.colors.border
+ border.width: 1
- AddLiquidityForm {
- id: addLiquidityForm
+ RowLayout {
+ id: compactStepRow
- poolState: poolState
- slippageTolerancePercent: root.slippageTolerancePercent
- visible: root.activeLiquidityTab === 0
+ anchors.fill: parent
+ anchors.margins: 16
+ spacing: 12
- Layout.fillWidth: true
- Layout.preferredHeight: visible ? implicitHeight : 0
+ StepMarker {
+ Layout.fillWidth: true
+ colors: theme.colors
+ stepNumber: 1
+ label: qsTr("Select pair and fees")
+ active: !form.hasPair
+ complete: form.hasPair
+ }
- onSlippageToleranceChangeRequested: function (tolerancePercent) {
- root.slippageTolerancePercent = poolState.clampSlippageTolerancePercent(tolerancePercent);
+ Rectangle {
+ Layout.preferredWidth: 20
+ Layout.preferredHeight: 1
+ color: form.hasPair ? theme.colors.ctaBg : theme.colors.divider
}
- onAddLiquidityRequested: function (snapshot) {
- confirmationDialog.openWithSnapshot(snapshot);
+ StepMarker {
+ Layout.fillWidth: true
+ colors: theme.colors
+ stepNumber: 2
+ label: form.missingPool
+ ? qsTr("Set price and deposit")
+ : qsTr("Enter deposit amounts")
+ active: form.hasPair
}
}
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignTop
+ spacing: root.wideLayout ? 40 : 0
+
+ Rectangle {
+ id: positionStepRail
+
+ objectName: "positionStepRail"
+ Layout.preferredWidth: root.stepRailWidth
+ Layout.alignment: Qt.AlignTop
+ implicitHeight: verticalSteps.implicitHeight + 40
+ visible: root.wideLayout
+ radius: 16
+ color: theme.colors.cardBg
+ border.color: theme.colors.border
+ border.width: 1
+
+ ColumnLayout {
+ id: verticalSteps
+
+ anchors.fill: parent
+ anchors.margins: 20
+ spacing: 0
+
+ StepMarker {
+ Layout.fillWidth: true
+ colors: theme.colors
+ stepNumber: 1
+ label: qsTr("Select token pair and fees")
+ active: !form.hasPair
+ complete: form.hasPair
+ }
- RemoveLiquidityForm {
- id: removeLiquidityForm
+ Rectangle {
+ Layout.leftMargin: 17
+ Layout.preferredWidth: 1
+ Layout.preferredHeight: 28
+ color: form.hasPair ? theme.colors.ctaBg : theme.colors.divider
+ }
- poolState: poolState
- slippageTolerancePercent: root.slippageTolerancePercent
- visible: root.activeLiquidityTab === 1
+ StepMarker {
+ Layout.fillWidth: true
+ colors: theme.colors
+ stepNumber: 2
+ label: form.missingPool
+ ? qsTr("Set price and deposit amounts")
+ : qsTr("Enter deposit amounts")
+ active: form.hasPair
+ }
+ }
+ }
+ NewPositionForm {
+ id: form
+
+ objectName: "newPositionForm"
Layout.fillWidth: true
- Layout.preferredHeight: visible ? implicitHeight : 0
+ Layout.alignment: Qt.AlignTop
+ theme: theme
+ headingText: form.hasPair ? qsTr("Deposit tokens") : qsTr("Select pair")
+ headingDetail: form.hasPair
+ ? qsTr("Specify the token amounts for your liquidity contribution.")
+ : qsTr("Choose two tokens and a fee tier for this position.")
+ showRefreshAction: false
+ newPositionContext: newPositionFlow.newPositionContext
+ flowState: newPositionFlow.viewState
+
+ onQuoteRequested: function(immediate, quoteRequest) {
+ newPositionFlow.scheduleQuote(immediate, quoteRequest)
+ }
- onSlippageToleranceChangeRequested: function (tolerancePercent) {
- root.slippageTolerancePercent = poolState.clampSlippageTolerancePercent(tolerancePercent);
+ onConfirmationRequested: function(snapshot) {
+ confirmationDialog.openWithSnapshot(snapshot)
}
- onRemoveLiquidityRequested: function (snapshot) {
- confirmationDialog.openWithSnapshot(snapshot);
+ onTokenResolveRequested: function(tokenId) {
+ newPositionFlow.resolveToken(tokenId)
}
+
+ onDraftChanged: newPositionFlow.draftChanged()
+ onRefreshRequested: newPositionFlow.refreshContext(true)
}
}
+ }
+ }
- SuccessToast {
- id: successToast
+ Connections {
+ target: newPositionFlow
- width: Math.max(0, Math.min(380, parent.width - 24))
+ function onTokenResolutionFinished(finalResponse) {
+ form.finishTokenResolution(finalResponse)
+ }
- anchors {
- bottom: parent.bottom
- bottomMargin: 14
- horizontalCenter: parent.horizontalCenter
- }
- }
+ function onTokenResolutionFailed(code) {
+ form.failTokenResolution(code)
+ }
+
+ function onPoolActivated(quote) {
+ form.acceptPoolActivation(quote)
+ }
+
+ function onQuoteRefreshRequested(immediate) {
+ form.requestQuote(immediate)
}
}
@@ -171,28 +265,73 @@ Item {
TransactionConfirmationDialog {
id: confirmationDialog
- title: snapshot.action === "add"
- ? qsTr("Confirm add liquidity")
- : qsTr("Confirm remove liquidity")
+
+ title: qsTr("Confirm new position")
+ confirmText: qsTr("Submit")
+ busy: newPositionFlow.submitting
summary: liquidityConfirmationSummary
- onConfirmed: function (snapshot) {
- root.confirmLiquidityAction(snapshot);
+ onConfirmed: function(snapshot) {
+ newPositionFlow.confirm(snapshot)
}
}
- function confirmLiquidityAction(snapshot) {
- if (snapshot.action === "add") {
- poolState.applyAddLiquidity(snapshot.actualA, snapshot.actualB, snapshot.deltaLp);
- addLiquidityForm.resetForm();
- successToast.show(qsTr("Liquidity added"), qsTr("Position updated"));
- return;
+ component StepMarker: RowLayout {
+ id: marker
+
+ required property var colors
+ required property int stepNumber
+ required property string label
+ property bool active: false
+ property bool complete: false
+
+ spacing: 12
+
+ Rectangle {
+ Layout.preferredWidth: 36
+ Layout.preferredHeight: 36
+ radius: 18
+ color: marker.active
+ ? marker.colors.ctaBg
+ : marker.complete ? marker.colors.selection : marker.colors.inputBg
+ border.color: marker.active || marker.complete
+ ? marker.colors.ctaBg : marker.colors.borderStrong
+ border.width: 1
+ Accessible.ignored: true
+
+ Text {
+ anchors.centerIn: parent
+ text: marker.stepNumber
+ color: marker.active
+ ? marker.colors.background
+ : marker.complete ? marker.colors.ctaBg : marker.colors.textPlaceholder
+ font.pixelSize: 13
+ font.weight: Font.DemiBold
+ }
}
- if (snapshot.action === "remove") {
- poolState.applyRemoveLiquidity(snapshot.withdrawA, snapshot.withdrawB, snapshot.burnAmount);
- removeLiquidityForm.resetForm();
- successToast.show(qsTr("Liquidity removed"), qsTr("Position updated"));
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Step %1").arg(marker.stepNumber)
+ color: marker.active || marker.complete
+ ? marker.colors.textSecondary : marker.colors.textPlaceholder
+ font.pixelSize: 11
+ elide: Text.ElideRight
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: marker.label
+ color: marker.active || marker.complete
+ ? marker.colors.textPrimary : marker.colors.textSecondary
+ font.pixelSize: 13
+ font.weight: marker.active ? Font.DemiBold : Font.Normal
+ wrapMode: Text.Wrap
+ }
}
}
}
diff --git a/apps/amm/qml/state/DummyPoolState.qml b/apps/amm/qml/state/DummyPoolState.qml
deleted file mode 100644
index 7e4d573d..00000000
--- a/apps/amm/qml/state/DummyPoolState.qml
+++ /dev/null
@@ -1,205 +0,0 @@
-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
- readonly property real minimumLiquidity: 1000
-
- readonly property real poolShare: totalLpSupply > 0 ? userLpBalance / totalLpSupply : 0
- readonly property real userOwnedA: reserveA * poolShare
- 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 parseAmount(value) {
- return Math.max(0, Number(value) || 0);
- }
-
- function floorAmount(value) {
- return Math.floor(parseAmount(value));
- }
-
- function amountBForA(amountA) {
- if (reserveA <= 0) {
- return 0;
- }
-
- return reserveB * parseAmount(amountA) / reserveA;
- }
-
- function amountAForB(amountB) {
- if (reserveB <= 0) {
- return 0;
- }
-
- return reserveA * parseAmount(amountB) / reserveB;
- }
-
- function addLiquidityPreview(maxA, maxB) {
- const safeMaxA = parseAmount(maxA);
- const safeMaxB = parseAmount(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 lpFromA = reserveA > 0 ? Math.floor(totalLpSupply * actualA / reserveA) : 0;
- const lpFromB = reserveB > 0 ? Math.floor(totalLpSupply * actualB / reserveB) : 0;
-
- return {
- "actualA": actualA,
- "actualB": actualB,
- "deltaLp": Math.min(lpFromA, lpFromB),
- "idealA": idealA,
- "idealB": idealB
- };
- }
-
- function maxAddLiquidityForBalances() {
- return addLiquidityPreview(walletBalanceA, walletBalanceB);
- }
-
- function clampBurnAmount(value) {
- return Math.min(floorAmount(value), Math.max(0, floorAmount(userLpBalance)));
- }
-
- function clampSlippageTolerancePercent(value) {
- return Math.max(0.01, Math.min(50, Number(value) || 0));
- }
-
- function minReceivedAmount(previewAmount, slippageTolerancePercent) {
- const safeAmount = floorAmount(previewAmount);
- const safeSlippage = clampSlippageTolerancePercent(slippageTolerancePercent);
-
- return Math.floor(safeAmount * (1 - safeSlippage / 100));
- }
-
- 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));
- }
-
- function removeLiquidityPreview(burnedLp) {
- const safeBurnedLp = totalLpSupply > 0 ? Math.min(clampBurnAmount(burnedLp), floorAmount(totalLpSupply)) : 0;
- const withdrawA = totalLpSupply > 0 ? Math.floor(reserveA * safeBurnedLp / totalLpSupply) : 0;
- const withdrawB = totalLpSupply > 0 ? Math.floor(reserveB * safeBurnedLp / totalLpSupply) : 0;
- const newTotalLpSupply = Math.max(0, floorAmount(totalLpSupply) - safeBurnedLp);
- const newUserLpBalance = Math.max(0, floorAmount(userLpBalance) - safeBurnedLp);
-
- return {
- "burnedLp": safeBurnedLp,
- "newReserveA": Math.max(0, reserveA - withdrawA),
- "newReserveB": Math.max(0, reserveB - withdrawB),
- "newTotalLpSupply": newTotalLpSupply,
- "newUserLpBalance": newUserLpBalance,
- "newUserShare": newTotalLpSupply > 0 ? newUserLpBalance / newTotalLpSupply : 0,
- "withdrawA": withdrawA,
- "withdrawB": withdrawB
- };
- }
-
- function formatInteger(value) {
- const rounded = Math.round(Number(value) || 0);
- return rounded.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
- }
-
- function formatDecimal(value) {
- const amount = Number(value) || 0;
-
- if (Math.abs(amount - Math.round(amount)) < 0.000001) {
- return formatInteger(amount);
- }
-
- return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, "");
- }
-
- function formatCompactDecimal(value) {
- const amount = Number(value) || 0;
-
- if (Math.abs(amount) >= 1000 || Math.abs(amount - Math.round(amount)) < 0.000001) {
- return formatInteger(amount);
- }
-
- if (Math.abs(amount) >= 1) {
- return amount.toFixed(2).replace(/0+$/, "").replace(/[.]$/, "");
- }
-
- return amount.toFixed(6).replace(/0+$/, "").replace(/[.]$/, "");
- }
-
- function formatInputAmount(value) {
- return formatDecimal(value);
- }
-
- function formatTokenAmount(value, token) {
- return formatDecimal(value) + " " + token;
- }
-
- function formatCompactTokenAmount(value, token) {
- return formatCompactDecimal(value) + " " + token;
- }
-
- function formatLpAmount(value) {
- return formatInteger(value) + " LP";
- }
-
- function formatPoolShare(value) {
- return "\u2248 " + (Math.max(0, Number(value) || 0) * 100).toFixed(2) + "%";
- }
-
- function formatPercent(value) {
- const amount = Math.max(0, Number(value) || 0);
-
- if (Math.abs(amount - Math.round(amount)) < 0.000001) {
- return Math.round(amount).toString() + "%";
- }
-
- return amount.toFixed(2).replace(/0+$/, "").replace(/[.]$/, "") + "%";
- }
-}
diff --git a/apps/amm/qml/state/NewPositionFlow.qml b/apps/amm/qml/state/NewPositionFlow.qml
new file mode 100644
index 00000000..6edba5cc
--- /dev/null
+++ b/apps/amm/qml/state/NewPositionFlow.qml
@@ -0,0 +1,375 @@
+import QtQml
+
+QtObject {
+ id: root
+
+ property var backend: null
+ property var runtime: null
+ property bool active: false
+
+ readonly property bool walletStateReady: root.backend !== null
+ && root.backend.walletStateReady === true
+ readonly property var newPositionContext: root.walletStateReady
+ && root.backend.newPositionContext
+ ? root.backend.newPositionContext
+ : root.loadingContext()
+ readonly property var viewState: ({
+ "quote": root.newPositionQuote,
+ "contextLoading": root.contextLoading || !root.walletStateReady
+ || root.newPositionContext.status === "loading",
+ "quoteLoading": root.quoteLoading,
+ "quoteStale": root.quoteStale,
+ "submitting": root.submitting,
+ "poolCreationPending": root.selectedPoolCreationPending(),
+ "transactionId": root.transactionId,
+ "errorCode": root.flowErrorCode || root.contextErrorCode
+ || root.quoteErrorCode
+ })
+
+ property var newPositionQuote: ({})
+ property var resolvedTokenIds: []
+ property int contextSerial: 0
+ property int quoteSerial: 0
+ property bool contextLoading: false
+ property bool quoteLoading: false
+ property bool quoteStale: true
+ property bool submitting: false
+ property string transactionId: ""
+ property string flowErrorCode: ""
+ property string contextErrorCode: ""
+ property string quoteErrorCode: ""
+ property var pendingQuoteRequest: ({ "ok": false, "request": ({}) })
+ property var pendingPoolProbes: []
+ property bool poolProbeInFlight: false
+
+ signal tokenResolutionFinished(bool finalResponse)
+ signal tokenResolutionFailed(string code)
+ signal poolActivated(var quote)
+ signal quoteRefreshRequested(bool immediate)
+ signal submitSucceeded
+ signal submitFailed
+
+ objectName: "newPositionFlow"
+
+ property Timer quoteDebounce: Timer {
+ interval: 250
+ repeat: false
+ onTriggered: root.requestQuoteNow(root.quoteSerial)
+ }
+
+ property Timer poolPoller: Timer {
+ interval: 5000
+ repeat: true
+ running: root.pendingPoolProbes.length > 0
+ onTriggered: root.pollPendingPool()
+ }
+
+ onNewPositionContextChanged: root.invalidateQuote()
+
+ onWalletStateReadyChanged: {
+ ++root.contextSerial
+ if (!root.walletStateReady)
+ root.contextLoading = false
+ root.invalidateQuote()
+ }
+
+ onActiveChanged: {
+ if (!root.active)
+ return
+ Qt.callLater(function() {
+ if (root.active && root.walletStateReady)
+ root.quoteRefreshRequested(true)
+ })
+ }
+
+ function contextHints(refreshWalletAccounts) {
+ const request = root.pendingQuoteRequest.request || {}
+ const recent = []
+ if (request.tokenAId)
+ recent.push(request.tokenAId)
+ if (request.tokenBId && request.tokenBId !== request.tokenAId)
+ recent.push(request.tokenBId)
+ return {
+ "recentTokenIds": recent,
+ "resolvedTokenIds": root.resolvedTokenIds,
+ "refreshWalletAccounts": refreshWalletAccounts === true
+ }
+ }
+
+ function refreshContext(refreshWalletAccounts, completed) {
+ const serial = ++root.contextSerial
+ root.contextLoading = true
+ if (!root.walletStateReady || root.runtime === null) {
+ root.contextLoading = false
+ return
+ }
+
+ root.runtime.watch(root.backend.refreshNewPositionContext(
+ root.contextHints(refreshWalletAccounts)),
+ function() {
+ root.finishContextRefresh(serial, completed)
+ },
+ function(error) {
+ root.failContextRefresh(serial)
+ })
+ }
+
+ function finishContextRefresh(serial, completed) {
+ if (serial !== root.contextSerial)
+ return
+ root.contextLoading = false
+ root.contextErrorCode = ""
+ Qt.callLater(function() {
+ if (serial !== root.contextSerial)
+ return
+ root.tokenResolutionFinished(true)
+ if (completed)
+ completed()
+ })
+ }
+
+ function failContextRefresh(serial) {
+ if (serial !== root.contextSerial)
+ return
+ root.contextLoading = false
+ root.contextErrorCode = "backend_error"
+ root.tokenResolutionFailed("backend_error")
+ }
+
+ function resolveToken(tokenId) {
+ const value = String(tokenId || "").trim()
+ if (value.length === 0)
+ return
+ if (root.resolvedTokenIds.indexOf(value) < 0) {
+ const next = root.resolvedTokenIds.slice(0)
+ next.push(value)
+ root.resolvedTokenIds = next
+ }
+ root.refreshContext(false)
+ }
+
+ function scheduleQuote(immediate, quoteRequest) {
+ ++root.quoteSerial
+ root.pendingQuoteRequest = quoteRequest
+ root.quoteStale = true
+ root.quoteLoading = root.walletStateReady && root.active
+ root.quoteDebounce.stop()
+ if (!root.walletStateReady || !root.active)
+ return
+ if (immediate)
+ root.requestQuoteNow(root.quoteSerial)
+ else
+ root.quoteDebounce.restart()
+ }
+
+ function requestQuoteNow(serial) {
+ if (serial !== root.quoteSerial)
+ return
+ const built = root.pendingQuoteRequest
+ if (!built.ok) {
+ root.quoteLoading = false
+ return
+ }
+ if (!root.walletStateReady || !root.active || root.runtime === null) {
+ root.quoteLoading = false
+ return
+ }
+
+ root.runtime.watch(root.backend.quoteNewPosition(built.request),
+ function(quote) {
+ if (serial !== root.quoteSerial)
+ return
+ root.quoteLoading = false
+ root.quoteStale = false
+ root.quoteErrorCode = ""
+ if (!quote || quote.schema !== "new-position.v1")
+ root.newPositionQuote = root.quoteError("unsupported_schema")
+ else
+ root.newPositionQuote = quote
+ },
+ function(error) {
+ if (serial !== root.quoteSerial)
+ return
+ root.quoteLoading = false
+ root.quoteStale = true
+ root.quoteErrorCode = "backend_error"
+ })
+ }
+
+ function confirm(snapshot) {
+ if (root.submitting)
+ return
+ root.submitting = true
+ root.flowErrorCode = ""
+
+ if (!root.backend || root.runtime === null) {
+ root.finishSubmitFailure(root.quoteError("wallet_unavailable"))
+ return
+ }
+
+ root.runtime.watch(root.backend.submitNewPosition(snapshot.request, snapshot.quoteHash),
+ function(result) {
+ if (result && result.schema === "new-position.v1"
+ && result.status === "submitted"
+ && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(
+ String(result.transactionId || ""))) {
+ if (snapshot.request.initialPriceRealRaw !== undefined)
+ root.watchPoolCreation(snapshot.poolProbeRequest, result.deadlineMs)
+ root.submitting = false
+ root.transactionId = result.transactionId
+ root.flowErrorCode = ""
+ root.contextErrorCode = ""
+ root.quoteErrorCode = ""
+ root.invalidateQuote()
+ root.submitSucceeded()
+ return
+ }
+ root.finishSubmitFailure(result || root.quoteError("wallet_submission_failed"))
+ },
+ function(error) {
+ root.finishSubmitFailure(root.quoteError("wallet_submission_failed"))
+ })
+ }
+
+ function finishSubmitFailure(result) {
+ root.submitting = false
+ const hasFreshQuote = result && result.quote
+ && result.quote.schema === "new-position.v1"
+ if (hasFreshQuote) {
+ root.newPositionQuote = result.quote
+ root.quoteLoading = false
+ root.quoteStale = false
+ }
+ const code = result && result.code ? result.code : "wallet_submission_failed"
+ root.flowErrorCode = code
+ root.submitFailed()
+ if (hasFreshQuote)
+ return
+ root.scheduleQuote(true, root.pendingQuoteRequest)
+ }
+
+ function watchPoolCreation(request, deadlineMs) {
+ const key = root.pairKey(request)
+ let deadline = Number(deadlineMs)
+ if (!isFinite(deadline) || deadline <= 0)
+ deadline = 0
+ const pending = root.pendingPoolProbes.filter(function(item) {
+ return item.key !== key
+ })
+ pending.push({
+ "key": key,
+ "request": request,
+ "deadlineMs": deadline
+ })
+ root.pendingPoolProbes = pending
+ Qt.callLater(root.pollPendingPool)
+ }
+
+ function pollPendingPool() {
+ if (root.poolProbeInFlight || root.pendingPoolProbes.length === 0
+ || !root.walletStateReady || root.runtime === null) {
+ return
+ }
+ const pending = root.pendingPoolProbes[0]
+ root.poolProbeInFlight = true
+ root.runtime.watch(root.backend.quoteNewPosition(pending.request),
+ function(quote) {
+ root.finishPoolProbe(pending, quote)
+ },
+ function(error) {
+ root.finishPoolProbe(pending, null)
+ })
+ }
+
+ function finishPoolProbe(pending, quote) {
+ root.poolProbeInFlight = false
+ if (quote && quote.schema === "new-position.v1"
+ && quote.poolStatus === "active_pool") {
+ root.removePendingPool(pending.key)
+ if (root.matchesSelectedPair(pending.request)) {
+ root.poolActivated(quote)
+ root.invalidateQuote()
+ root.refreshContext(true)
+ }
+ return
+ }
+ if (pending.deadlineMs > 0 && Date.now() >= pending.deadlineMs) {
+ root.removePendingPool(pending.key)
+ return
+ }
+ root.rotatePendingPool()
+ }
+
+ function pairKey(request) {
+ return String(request.tokenAId || "") + ":" + String(request.tokenBId || "")
+ }
+
+ function matchesSelectedPair(request) {
+ return root.pairKey(root.pendingQuoteRequest.request || {}) === root.pairKey(request)
+ }
+
+ function selectedPoolCreationPending() {
+ const request = root.pendingQuoteRequest.request || {}
+ if (!request.tokenAId || !request.tokenBId)
+ return false
+ const selected = root.pairKey(request)
+ return root.pendingPoolProbes.some(function(item) {
+ return item.key === selected
+ })
+ }
+
+ function removePendingPool(key) {
+ root.pendingPoolProbes = root.pendingPoolProbes.filter(function(item) {
+ return item.key !== key
+ })
+ }
+
+ function rotatePendingPool() {
+ if (root.pendingPoolProbes.length < 2)
+ return
+ const pending = root.pendingPoolProbes.slice(1)
+ pending.push(root.pendingPoolProbes[0])
+ root.pendingPoolProbes = pending
+ }
+
+ function draftChanged() {
+ root.invalidateQuote()
+ root.transactionId = ""
+ root.flowErrorCode = ""
+ root.contextErrorCode = ""
+ root.quoteErrorCode = ""
+ }
+
+ function invalidateQuote() {
+ ++root.quoteSerial
+ root.quoteDebounce.stop()
+ root.quoteLoading = false
+ root.quoteStale = true
+ }
+
+ function loadingContext() {
+ return {
+ "schema": "new-position.v1",
+ "status": "loading",
+ "tokens": [],
+ "feeTiers": []
+ }
+ }
+
+ function quoteError(code) {
+ return {
+ "schema": "new-position.v1",
+ "status": "error",
+ "canSubmit": false,
+ "code": code,
+ "poolStatus": "unavailable_pool",
+ "errors": [{
+ "code": code,
+ "blockingFields": [],
+ "details": ({})
+ }],
+ "warnings": [],
+ "accountPreview": []
+ }
+ }
+}
diff --git a/apps/amm/src/ActiveNetwork.cpp b/apps/amm/src/ActiveNetwork.cpp
new file mode 100644
index 00000000..c92bc73b
--- /dev/null
+++ b/apps/amm/src/ActiveNetwork.cpp
@@ -0,0 +1,141 @@
+#include "ActiveNetwork.h"
+
+#include
+#include
+#include
+#include
+
+namespace {
+ const char NETWORK_ENV[] = "AMM_UI_NETWORK";
+ const char DEVNET_FILE_ENV[] = "AMM_UI_DEVNET_FILE";
+
+ bool isLowerHex(const QString& value, int size)
+ {
+ if (value.size() != size)
+ return false;
+ for (const QChar character : value) {
+ const bool isAsciiDigit = character >= QLatin1Char('0')
+ && character <= QLatin1Char('9');
+ if (!isAsciiDigit
+ && (character < QLatin1Char('a') || character > QLatin1Char('f'))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+bool ActiveNetwork::load()
+{
+ m_network = {};
+ m_network.status = QStringLiteral("config_missing");
+ m_expectedIdentity.clear();
+
+ const QByteArray selected = qgetenv(NETWORK_ENV);
+ m_network.id = selected.isEmpty()
+ ? QStringLiteral("testnet")
+ : QString::fromLocal8Bit(selected).trimmed();
+
+ QJsonObject entry;
+ if (isDevnet()) {
+ const QString path = QString::fromLocal8Bit(qgetenv(DEVNET_FILE_ENV));
+ QFile file(path);
+ if (path.isEmpty() || !file.open(QIODevice::ReadOnly))
+ return false;
+ const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
+ if (!document.isObject())
+ return false;
+ entry = document.object();
+ m_expectedIdentity = entry.value(QStringLiteral("channelId")).toString();
+ } else {
+ QFile file(QStringLiteral(":/amm/config/networks.json"));
+ if (!file.open(QIODevice::ReadOnly))
+ return false;
+ const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
+ if (!document.isObject())
+ return false;
+ const QJsonObject networks = document.object();
+ entry = networks.value(m_network.id).toObject();
+ m_expectedIdentity = entry.value(QStringLiteral("checkpointHash")).toString();
+ }
+
+ m_network.ammProgramId = entry.value(QStringLiteral("ammProgramId")).toString();
+ if (!isValidIdentity(m_expectedIdentity)
+ || !isLowerHex(m_network.ammProgramId, 64)) {
+ return false;
+ }
+
+ for (const QJsonValue& value : entry.value(QStringLiteral("tokenDefinitionIds")).toArray()) {
+ const QString id = value.toString();
+ if (!isLowerHex(id, 64)) {
+ m_network.tokenIds.clear();
+ return false;
+ }
+ m_network.tokenIds.append(id);
+ }
+ m_network.status = QStringLiteral("network_unknown");
+ return true;
+}
+
+bool ActiveNetwork::isConfigured() const
+{
+ return m_network.status != QStringLiteral("config_missing");
+}
+
+bool ActiveNetwork::isDevnet() const
+{
+ return m_network.id == QStringLiteral("devnet");
+}
+
+bool ActiveNetwork::needsIdentityProbe() const
+{
+ return m_network.status == QStringLiteral("loading")
+ || m_network.status == QStringLiteral("network_unknown");
+}
+
+void ActiveNetwork::sequencerChanged(bool available)
+{
+ if (isConfigured())
+ clearIdentity(available ? QStringLiteral("loading") : QStringLiteral("network_unknown"));
+}
+
+void ActiveNetwork::reachabilityChanged(bool reachable, bool wasReachable)
+{
+ if (!isConfigured())
+ return;
+ if (!reachable)
+ clearIdentity(QStringLiteral("network_unknown"));
+ else if (!wasReachable)
+ clearIdentity(QStringLiteral("loading"));
+}
+
+void ActiveNetwork::beginIdentityProbe()
+{
+ if (isConfigured())
+ clearIdentity(QStringLiteral("loading"));
+}
+
+void ActiveNetwork::finishIdentityProbe(const QString& identity)
+{
+ if (identity.isEmpty()) {
+ clearIdentity(QStringLiteral("network_unknown"));
+ } else if (identity != m_expectedIdentity) {
+ clearIdentity(QStringLiteral("network_mismatch"));
+ } else {
+ m_network.status = QStringLiteral("ready");
+ m_network.fingerprint = (isDevnet() ? QStringLiteral("channel:")
+ : QStringLiteral("block10:"))
+ + identity;
+ }
+}
+
+bool ActiveNetwork::isValidIdentity(const QString& value)
+{
+ return isLowerHex(value, 64);
+}
+
+void ActiveNetwork::clearIdentity(const QString& status)
+{
+ m_network.status = status;
+ m_network.fingerprint.clear();
+}
diff --git a/apps/amm/src/ActiveNetwork.h b/apps/amm/src/ActiveNetwork.h
new file mode 100644
index 00000000..9b1db9b7
--- /dev/null
+++ b/apps/amm/src/ActiveNetwork.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include
+#include
+
+struct ActiveNetworkSnapshot {
+ QString id;
+ QString status;
+ QString fingerprint;
+ QString ammProgramId;
+ QStringList tokenIds;
+};
+
+class ActiveNetwork final {
+public:
+ bool load();
+
+ const QString& status() const { return m_network.status; }
+ bool isConfigured() const;
+ bool isDevnet() const;
+ bool needsIdentityProbe() const;
+ ActiveNetworkSnapshot snapshot() const { return m_network; }
+
+ void sequencerChanged(bool available);
+ void reachabilityChanged(bool reachable, bool wasReachable);
+ void beginIdentityProbe();
+ void finishIdentityProbe(const QString& identity);
+
+ static bool isValidIdentity(const QString& value);
+
+private:
+ void clearIdentity(const QString& status);
+
+ ActiveNetworkSnapshot m_network;
+ QString m_expectedIdentity;
+};
diff --git a/apps/amm/src/AmmClient.cpp b/apps/amm/src/AmmClient.cpp
new file mode 100644
index 00000000..17d6d61b
--- /dev/null
+++ b/apps/amm/src/AmmClient.cpp
@@ -0,0 +1,71 @@
+#include "AmmClient.h"
+
+#include
+#include
+#include
+
+#include "amm_client.h"
+
+namespace {
+ using Operation = char* (*)(const char*);
+
+ AmmClientResult call(Operation operation, const QJsonObject& request)
+ {
+ const QByteArray payload = QJsonDocument(request).toJson(QJsonDocument::Compact);
+ char* raw = operation(payload.constData());
+ if (!raw) {
+ qWarning() << "AmmClient: bundled client returned a null response";
+ return {};
+ }
+ const QByteArray response(raw);
+ amm_free(raw);
+
+ QJsonParseError parseError;
+ const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
+ if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
+ qWarning() << "AmmClient: bundled client returned invalid JSON";
+ return {};
+ }
+ const QJsonObject envelope = document.object();
+ if (!envelope.value(QStringLiteral("ok")).toBool()) {
+ qWarning() << "AmmClient: bundled client failure:"
+ << envelope.value(QStringLiteral("error")).toString();
+ return {};
+ }
+ if (!envelope.value(QStringLiteral("value")).isObject()) {
+ qWarning() << "AmmClient: bundled client value is not an object";
+ return {};
+ }
+ return { true, envelope.value(QStringLiteral("value")).toObject() };
+ }
+}
+
+AmmClientResult BundledAmmClient::configId(const QJsonObject& request) const
+{
+ return call(amm_config_id, request);
+}
+
+AmmClientResult BundledAmmClient::tokenIds(const QJsonObject& request) const
+{
+ return call(amm_token_ids, request);
+}
+
+AmmClientResult BundledAmmClient::pairIds(const QJsonObject& request) const
+{
+ return call(amm_pair_ids, request);
+}
+
+AmmClientResult BundledAmmClient::context(const QJsonObject& request) const
+{
+ return call(amm_context, request);
+}
+
+AmmClientResult BundledAmmClient::quote(const QJsonObject& request) const
+{
+ return call(amm_quote, request);
+}
+
+AmmClientResult BundledAmmClient::plan(const QJsonObject& request) const
+{
+ return call(amm_plan, request);
+}
diff --git a/apps/amm/src/AmmClient.h b/apps/amm/src/AmmClient.h
new file mode 100644
index 00000000..6c69f48a
--- /dev/null
+++ b/apps/amm/src/AmmClient.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include
+
+struct AmmClientResult {
+ bool ok = false;
+ QJsonObject value;
+};
+
+class AmmClient {
+public:
+ virtual ~AmmClient() = default;
+
+ virtual AmmClientResult configId(const QJsonObject& request) const = 0;
+ virtual AmmClientResult tokenIds(const QJsonObject& request) const = 0;
+ virtual AmmClientResult pairIds(const QJsonObject& request) const = 0;
+ virtual AmmClientResult context(const QJsonObject& request) const = 0;
+ virtual AmmClientResult quote(const QJsonObject& request) const = 0;
+ virtual AmmClientResult plan(const QJsonObject& request) const = 0;
+};
+
+class BundledAmmClient final : public AmmClient {
+public:
+ AmmClientResult configId(const QJsonObject& request) const override;
+ AmmClientResult tokenIds(const QJsonObject& request) const override;
+ AmmClientResult pairIds(const QJsonObject& request) const override;
+ AmmClientResult context(const QJsonObject& request) const override;
+ AmmClientResult quote(const QJsonObject& request) const override;
+ AmmClientResult plan(const QJsonObject& request) const override;
+};
diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp
index 6b30cd13..353c4a3a 100644
--- a/apps/amm/src/AmmUiBackend.cpp
+++ b/apps/amm/src/AmmUiBackend.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -20,7 +21,9 @@
#include
#include
+#include "AmmClient.h"
#include "LogosWalletProvider.h"
+#include "NewPositionRuntime.h"
#include "WalletController.h"
#include "logos_api.h"
#include "logos_sdk.h"
@@ -141,18 +144,70 @@ namespace {
}
}
+namespace {
+ const int CHECKPOINT_BLOCK_ID = 10;
+ const int BLOCK_HASH_OFFSET = 40;
+ const int BLOCK_HASH_SIZE = 32;
+
+ QByteArray jsonRpcBody(const QString& method, const QJsonArray& params)
+ {
+ return QJsonDocument(QJsonObject {
+ { QStringLiteral("jsonrpc"), QStringLiteral("2.0") },
+ { QStringLiteral("id"), 1 },
+ { QStringLiteral("method"), method },
+ { QStringLiteral("params"), params },
+ }).toJson(QJsonDocument::Compact);
+ }
+
+ QString blockHashFromResponse(const QByteArray& payload)
+ {
+ QJsonParseError parseError;
+ const QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
+ if (parseError.error != QJsonParseError::NoError || !document.isObject())
+ return {};
+ const QByteArray block =
+ QByteArray::fromBase64(document.object().value(QStringLiteral("result")).toString().toLatin1());
+ if (block.size() < BLOCK_HASH_OFFSET + BLOCK_HASH_SIZE)
+ return {};
+ return QString::fromLatin1(block.mid(BLOCK_HASH_OFFSET, BLOCK_HASH_SIZE).toHex());
+ }
+
+ QString channelIdFromResponse(const QByteArray& payload)
+ {
+ QJsonParseError parseError;
+ const QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
+ if (parseError.error != QJsonParseError::NoError || !document.isObject())
+ return {};
+ const QString channel = document.object().value(QStringLiteral("result")).toString();
+ return ActiveNetwork::isValidIdentity(channel) ? channel : QString();
+ }
+
+}
+
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
: AmmUiBackendSimpleSource(parent),
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
m_logos(std::make_unique(m_logosAPI)),
m_wallet(std::make_unique(m_logosAPI)),
m_walletController(std::make_unique(
- *m_wallet, QStringLiteral("AmmUI")))
+ *m_wallet, QStringLiteral("AmmUI"))),
+ m_ammClient(std::make_unique()),
+ m_newPosition(std::make_unique(m_wallet.get(), m_ammClient.get())),
+ m_net(new QNetworkAccessManager(this))
{
+ setWalletStateReady(false);
+ m_network.load();
+ setNewPositionContext(m_newPosition->context(
+ QVariantMap(), m_network.snapshot(), false, false));
+
connect(m_walletController.get(), &WalletController::stateChanged,
this, &AmmUiBackend::syncWalletState);
syncWalletState();
m_walletController->start();
+ QTimer::singleShot(0, this, [this]() {
+ setWalletStateReady(true);
+ syncWalletState();
+ });
}
AmmUiBackend::~AmmUiBackend() = default;
@@ -162,6 +217,42 @@ WalletAccountModel* AmmUiBackend::accountModel() const
return m_walletController->accountModel();
}
+QString AmmUiBackend::createNewDefault(QString password)
+{
+ setWalletStateReady(false);
+ const QString mnemonic = m_walletController->createDefaultWallet(password);
+ setWalletStateReady(true);
+ syncWalletState();
+ return mnemonic;
+}
+
+QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
+{
+ setWalletStateReady(false);
+ const QString mnemonic =
+ m_walletController->createWallet(configPath, storagePath, password);
+ setWalletStateReady(true);
+ syncWalletState();
+ return mnemonic;
+}
+
+bool AmmUiBackend::openExisting()
+{
+ setWalletStateReady(false);
+ const bool opened = m_walletController->open();
+ setWalletStateReady(true);
+ syncWalletState();
+ return opened;
+}
+
+void AmmUiBackend::disconnectWallet()
+{
+ m_walletController->disconnect();
+ setWalletStateReady(true);
+ m_newPosition->clearWalletAccounts();
+ refreshNewPositionContext(QVariantMap());
+}
+
QString AmmUiBackend::createAccountPublic()
{
return m_walletController->createAccount(true);
@@ -187,31 +278,41 @@ QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
return m_walletController->balance(accountIdHex, isPublic);
}
-QString AmmUiBackend::createNewDefault(QString password)
-{
- return m_walletController->createDefaultWallet(password);
-}
-
-QString AmmUiBackend::createNew(QString configPath,
- QString storagePath,
- QString password)
+void AmmUiBackend::refreshNewPositionContext(QVariantMap request)
{
- return m_walletController->createWallet(configPath, storagePath, password);
+ const bool refreshWalletAccounts =
+ request.take(QStringLiteral("refreshWalletAccounts")).toBool();
+ if (request.contains(QStringLiteral("recentTokenIds"))
+ || request.contains(QStringLiteral("resolvedTokenIds"))) {
+ m_newPositionHints = request;
+ }
+ else {
+ request = m_newPositionHints;
+ }
+ if (m_network.status() == QStringLiteral("network_unknown"))
+ probeNetworkIdentity();
+ setNewPositionContext(m_newPosition->context(
+ request, m_network.snapshot(), isWalletOpen(), refreshWalletAccounts));
}
-bool AmmUiBackend::openExisting()
+QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request)
{
- return m_walletController->open();
+ return m_newPosition->quote(request, m_network.snapshot(), isWalletOpen());
}
-void AmmUiBackend::disconnectWallet()
+QVariantMap AmmUiBackend::submitNewPosition(QVariantMap request, QString quoteHash)
{
- m_walletController->disconnect();
+ return m_newPosition->submit(
+ request, quoteHash, m_network.snapshot(), isWalletOpen());
}
void AmmUiBackend::syncWalletState()
{
const WalletUiState& state = m_walletController->state();
+ const bool walletWasOpen = isWalletOpen();
+ const bool wasReachable = sequencerReachable();
+ const QString previousAddress = sequencerAddr();
+
setIsWalletOpen(state.isWalletOpen);
setWalletExists(state.walletExists);
setConfigPath(state.configPath);
@@ -221,6 +322,70 @@ void AmmUiBackend::syncWalletState()
setCurrentBlockHeight(state.currentBlockHeight);
setSequencerAddr(state.sequencerAddress);
setSequencerReachable(state.sequencerReachable);
+
+ const bool addressChanged = previousAddress != state.sequencerAddress;
+ if (addressChanged)
+ m_network.sequencerChanged(!state.sequencerAddress.isEmpty());
+ if (addressChanged || wasReachable != state.sequencerReachable) {
+ m_network.reachabilityChanged(state.sequencerReachable, wasReachable);
+ }
+ if (walletWasOpen && !state.isWalletOpen)
+ m_newPosition->clearWalletAccounts();
+
+ publishNetworkContext();
+ if (state.sequencerReachable && m_network.needsIdentityProbe())
+ probeNetworkIdentity();
+}
+
+void AmmUiBackend::probeNetworkIdentity()
+{
+ if (m_identityProbeInFlight
+ || !m_network.isConfigured()
+ || sequencerAddr().isEmpty()) {
+ return;
+ }
+ m_identityProbeInFlight = true;
+ m_network.beginIdentityProbe();
+ publishNetworkContext();
+ const QString address = sequencerAddr();
+ const bool devnet = m_network.isDevnet();
+ const QString method = devnet
+ ? QStringLiteral("getChannelId")
+ : QStringLiteral("getBlock");
+ const QJsonArray params = devnet
+ ? QJsonArray()
+ : QJsonArray { CHECKPOINT_BLOCK_ID };
+
+ QNetworkRequest request{QUrl(address)};
+ request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
+ request.setTransferTimeout(4000);
+ QNetworkReply* reply = m_net->post(request, jsonRpcBody(method, params));
+ connect(reply, &QNetworkReply::finished, this, [this, reply, address, devnet]() {
+ m_identityProbeInFlight = false;
+ if (address != sequencerAddr()) {
+ reply->deleteLater();
+ probeNetworkIdentity();
+ return;
+ }
+ if (!sequencerReachable()) {
+ reply->deleteLater();
+ return;
+ }
+
+ const QByteArray payload = reply->readAll();
+ const QString actual = devnet
+ ? channelIdFromResponse(payload)
+ : blockHashFromResponse(payload);
+ m_network.finishIdentityProbe(actual);
+ reply->deleteLater();
+ publishNetworkContext();
+ });
+}
+
+void AmmUiBackend::publishNetworkContext()
+{
+ setNewPositionContext(m_newPosition->context(
+ m_newPositionHints, m_network.snapshot(), isWalletOpen(), false));
}
QString AmmUiBackend::normalizeAccountId(const QString& id)
diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h
index 244ccc6d..df694e6b 100644
--- a/apps/amm/src/AmmUiBackend.h
+++ b/apps/amm/src/AmmUiBackend.h
@@ -3,12 +3,15 @@
#include
+#include
#include
+#include
#include
#include
#include "rep_AmmUiBackend_source.h"
+#include "ActiveNetwork.h"
#include "WalletAccountModel.h"
extern "C" {
@@ -17,9 +20,15 @@ extern "C" {
class LogosAPI;
struct LogosModules;
+class AmmClient;
class LogosWalletProvider;
+class NewPositionRuntime;
+class QNetworkAccessManager;
class WalletController;
+// Source-side implementation of the AmmUiBackend .rep interface.
+// Inheriting from AmmUiBackendSimpleSource gives us the generated PROPs and
+// SLOTs from AmmUiBackend.rep — all the simple ones flow over QtRO.
class AmmUiBackend : public AmmUiBackendSimpleSource {
Q_OBJECT
Q_PROPERTY(WalletAccountModel* accountModel READ accountModel CONSTANT)
@@ -31,11 +40,17 @@ class AmmUiBackend : public AmmUiBackendSimpleSource {
WalletAccountModel* accountModel() const;
public slots:
+ // Overrides of the pure-virtual slots generated from the .rep.
QString createAccountPublic() override;
QString createAccountPrivate() override;
void refreshAccounts() override;
void refreshBalances() override;
QString getBalance(QString accountIdHex, bool isPublic) override;
+ void refreshNewPositionContext(QVariantMap request) override;
+ QVariantMap quoteNewPosition(QVariantMap request) override;
+ QVariantMap submitNewPosition(QVariantMap request, QString quoteHash) override;
+ // Return the new wallet's BIP39 mnemonic (empty string on failure) so the
+ // UI can force a one-time seed-phrase backup step.
QString createNewDefault(QString password) override;
QString createNew(QString configPath, QString storagePath, QString password) override;
bool openExisting() override;
@@ -52,6 +67,8 @@ public slots:
private:
void syncWalletState();
+ void probeNetworkIdentity();
+ void publishNetworkContext();
// Normalizes an account id given as either 64-char lowercase/uppercase hex
// or base58 to lowercase hex. Returns an empty QString if `id` is neither
@@ -72,6 +89,14 @@ public slots:
std::unique_ptr m_logos;
std::unique_ptr m_wallet;
std::unique_ptr m_walletController;
+ std::unique_ptr m_ammClient;
+ std::unique_ptr m_newPosition;
+
+ QNetworkAccessManager* m_net;
+
+ ActiveNetwork m_network;
+ QVariantMap m_newPositionHints;
+ bool m_identityProbeInFlight = false;
};
#endif // AMM_UI_BACKEND_H
diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep
index f389b9dc..447ca286 100644
--- a/apps/amm/src/AmmUiBackend.rep
+++ b/apps/amm/src/AmmUiBackend.rep
@@ -5,6 +5,9 @@
class AmmUiBackend
{
PROP(bool isWalletOpen READONLY)
+ // False while startup or reconnect is still resolving wallet state. This
+ // stays distinct from isWalletOpen because a disconnected wallet is ready.
+ PROP(bool walletStateReady READONLY)
PROP(bool walletExists READONLY)
PROP(QString configPath READONLY)
PROP(QString storagePath READONLY)
@@ -23,8 +26,16 @@ class AmmUiBackend
SLOT(void refreshBalances())
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
+ // New Position backend surface. QML calls these through logos.watch(...).
+ // The QVariant payloads are stable maps/lists so the UI never assembles AMM
+ // transactions or duplicates quote state.
+ PROP(QVariantMap newPositionContext READONLY)
+ SLOT(void refreshNewPositionContext(QVariantMap request))
+ SLOT(QVariantMap quoteNewPosition(QVariantMap request))
+ SLOT(QVariantMap submitNewPosition(QVariantMap request, QString quoteHash))
+
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
- // fresh per-app wallet at walletHome with no path picking. createNew()
+ // fresh wallet at the canonical walletHome with no path picking. createNew()
// keeps explicit paths for an "advanced" flow. Both return the new wallet's
// BIP39 mnemonic (empty on failure) so the UI can force a seed-phrase backup
// before the wallet is usable — this is the only chance to record it.
diff --git a/apps/amm/src/NewPositionRuntime.cpp b/apps/amm/src/NewPositionRuntime.cpp
new file mode 100644
index 00000000..3f016ef6
--- /dev/null
+++ b/apps/amm/src/NewPositionRuntime.cpp
@@ -0,0 +1,390 @@
+#include "NewPositionRuntime.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include "AmmClient.h"
+#include "WalletProvider.h"
+
+namespace {
+ const char SCHEMA[] = "new-position.v1";
+ constexpr qsizetype HASH_BYTES = 32;
+ constexpr std::size_t BASE58_BUFFER_SIZE = 45;
+
+ QString base58TransactionId(const QString& transactionHash)
+ {
+ const QByteArray hex = transactionHash.toLatin1();
+ const QByteArray bytes = QByteArray::fromHex(hex);
+ if (bytes.size() != HASH_BYTES || bytes.toHex() != hex)
+ return {};
+
+ std::array encoded {};
+ std::size_t size = encoded.size();
+ if (!b58enc(encoded.data(), &size, bytes.constData(),
+ static_cast(bytes.size()))) {
+ return {};
+ }
+ return QString::fromLatin1(
+ encoded.data(), static_cast(size - 1));
+ }
+
+ QJsonObject issue(const QString& code,
+ const QJsonArray& blockingFields = {})
+ {
+ return {
+ { QStringLiteral("code"), code },
+ { QStringLiteral("recoverable"), true },
+ { QStringLiteral("blockingFields"), blockingFields },
+ { QStringLiteral("details"), QJsonObject() },
+ };
+ }
+
+ QJsonObject publicError(const QString& code,
+ const QJsonArray& blockingFields = {},
+ const QJsonObject& details = {})
+ {
+ QJsonObject error = issue(code, blockingFields);
+ error.insert(QStringLiteral("details"), details);
+ return {
+ { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
+ { QStringLiteral("status"), QStringLiteral("error") },
+ { QStringLiteral("canSubmit"), false },
+ { QStringLiteral("code"), code },
+ { QStringLiteral("errors"), QJsonArray { error } },
+ { QStringLiteral("warnings"), QJsonArray() },
+ { QStringLiteral("accountPreview"), QJsonArray() },
+ };
+ }
+
+ QJsonObject contextState(const QString& status,
+ const ActiveNetworkSnapshot& network,
+ const QString& code = {})
+ {
+ QJsonObject state {
+ { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
+ { QStringLiteral("status"), status },
+ { QStringLiteral("networkId"), network.id },
+ { QStringLiteral("networkFingerprint"), network.fingerprint },
+ { QStringLiteral("tokens"), QJsonArray() },
+ { QStringLiteral("feeTiers"), QJsonArray() },
+ { QStringLiteral("warnings"), QJsonArray() },
+ };
+ if (!code.isEmpty())
+ state.insert(QStringLiteral("code"), code);
+ return state;
+ }
+
+ QJsonArray variantStringArray(const QVariant& value)
+ {
+ QJsonArray result;
+ for (const QVariant& item : value.toList())
+ result.append(item.toString());
+ return result;
+ }
+
+ QStringList jsonStringList(const QJsonArray& values)
+ {
+ QStringList result;
+ result.reserve(values.size());
+ for (const QJsonValue& value : values)
+ result.append(value.toString());
+ return result;
+ }
+
+ QVector jsonBoolList(const QJsonArray& values)
+ {
+ QVector result;
+ result.reserve(values.size());
+ for (const QJsonValue& value : values)
+ result.append(value.toBool());
+ return result;
+ }
+
+ QVector jsonUIntList(const QJsonArray& values)
+ {
+ QVector result;
+ result.reserve(values.size());
+ for (const QJsonValue& value : values)
+ result.append(static_cast(value.toInteger()));
+ return result;
+ }
+
+ QJsonObject accountReadJson(const WalletAccountRead& read)
+ {
+ QJsonObject result {
+ { QStringLiteral("id"), read.accountId },
+ { QStringLiteral("status"), read.status },
+ };
+ if (read.ok()) {
+ result.insert(QStringLiteral("account"), QJsonObject {
+ { QStringLiteral("program_owner"), read.programOwner },
+ { QStringLiteral("balance"), read.balanceHex },
+ { QStringLiteral("nonce"), read.nonceHex },
+ { QStringLiteral("data"), read.dataHex },
+ });
+ }
+ return result;
+ }
+
+ QJsonArray accountReadsJson(const QVector& reads)
+ {
+ QJsonArray result;
+ for (const WalletAccountRead& read : reads)
+ result.append(accountReadJson(read));
+ return result;
+ }
+}
+
+NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet, AmmClient* client)
+ : m_wallet(wallet),
+ m_client(client)
+{
+}
+
+void NewPositionRuntime::clearWalletAccounts()
+{
+ m_wallet->clearSnapshot();
+}
+
+QJsonArray NewPositionRuntime::walletAccountReads(bool walletOpen, bool refresh) const
+{
+ if (!walletOpen)
+ return {};
+ return accountReadsJson(m_wallet->snapshot(refresh).publicAccountReads);
+}
+
+QJsonObject NewPositionRuntime::buildQuoteInput(const QVariantMap& request,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen,
+ bool freshWalletAccounts,
+ QJsonObject* error) const
+{
+ if (network.status != QStringLiteral("ready")) {
+ *error = publicError(network.status);
+ return {};
+ }
+ const AmmClientResult configResult = m_client->configId(
+ QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
+ if (!configResult.ok) {
+ *error = publicError(QStringLiteral("backend_error"));
+ return {};
+ }
+ const QJsonObject configManifest = configResult.value;
+ const QJsonObject config = accountReadJson(m_wallet->readPublicAccount(
+ configManifest.value(QStringLiteral("configId")).toString()));
+ const QJsonObject requestObject = QJsonObject::fromVariantMap(request);
+ const AmmClientResult pairResult = m_client->pairIds(
+ QJsonObject {
+ { QStringLiteral("ammProgramId"), network.ammProgramId },
+ { QStringLiteral("config"), config },
+ { QStringLiteral("tokenAId"), requestObject.value(QStringLiteral("tokenAId")) },
+ { QStringLiteral("tokenBId"), requestObject.value(QStringLiteral("tokenBId")) },
+ });
+ if (!pairResult.ok) {
+ *error = publicError(QStringLiteral("backend_error"));
+ return {};
+ }
+ const QJsonObject pairManifest = pairResult.value;
+ if (pairManifest.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
+ *error = publicError(pairManifest.value(QStringLiteral("code")).toString());
+ return {};
+ }
+
+ const QJsonArray walletAccounts = walletAccountReads(walletOpen, freshWalletAccounts);
+ const QJsonObject snapshot {
+ { QStringLiteral("config"), config },
+ { QStringLiteral("tokenA"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("tokenAId")).toString())) },
+ { QStringLiteral("tokenB"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("tokenBId")).toString())) },
+ { QStringLiteral("pool"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("poolId")).toString())) },
+ { QStringLiteral("vaultA"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("vaultAId")).toString())) },
+ { QStringLiteral("vaultB"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("vaultBId")).toString())) },
+ { QStringLiteral("lpDefinition"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("lpDefinitionId")).toString())) },
+ { QStringLiteral("lpLockHolding"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("lpLockHoldingId")).toString())) },
+ { QStringLiteral("currentTick"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("currentTickId")).toString())) },
+ { QStringLiteral("clock"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("clockId")).toString())) },
+ { QStringLiteral("walletAvailable"), walletOpen },
+ { QStringLiteral("walletAccounts"), walletAccounts },
+ };
+ return {
+ { QStringLiteral("networkId"), network.id },
+ { QStringLiteral("networkFingerprint"), network.fingerprint },
+ { QStringLiteral("ammProgramId"), network.ammProgramId },
+ { QStringLiteral("request"), requestObject },
+ { QStringLiteral("snapshot"), snapshot },
+ };
+}
+
+QVariantMap NewPositionRuntime::context(const QVariantMap& request,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen,
+ bool refreshWalletAccounts)
+{
+ if (network.status != QStringLiteral("ready"))
+ return contextState(network.status, network).toVariantMap();
+
+ const QJsonArray walletAccounts = walletAccountReads(walletOpen, refreshWalletAccounts);
+
+ const QJsonObject hints = QJsonObject::fromVariantMap(request);
+ const AmmClientResult configResult = m_client->configId(
+ QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
+ if (!configResult.ok)
+ return contextState(
+ QStringLiteral("error"), network, QStringLiteral("backend_error")).toVariantMap();
+
+ const QJsonObject configManifest = configResult.value;
+ const QJsonObject config = accountReadJson(m_wallet->readPublicAccount(
+ configManifest.value(QStringLiteral("configId")).toString()));
+ QJsonArray configured;
+ for (const QString& id : network.tokenIds)
+ configured.append(id);
+ const QJsonArray recent = variantStringArray(hints.value(QStringLiteral("recentTokenIds")).toVariant());
+ const QJsonArray resolved = variantStringArray(hints.value(QStringLiteral("resolvedTokenIds")).toVariant());
+
+ const AmmClientResult tokenResult = m_client->tokenIds(
+ QJsonObject {
+ { QStringLiteral("ammProgramId"), network.ammProgramId },
+ { QStringLiteral("config"), config },
+ { QStringLiteral("walletAccounts"), walletAccounts },
+ { QStringLiteral("configuredTokenIds"), configured },
+ { QStringLiteral("recentTokenIds"), recent },
+ { QStringLiteral("resolvedTokenIds"), resolved },
+ });
+ const QJsonObject tokenManifest = tokenResult.value;
+ if (!tokenResult.ok
+ || tokenManifest.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
+ const QString code = tokenResult.ok
+ ? tokenManifest.value(QStringLiteral("code")).toString()
+ : QStringLiteral("backend_error");
+ return contextState(
+ QStringLiteral("error"),
+ network,
+ code.isEmpty() ? QStringLiteral("backend_error") : code).toVariantMap();
+ }
+
+ QJsonArray definitions;
+ for (const QJsonValue& id : tokenManifest.value(QStringLiteral("tokenIds")).toArray())
+ definitions.append(accountReadJson(m_wallet->readPublicAccount(id.toString())));
+
+ const AmmClientResult contextResult = m_client->context(
+ QJsonObject {
+ { QStringLiteral("networkId"), network.id },
+ { QStringLiteral("networkFingerprint"), network.fingerprint },
+ { QStringLiteral("ammProgramId"), network.ammProgramId },
+ { QStringLiteral("walletAvailable"), walletOpen },
+ { QStringLiteral("config"), config },
+ { QStringLiteral("walletAccounts"), walletAccounts },
+ { QStringLiteral("tokenDefinitions"), definitions },
+ { QStringLiteral("configuredTokenIds"), configured },
+ { QStringLiteral("recentTokenIds"), recent },
+ { QStringLiteral("resolvedTokenIds"), resolved },
+ });
+ return (contextResult.ok
+ ? contextResult.value
+ : contextState(
+ QStringLiteral("error"), network, QStringLiteral("backend_error"))).toVariantMap();
+}
+
+QVariantMap NewPositionRuntime::quote(const QVariantMap& request,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen)
+{
+ QJsonObject error;
+ const QJsonObject input = buildQuoteInput(request, network, walletOpen, false, &error);
+ if (!error.isEmpty())
+ return error.toVariantMap();
+
+ const AmmClientResult result = m_client->quote(input);
+ return (result.ok ? result.value : publicError(QStringLiteral("backend_error"))).toVariantMap();
+}
+
+QVariantMap NewPositionRuntime::submit(const QVariantMap& request,
+ const QString& quoteHash,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen)
+{
+ if (m_submitInFlight)
+ return publicError(QStringLiteral("submit_in_progress")).toVariantMap();
+ if (!walletOpen)
+ return publicError(QStringLiteral("wallet_unavailable")).toVariantMap();
+ QScopedValueRollback submitGuard(m_submitInFlight, true);
+
+ QJsonObject error;
+ const QJsonObject input = buildQuoteInput(request, network, walletOpen, true, &error);
+ if (!error.isEmpty())
+ return error.toVariantMap();
+
+ const AmmClientResult quoteResult = m_client->quote(input);
+ if (!quoteResult.ok)
+ return publicError(QStringLiteral("backend_error")).toVariantMap();
+ const QJsonObject quote = quoteResult.value;
+ if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) {
+ QJsonObject result = publicError(QStringLiteral("quote_changed"));
+ result.insert(QStringLiteral("quote"), quote);
+ return result.toVariantMap();
+ }
+ if (!quote.value(QStringLiteral("canSubmit")).toBool(false)) {
+ QJsonObject result = publicError(QStringLiteral("quote_not_submittable"));
+ result.insert(QStringLiteral("quote"), quote);
+ return result.toVariantMap();
+ }
+
+ QJsonValue freshLp;
+ if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) {
+ const WalletAccountCreation creation = m_wallet->createAccount(true);
+ if (!creation.ok() || !creation.publicAccount.ok())
+ return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
+ freshLp = accountReadJson(creation.publicAccount);
+ }
+
+ QJsonObject planInput = input;
+ planInput.insert(QStringLiteral("quoteHash"), quoteHash);
+ planInput.insert(QStringLiteral("nowMs"), QDateTime::currentMSecsSinceEpoch());
+ if (!freshLp.isUndefined())
+ planInput.insert(QStringLiteral("freshLp"), freshLp);
+
+ const AmmClientResult planResult = m_client->plan(planInput);
+ if (!planResult.ok)
+ return publicError(QStringLiteral("backend_error")).toVariantMap();
+ const QJsonObject plan = planResult.value;
+ if (plan.value(QStringLiteral("status")).toString() != QStringLiteral("ready")) {
+ const QString code = plan.value(QStringLiteral("code")).toString();
+ return publicError(code.isEmpty() ? QStringLiteral("wallet_submission_failed") : code)
+ .toVariantMap();
+ }
+
+ const QStringList accountIds = jsonStringList(plan.value(QStringLiteral("accountIds")).toArray());
+ const QVector signingRequirements = jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray());
+ const QVector instruction = jsonUIntList(plan.value(QStringLiteral("instruction")).toArray());
+ const QString programId = plan.value(QStringLiteral("programId")).toString();
+ bool deadlineValid = false;
+ const qulonglong deadline = plan.value(QStringLiteral("deadlineMs")).toString().toULongLong(&deadlineValid);
+ if (!deadlineValid
+ || static_cast(QDateTime::currentMSecsSinceEpoch()) >= deadline) {
+ return publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap();
+ }
+ const WalletSubmission submission = m_wallet->submitPublicTransaction({
+ programId,
+ accountIds,
+ signingRequirements,
+ instruction,
+ });
+ if (!submission.accepted())
+ return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
+ const QString transactionId = base58TransactionId(submission.nativeHash);
+ if (transactionId.isEmpty())
+ return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
+
+ return QJsonObject {
+ { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
+ { QStringLiteral("status"), QStringLiteral("submitted") },
+ { QStringLiteral("transactionId"), transactionId },
+ { QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) },
+ }.toVariantMap();
+}
diff --git a/apps/amm/src/NewPositionRuntime.h b/apps/amm/src/NewPositionRuntime.h
new file mode 100644
index 00000000..99e190e9
--- /dev/null
+++ b/apps/amm/src/NewPositionRuntime.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "ActiveNetwork.h"
+
+class AmmClient;
+class WalletProvider;
+
+class NewPositionRuntime {
+public:
+ NewPositionRuntime(WalletProvider* wallet, AmmClient* client);
+
+ void clearWalletAccounts();
+
+ QVariantMap context(const QVariantMap& request,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen,
+ bool refreshWalletAccounts);
+ QVariantMap quote(const QVariantMap& request,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen);
+ QVariantMap submit(const QVariantMap& request,
+ const QString& quoteHash,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen);
+
+private:
+ QJsonArray walletAccountReads(bool walletOpen, bool refresh) const;
+ QJsonObject buildQuoteInput(const QVariantMap& request,
+ const ActiveNetworkSnapshot& network,
+ bool walletOpen,
+ bool freshWalletAccounts,
+ QJsonObject* error) const;
+
+ WalletProvider* m_wallet;
+ AmmClient* m_client;
+ bool m_submitInFlight = false;
+};
diff --git a/apps/amm/tests/cpp/ActiveNetworkTest.cpp b/apps/amm/tests/cpp/ActiveNetworkTest.cpp
new file mode 100644
index 00000000..239f3e8d
--- /dev/null
+++ b/apps/amm/tests/cpp/ActiveNetworkTest.cpp
@@ -0,0 +1,65 @@
+#include "ActiveNetwork.h"
+
+#include
+#include
+#include
+#include
+
+namespace {
+ bool expect(bool condition, const char* message)
+ {
+ if (!condition)
+ qCritical("%s", message);
+ return condition;
+ }
+}
+
+int main()
+{
+ const QString identity(64, QLatin1Char('a'));
+ const QString programId(64, QLatin1Char('b'));
+ const QString tokenId(64, QLatin1Char('c'));
+
+ QTemporaryFile config;
+ if (!config.open())
+ return 1;
+ config.write(QJsonDocument(QJsonObject {
+ { QStringLiteral("channelId"), identity },
+ { QStringLiteral("ammProgramId"), programId },
+ { QStringLiteral("tokenDefinitionIds"), QJsonArray { tokenId } },
+ }).toJson(QJsonDocument::Compact));
+ config.flush();
+
+ qputenv("AMM_UI_NETWORK", "devnet");
+ qputenv("AMM_UI_DEVNET_FILE", config.fileName().toLocal8Bit());
+
+ ActiveNetwork network;
+ if (!expect(network.load(), "devnet config should load"))
+ return 1;
+ if (!expect(network.snapshot().status == QStringLiteral("network_unknown"),
+ "loaded network should await identity"))
+ return 1;
+
+ network.sequencerChanged(true);
+ network.finishIdentityProbe(QString(64, QLatin1Char('d')));
+ if (!expect(network.status() == QStringLiteral("network_mismatch"),
+ "wrong identity should block the network"))
+ return 1;
+
+ network.reachabilityChanged(false, true);
+ network.reachabilityChanged(true, false);
+ network.finishIdentityProbe(identity);
+ const ActiveNetworkSnapshot snapshot = network.snapshot();
+ if (!expect(snapshot.status == QStringLiteral("ready"),
+ "matching identity should ready the network"))
+ return 1;
+ if (!expect(snapshot.fingerprint == QStringLiteral("channel:") + identity,
+ "devnet fingerprint should use channel identity"))
+ return 1;
+ if (!expect(snapshot.ammProgramId == programId
+ && snapshot.tokenIds == QStringList { tokenId },
+ "network snapshot should preserve configured program and tokens"))
+ return 1;
+
+ return 0;
+}
diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp
new file mode 100644
index 00000000..38f174f9
--- /dev/null
+++ b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp
@@ -0,0 +1,230 @@
+#include "AmmClient.h"
+#include "NewPositionRuntime.h"
+#include "WalletProvider.h"
+
+#include
+
+namespace {
+ bool expect(bool condition, const char* message)
+ {
+ if (!condition)
+ qCritical("%s", message);
+ return condition;
+ }
+
+ class FakeWallet final : public WalletProvider {
+ public:
+ WalletSession connect(const WalletPaths&) override
+ {
+ return {};
+ }
+
+ WalletCreation createWallet(const WalletPaths&, const QString&) override
+ {
+ return {};
+ }
+
+ WalletSnapshot snapshot(bool) override
+ {
+ return {};
+ }
+
+ void clearSnapshot() override {}
+
+ WalletAccountCreation createAccount(bool isPublic) override
+ {
+ ++createdAccounts;
+ WalletAccountCreation creation;
+ creation.accountId = QStringLiteral("fresh-lp");
+ if (isPublic)
+ creation.publicAccount = readPublicAccount(creation.accountId);
+ return creation;
+ }
+
+ WalletAccountRead readPublicAccount(const QString& accountId) const override
+ {
+ WalletAccountRead read;
+ read.accountId = accountId;
+ read.status = QStringLiteral("ok");
+ return read;
+ }
+
+ WalletSubmission submitPublicTransaction(const WalletTransaction& transaction) override
+ {
+ ++submissions;
+ submitted = transaction;
+ return { WalletFailure::None, transactionHash };
+ }
+
+ void disconnect() override {}
+
+ QString transactionHash = QStringLiteral(
+ "000102030405060708090a0b0c0d0e0f"
+ "101112131415161718191a1b1c1d1e1f");
+ int createdAccounts = 0;
+ int submissions = 0;
+ WalletTransaction submitted;
+ };
+
+ class FakeAmmClient final : public AmmClient {
+ public:
+ AmmClientResult configId(const QJsonObject&) const override
+ {
+ return success({ { QStringLiteral("configId"), QStringLiteral("config") } });
+ }
+
+ AmmClientResult tokenIds(const QJsonObject&) const override
+ {
+ return success({ { QStringLiteral("status"), QStringLiteral("ok") } });
+ }
+
+ AmmClientResult pairIds(const QJsonObject&) const override
+ {
+ return success({
+ { QStringLiteral("status"), QStringLiteral("ok") },
+ { QStringLiteral("tokenAId"), QStringLiteral("token-a") },
+ { QStringLiteral("tokenBId"), QStringLiteral("token-b") },
+ { QStringLiteral("poolId"), QStringLiteral("pool") },
+ { QStringLiteral("vaultAId"), QStringLiteral("vault-a") },
+ { QStringLiteral("vaultBId"), QStringLiteral("vault-b") },
+ { QStringLiteral("lpDefinitionId"), QStringLiteral("lp") },
+ { QStringLiteral("lpLockHoldingId"), QStringLiteral("lp-lock") },
+ { QStringLiteral("currentTickId"), QStringLiteral("tick") },
+ { QStringLiteral("clockId"), QStringLiteral("clock") },
+ });
+ }
+
+ AmmClientResult context(const QJsonObject&) const override
+ {
+ return success({});
+ }
+
+ AmmClientResult quote(const QJsonObject&) const override
+ {
+ return success({
+ { QStringLiteral("schema"), QStringLiteral("new-position.v1") },
+ { QStringLiteral("status"), QStringLiteral("ok") },
+ { QStringLiteral("canSubmit"), true },
+ { QStringLiteral("quoteHash"), quoteHash },
+ { QStringLiteral("requiresFreshLp"), requiresFreshLp },
+ });
+ }
+
+ AmmClientResult plan(const QJsonObject& request) const override
+ {
+ sawFreshLp = request.contains(QStringLiteral("freshLp"));
+ return success({
+ { QStringLiteral("status"), QStringLiteral("ready") },
+ { QStringLiteral("accountIds"), QJsonArray { QStringLiteral("account") } },
+ { QStringLiteral("signingRequirements"), QJsonArray { true } },
+ { QStringLiteral("instruction"), QJsonArray { 1 } },
+ { QStringLiteral("programId"), QStringLiteral("program") },
+ { QStringLiteral("deadlineMs"),
+ QString::number(QDateTime::currentMSecsSinceEpoch() + 60'000) },
+ });
+ }
+
+ static AmmClientResult success(const QJsonObject& value)
+ {
+ return { true, value };
+ }
+
+ QString quoteHash = QStringLiteral("sha256:expected");
+ bool requiresFreshLp = true;
+ mutable bool sawFreshLp = false;
+ };
+
+ ActiveNetworkSnapshot readyNetwork()
+ {
+ return {
+ QStringLiteral("testnet"),
+ QStringLiteral("ready"),
+ QStringLiteral("block10:identity"),
+ QStringLiteral("program"),
+ {},
+ };
+ }
+}
+
+int main()
+{
+ const QVariantMap request {
+ { QStringLiteral("schema"), QStringLiteral("new-position.v1") },
+ { QStringLiteral("tokenAId"), QStringLiteral("token-a") },
+ { QStringLiteral("tokenBId"), QStringLiteral("token-b") },
+ { QStringLiteral("feeBps"), 30 },
+ };
+
+ FakeWallet wallet;
+ FakeAmmClient client;
+ NewPositionRuntime runtime(&wallet, &client);
+ const QVariantMap result = runtime.submit(
+ request, QStringLiteral("sha256:expected"), readyNetwork(), true);
+ if (!expect(result.value(QStringLiteral("status")).toString()
+ == QStringLiteral("submitted"),
+ "valid plan should submit"))
+ return 1;
+ if (!expect(result.value(QStringLiteral("transactionId")).toString()
+ == QStringLiteral("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"),
+ "native hash should become the expected base58 transaction ID"))
+ return 1;
+ if (!expect(wallet.createdAccounts == 1 && client.sawFreshLp,
+ "fresh LP account should enter the plan"))
+ return 1;
+ if (!expect(wallet.submissions == 1
+ && wallet.submitted.accountIds
+ == QStringList { QStringLiteral("account") }
+ && wallet.submitted.signingRequirements.size() == 1
+ && wallet.submitted.signingRequirements.constFirst()
+ && wallet.submitted.instruction.size() == 1
+ && wallet.submitted.instruction.constFirst() == 1
+ && wallet.submitted.programId == QStringLiteral("program"),
+ "runtime should dispatch the unchanged plan once"))
+ return 1;
+
+ FakeWallet orderedBytesWallet;
+ orderedBytesWallet.transactionHash = QStringLiteral(
+ "0102030405060708090a0b0c0d0e0f10"
+ "1112131415161718191a1b1c1d1e1f20");
+ FakeAmmClient orderedBytesClient;
+ orderedBytesClient.requiresFreshLp = false;
+ NewPositionRuntime orderedBytesRuntime(&orderedBytesWallet, &orderedBytesClient);
+ const QVariantMap orderedBytes = orderedBytesRuntime.submit(
+ request, QStringLiteral("sha256:expected"), readyNetwork(), true);
+ if (!expect(orderedBytes.value(QStringLiteral("transactionId")).toString()
+ == QStringLiteral("4wBqpZM9xaSheZzJSMawUKKwhdpChKbZ5eu5ky4Vigw"),
+ "ordered native hash bytes should preserve byte order"))
+ return 1;
+
+ FakeWallet invalidHashWallet;
+ invalidHashWallet.transactionHash = QStringLiteral("not-a-hash");
+ FakeAmmClient invalidHashClient;
+ invalidHashClient.requiresFreshLp = false;
+ NewPositionRuntime invalidHashRuntime(&invalidHashWallet, &invalidHashClient);
+ const QVariantMap invalidHash = invalidHashRuntime.submit(
+ request, QStringLiteral("sha256:expected"), readyNetwork(), true);
+ if (!expect(invalidHash.value(QStringLiteral("code")).toString()
+ == QStringLiteral("wallet_submission_failed")
+ && !invalidHash.contains(QStringLiteral("transactionId")),
+ "invalid native hash should fail without a hex fallback"))
+ return 1;
+ if (!expect(invalidHashWallet.submissions == 1,
+ "hash conversion should happen after wallet submission"))
+ return 1;
+
+ FakeWallet staleWallet;
+ FakeAmmClient staleClient;
+ staleClient.quoteHash = QStringLiteral("sha256:changed");
+ NewPositionRuntime staleRuntime(&staleWallet, &staleClient);
+ const QVariantMap stale = staleRuntime.submit(
+ request, QStringLiteral("sha256:expected"), readyNetwork(), true);
+ if (!expect(stale.value(QStringLiteral("code")).toString()
+ == QStringLiteral("quote_changed"),
+ "changed quote should stop submission"))
+ return 1;
+ if (!expect(staleWallet.createdAccounts == 0 && staleWallet.submissions == 0,
+ "stale quote should have no wallet side effects"))
+ return 1;
+
+ return 0;
+}
diff --git a/apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml b/apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml
new file mode 100644
index 00000000..11ca0268
--- /dev/null
+++ b/apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml
@@ -0,0 +1,29 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtTest
+
+import "../../qml/components/liquidity" as Liquidity
+
+TestCase {
+ id: testCase
+
+ name: "LiquidityConfirmationSummary"
+
+ Component {
+ id: summaryComponent
+
+ Liquidity.LiquidityConfirmationSummary {
+ width: 480
+ }
+ }
+
+ function test_protocolActionsUseDisplayLabels() {
+ var summary = createTemporaryObject(summaryComponent, testCase)
+ verify(summary)
+
+ compare(summary.actionText("NewDefinition"), "Create pool")
+ compare(summary.actionText("AddLiquidity"), "Add liquidity")
+ compare(summary.actionText(""), "-")
+ }
+}
diff --git a/apps/amm/tests/qml/tst_LiquidityPage.qml b/apps/amm/tests/qml/tst_LiquidityPage.qml
new file mode 100644
index 00000000..afc9b4a3
--- /dev/null
+++ b/apps/amm/tests/qml/tst_LiquidityPage.qml
@@ -0,0 +1,335 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtTest
+
+import "../../qml/pages" as Pages
+
+TestCase {
+ id: testCase
+
+ name: "LiquidityPage"
+ readonly property string submittedTransactionId:
+ "1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"
+
+ Component {
+ id: backendComponent
+
+ QtObject {
+ property bool walletStateReady: false
+ property var submitResult: ({})
+ property var quoteResult: ({
+ "schema": "new-position.v1",
+ "status": "ok",
+ "poolStatus": "missing_pool"
+ })
+ property var newPositionContext: ({
+ "schema": "new-position.v1",
+ "status": "ready",
+ "tokens": [],
+ "feeTiers": []
+ })
+
+ function submitNewPosition(request, quoteHash) {
+ return submitResult
+ }
+
+ function quoteNewPosition(request) {
+ return quoteResult
+ }
+ }
+ }
+
+ function test_positionLayoutUsesDesktopRailAndCompactProgress() {
+ var page = createTemporaryObject(pageComponent, testCase)
+ verify(page)
+ page.visible = true
+ wait(0)
+
+ var rail = findChild(page, "positionStepRail")
+ var compactSteps = findChild(page, "compactPositionSteps")
+ var form = findChild(page, "newPositionForm")
+ verify(rail)
+ verify(compactSteps)
+ verify(form)
+
+ compare(page.wideLayout, true)
+ verify(rail.width > 0)
+ verify(form.width > rail.width)
+
+ page.width = 600
+ wait(0)
+
+ compare(page.wideLayout, false)
+ verify(compactSteps.implicitHeight > 0)
+ verify(form.width > 0)
+ verify(form.width <= page.width - 32)
+ }
+
+ Component {
+ id: runtimeComponent
+
+ QtObject {
+ function watch(value, succeeded, failed) {
+ succeeded(value)
+ }
+ }
+ }
+
+ Component {
+ id: pageComponent
+
+ Pages.LiquidityPage {
+ visible: false
+ width: 800
+ height: 600
+ }
+ }
+
+ function test_contextWaitsForWalletState() {
+ var backend = createTemporaryObject(backendComponent, testCase)
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend
+ })
+ verify(backend)
+ verify(page)
+
+ compare(page.flow.newPositionContext.status, "loading")
+
+ backend.walletStateReady = true
+ tryCompare(page.flow.newPositionContext, "status", "ready")
+ }
+
+ function test_contextRefreshControlsWalletScan() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true
+ })
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend
+ })
+ verify(backend)
+ verify(page)
+
+ compare(page.flow.contextHints(false).refreshWalletAccounts, false)
+ compare(page.flow.contextHints(true).refreshWalletAccounts, true)
+ }
+
+ function test_staleContextCompletionCannotFinishNewerRefresh() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true
+ })
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend
+ })
+ verify(backend)
+ verify(page)
+
+ page.flow.contextSerial = 2
+ page.flow.contextLoading = true
+ page.flow.contextErrorCode = "newer_request_pending"
+
+ page.flow.finishContextRefresh(1, null)
+ page.flow.failContextRefresh(1)
+
+ compare(page.flow.contextLoading, true)
+ compare(page.flow.contextErrorCode, "newer_request_pending")
+
+ page.flow.finishContextRefresh(2, null)
+ compare(page.flow.contextLoading, false)
+ compare(page.flow.contextErrorCode, "")
+ }
+
+ function test_submitFailureKeepsReturnedFreshQuoteWithoutRequery() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true
+ })
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend
+ })
+ verify(backend)
+ verify(page)
+
+ page.flow.quoteSerial = 7
+ page.flow.finishSubmitFailure({
+ "schema": "new-position.v1",
+ "status": "error",
+ "code": "quote_not_submittable",
+ "quote": {
+ "schema": "new-position.v1",
+ "status": "ok",
+ "canSubmit": false,
+ "quoteHash": "sha256:fresh"
+ }
+ })
+
+ compare(page.flow.quoteSerial, 7)
+ compare(page.flow.newPositionQuote.quoteHash, "sha256:fresh")
+ compare(page.flow.quoteStale, false)
+ }
+
+ function test_base58SubmittedResultEntersSuccessState() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true,
+ "submitResult": {
+ "schema": "new-position.v1",
+ "status": "submitted",
+ "transactionId": submittedTransactionId,
+ "deadlineMs": String(Date.now() + 60000)
+ }
+ })
+ var runtime = createTemporaryObject(runtimeComponent, testCase)
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend,
+ "runtime": runtime
+ })
+ verify(backend)
+ verify(runtime)
+ verify(page)
+
+ page.flow.confirm({
+ "request": ({}),
+ "quoteHash": "sha256:expected"
+ })
+
+ compare(page.flow.transactionId, submittedTransactionId)
+ compare(page.flow.flowErrorCode, "")
+ compare(page.flow.submitting, false)
+ }
+
+ function test_base58MissingPoolSubmissionStartsPoolWatch() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true,
+ "submitResult": {
+ "schema": "new-position.v1",
+ "status": "submitted",
+ "transactionId": submittedTransactionId,
+ "deadlineMs": String(Date.now() + 60000)
+ }
+ })
+ var runtime = createTemporaryObject(runtimeComponent, testCase)
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend,
+ "runtime": runtime
+ })
+ verify(backend)
+ verify(runtime)
+ verify(page)
+
+ var probe = {
+ "tokenAId": "22222222222222222222222222222222",
+ "tokenBId": "33333333333333333333333333333333"
+ }
+ page.flow.pendingQuoteRequest = { "ok": true, "request": probe }
+ page.flow.confirm({
+ "request": {
+ "initialPriceRealRaw": "18446744073709551616"
+ },
+ "poolProbeRequest": probe,
+ "quoteHash": "sha256:expected"
+ })
+ wait(0)
+
+ compare(page.flow.transactionId, submittedTransactionId)
+ compare(page.flow.pendingPoolProbes.length, 1)
+ compare(page.flow.selectedPoolCreationPending(), true)
+ compare(page.flow.poolProbeInFlight, false)
+ }
+
+ function test_nativeHexSubmittedResultDoesNotEnterSuccessState() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true,
+ "submitResult": {
+ "schema": "new-position.v1",
+ "status": "submitted",
+ "transactionId": "000102030405060708090a0b0c0d0e0f"
+ + "101112131415161718191a1b1c1d1e1f"
+ }
+ })
+ var runtime = createTemporaryObject(runtimeComponent, testCase)
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend,
+ "runtime": runtime
+ })
+ verify(backend)
+ verify(runtime)
+ verify(page)
+
+ page.flow.confirm({
+ "request": ({}),
+ "quoteHash": "sha256:expected"
+ })
+
+ compare(page.flow.transactionId, "")
+ compare(page.flow.flowErrorCode, "wallet_submission_failed")
+ compare(page.flow.submitting, false)
+ }
+
+ function test_poolProbeDoesNotPublishProbeAmountsAsCurrentQuote() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true
+ })
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend
+ })
+ verify(backend)
+ verify(page)
+
+ var request = {
+ "tokenAId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
+ "tokenBId": "22222222222222222222222222222222"
+ }
+ var pending = { "key": page.flow.pairKey(request), "request": request }
+ page.flow.pendingQuoteRequest = { "ok": true, "request": request }
+ page.flow.pendingPoolProbes = [pending]
+ page.flow.newPositionQuote = {
+ "schema": "new-position.v1",
+ "status": "ok",
+ "poolStatus": "missing_pool",
+ "tokenAId": request.tokenAId,
+ "tokenBId": request.tokenBId
+ }
+ page.flow.quoteStale = false
+
+ page.flow.finishPoolProbe(pending, {
+ "schema": "new-position.v1",
+ "status": "ok",
+ "poolStatus": "active_pool",
+ "tokenAId": request.tokenAId,
+ "tokenBId": request.tokenBId
+ })
+
+ compare(page.flow.pendingPoolProbes.length, 0)
+ compare(page.flow.newPositionQuote.poolStatus, "missing_pool")
+ verify(page.flow.quoteStale)
+ }
+
+ function test_poolProbeStopsBlockingAfterTransactionDeadline() {
+ var backend = createTemporaryObject(backendComponent, testCase, {
+ "walletStateReady": true
+ })
+ var page = createTemporaryObject(pageComponent, testCase, {
+ "backend": backend
+ })
+ verify(backend)
+ verify(page)
+
+ var request = {
+ "tokenAId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
+ "tokenBId": "22222222222222222222222222222222"
+ }
+ var pending = {
+ "key": page.flow.pairKey(request),
+ "request": request,
+ "deadlineMs": Date.now() - 1
+ }
+ page.flow.pendingQuoteRequest = { "ok": true, "request": request }
+ page.flow.pendingPoolProbes = [pending]
+ page.flow.poolProbeInFlight = true
+
+ page.flow.finishPoolProbe(pending, null)
+
+ compare(page.flow.pendingPoolProbes.length, 0)
+ compare(page.flow.poolProbeInFlight, false)
+ verify(!page.flow.selectedPoolCreationPending())
+ }
+}
diff --git a/apps/amm/tests/qml/tst_NewPositionForm.qml b/apps/amm/tests/qml/tst_NewPositionForm.qml
new file mode 100644
index 00000000..083be1fa
--- /dev/null
+++ b/apps/amm/tests/qml/tst_NewPositionForm.qml
@@ -0,0 +1,633 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtTest
+
+import "../../qml/components/liquidity" as Liquidity
+
+TestCase {
+ id: testCase
+
+ name: "NewPositionForm"
+
+ readonly property string tokenLow: "22222222222222222222222222222222"
+ readonly property string tokenHigh: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
+ readonly property string tokenThird: "33333333333333333333333333333333"
+ readonly property string submittedTransactionId:
+ "1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"
+
+ Component {
+ id: formComponent
+
+ Liquidity.NewPositionForm {
+ visible: false
+ width: 760
+ newPositionContext: testCase.readyContext()
+ }
+ }
+
+ Component {
+ id: clipboardSinkComponent
+
+ TextEdit {}
+ }
+
+ SignalSpy {
+ id: quoteRequestedSpy
+ signalName: "quoteRequested"
+ }
+
+ function readyContext() {
+ return {
+ "status": "ready",
+ "tokens": [
+ {
+ "definitionId": tokenLow,
+ "name": "Low",
+ "totalSupplyRaw": "1000000",
+ "balanceRaw": "1000",
+ "selectable": true
+ },
+ {
+ "definitionId": tokenHigh,
+ "name": "High",
+ "totalSupplyRaw": "1000000000000",
+ "balanceRaw": "5000000000",
+ "selectable": true
+ }
+ ]
+ }
+ }
+
+ function rawAmountsContext() {
+ return {
+ "status": "ready",
+ "tokens": [
+ {
+ "definitionId": tokenLow,
+ "name": "Sir Mints-a-Lot",
+ "totalSupplyRaw": "1000000000000",
+ "balanceRaw": "1000000000",
+ "selectable": true
+ },
+ {
+ "definitionId": tokenHigh,
+ "name": "Aurora",
+ "totalSupplyRaw": "1000000000000",
+ "balanceRaw": "1000000000",
+ "selectable": true
+ }
+ ]
+ }
+ }
+
+ function flowState(quote) {
+ return {
+ "quote": quote || ({}),
+ "contextLoading": false,
+ "quoteLoading": false,
+ "quoteStale": false,
+ "submitting": false
+ }
+ }
+
+ function createForm() {
+ var form = createTemporaryObject(formComponent, testCase, {
+ "flowState": flowState(({}))
+ })
+ verify(form)
+ wait(0)
+ compare(form.selectedTokenAId, tokenLow)
+ compare(form.selectedTokenBId, tokenHigh)
+ return form
+ }
+
+ function test_displayOrderMapsToCanonicalRequestAfterSwap() {
+ var form = createForm()
+ var built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.tokenAId, tokenHigh)
+ compare(built.request.tokenBId, tokenLow)
+
+ form.swapTokens()
+ compare(form.selectedTokenAId, tokenHigh)
+ compare(form.selectedTokenBId, tokenLow)
+ built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.tokenAId, tokenHigh)
+ compare(built.request.tokenBId, tokenLow)
+ }
+
+ function test_tokenAmountsUseRawUnits() {
+ var form = createForm()
+ compare(form.decimalsA, 0)
+ compare(form.decimalsB, 0)
+ compare(form.balanceText(form.tokenA, form.decimalsA), "1000")
+ compare(form.balanceText(form.tokenB, form.decimalsB), "5000000000")
+ }
+
+ function test_missingPoolKeepsMinimumRatioWhileEditingDeposit() {
+ var quote = {
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "missing_pool",
+ "minimumAmountARaw": "2",
+ "minimumAmountBRaw": "3",
+ "actualAmountARaw": "2",
+ "actualAmountBRaw": "3"
+ }
+ var form = createForm()
+ form.priceAmountA = "3"
+ form.priceAmountB = "2"
+ form.flowState = flowState(quote)
+ wait(0)
+ compare(form.amountA, "3")
+ compare(form.amountB, "2")
+
+ form.editMissingAmount("A", "6")
+ compare(form.amountA, "6")
+ compare(form.amountB, "2")
+
+ form.finishMissingAmount("A", "6")
+ compare(form.amountB, "4")
+
+ var built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.amountARaw, "4")
+ compare(built.request.amountBRaw, "6")
+ }
+
+ function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() {
+ var form = createTemporaryObject(formComponent, testCase, {
+ "flowState": flowState(({})),
+ "newPositionContext": rawAmountsContext()
+ })
+ verify(form)
+ wait(0)
+ form.priceAmountA = "15"
+ form.priceAmountB = "10"
+ form.flowState = flowState({
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "missing_pool",
+ "minimumAmountARaw": "26",
+ "minimumAmountBRaw": "39",
+ "actualAmountARaw": "26",
+ "actualAmountBRaw": "39"
+ })
+ wait(0)
+
+ form.finishMissingAmount("A", "150")
+ compare(form.amountA, "150")
+ compare(form.amountB, "100")
+ var built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.amountARaw, "100")
+ compare(built.request.amountBRaw, "150")
+ compare(built.request.initialPriceRealRaw, "27670116110564327424")
+ verify(!built.request.hasOwnProperty("depositScaleBps"))
+
+ form.finishMissingAmount("B", "200")
+ compare(form.amountA, "300")
+ compare(form.amountB, "200")
+ built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.amountARaw, "200")
+ compare(built.request.amountBRaw, "300")
+ }
+
+ function test_missingPoolRoundsPairedRawAmounts() {
+ var form = createTemporaryObject(formComponent, testCase, {
+ "flowState": flowState(({})),
+ "newPositionContext": rawAmountsContext()
+ })
+ verify(form)
+ wait(0)
+ form.priceAmountA = "15"
+ form.priceAmountB = "10"
+ form.flowState = flowState({
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "missing_pool",
+ "minimumAmountARaw": "1",
+ "minimumAmountBRaw": "1",
+ "actualAmountARaw": "1",
+ "actualAmountBRaw": "1"
+ })
+ wait(0)
+
+ var cases = [
+ { "input": "1", "paired": "1", "rawA": "1", "rawB": "1" },
+ { "input": "2", "paired": "1", "rawA": "1", "rawB": "2" },
+ { "input": "3", "paired": "2", "rawA": "2", "rawB": "3" },
+ { "input": "4", "paired": "3", "rawA": "3", "rawB": "4" }
+ ]
+ for (var i = 0; i < cases.length; ++i) {
+ form.finishMissingAmount("A", cases[i].input)
+ compare(form.amountB, cases[i].paired)
+ var built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.amountARaw, cases[i].rawA)
+ compare(built.request.amountBRaw, cases[i].rawB)
+ }
+
+ form.finishMissingAmount("A", "1.1234567")
+ compare(form.amountA, "1.1234567")
+ verify(!form.buildQuoteRequest().ok)
+ compare(form.fieldError("amountA"), form.issueText("invalid_amount_precision"))
+ }
+
+ function test_missingPoolUsesTradeStyleInputsWithInlinePrice() {
+ var form = createForm()
+ form.flowState = flowState({
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "missing_pool",
+ "minimumAmountARaw": "2000000",
+ "minimumAmountBRaw": "3",
+ "actualAmountARaw": "2000000",
+ "actualAmountBRaw": "3"
+ })
+ wait(0)
+
+ var amountAInput = findChild(form, "tokenAAmountInput")
+ var amountBInput = findChild(form, "tokenBAmountInput")
+ verify(amountAInput)
+ verify(amountBInput)
+ compare(amountAInput.selectedTokenId, tokenLow)
+ compare(amountBInput.selectedTokenId, tokenHigh)
+ verify(amountAInput.height <= 114)
+ verify(amountBInput.height <= 114)
+ verify(findChild(form, "priceAmountAField"))
+ verify(findChild(form, "priceAmountBField"))
+
+ form.width = 328
+ wait(0)
+ compare(amountAInput.adjustmentWidth, 100)
+ compare(amountBInput.adjustmentWidth, 100)
+ }
+
+ function test_errorMessageStaysAbovePairAndMarksCausingControl() {
+ var form = createForm()
+ form.flowState = flowState({
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "missing_pool",
+ "minimumAmountARaw": "2000000",
+ "minimumAmountBRaw": "3",
+ "actualAmountARaw": "2000000",
+ "actualAmountBRaw": "3"
+ })
+ wait(0)
+ var amountAInput = findChild(form, "tokenAAmountInput")
+ var amountBInput = findChild(form, "tokenBAmountInput")
+
+ form.localErrors = [form.localIssue("amount_exceeds_balance", ["amountB"])]
+ wait(0)
+
+ compare(amountAInput.errorText, form.issueText("amount_exceeds_balance"))
+ compare(amountAInput.invalid, false)
+ compare(amountBInput.invalid, true)
+
+ form.resolveToken("B", "invalid")
+ wait(0)
+ compare(amountAInput.errorText, form.issueText("invalid_token_id"))
+ compare(amountAInput.tokenInvalid, false)
+ compare(amountBInput.tokenInvalid, true)
+ }
+
+ function test_staleMissingPoolQuoteDoesNotReplaceDraft() {
+ var quote = {
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "missing_pool",
+ "minimumAmountARaw": "2000000",
+ "minimumAmountBRaw": "3",
+ "actualAmountARaw": "2000000",
+ "actualAmountBRaw": "3"
+ }
+ var form = createForm()
+ form.flowState = flowState(quote)
+ wait(0)
+ compare(form.amountA, "3")
+
+ form.editMissingAmount("A", "2")
+ form.flowState = {
+ "quote": {
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "missing_pool",
+ "minimumAmountARaw": "2000000",
+ "minimumAmountBRaw": "3",
+ "actualAmountARaw": "2000000",
+ "actualAmountBRaw": "3"
+ },
+ "contextLoading": false,
+ "quoteLoading": false,
+ "quoteStale": true,
+ "submitting": false
+ }
+ wait(0)
+
+ compare(form.amountA, "2")
+ }
+
+ function test_poolActivationClearsCreationDraftWithoutPublishingProbeAmounts() {
+ var form = createForm()
+ form.confirmedPoolStatus = "missing_pool"
+ form.amountA = "3"
+ form.amountB = "2"
+ form.minimumAmountARaw = "3"
+ form.minimumAmountBRaw = "2000000"
+
+ verify(form.acceptPoolActivation({
+ "schema": "new-position.v1",
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "active_pool",
+ "reserveARaw": "3000000",
+ "reserveBRaw": "2"
+ }))
+
+ verify(form.activePool)
+ compare(form.activePoolQuote.poolStatus, "active_pool")
+ compare(form.amountA, "")
+ compare(form.amountB, "")
+ compare(form.minimumAmountARaw, "")
+ compare(form.minimumAmountBRaw, "")
+ quoteRequestedSpy.target = form
+ quoteRequestedSpy.clear()
+ form.requestQuote(true)
+ compare(quoteRequestedSpy.count, 0)
+ compare(form.localErrors.length, 0)
+ }
+
+ function test_staleQuoteErrorsDoNotMarkCurrentDraft() {
+ var quote = {
+ "schema": "new-position.v1",
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "active_pool",
+ "reserveARaw": "2",
+ "reserveBRaw": "10",
+ "maxAmountARaw": "4",
+ "maxAmountBRaw": "20",
+ "errors": [{
+ "code": "amount_exceeds_balance",
+ "blockingFields": ["maxAmountARaw"]
+ }]
+ }
+ var form = createForm()
+ form.flowState = flowState(quote)
+ wait(0)
+ compare(form.fieldError("amountB"), form.issueText("amount_exceeds_balance"))
+
+ var staleState = flowState(quote)
+ staleState.quoteStale = true
+ form.flowState = staleState
+ wait(0)
+
+ compare(form.fieldError("amountB"), "")
+ compare(form.formErrorText(), "")
+ compare(form.accountPreview().length, 0)
+ }
+
+ function test_activePoolEditUsesDisplayReserveRatio() {
+ var quote = {
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "active_pool",
+ "reserveARaw": "2",
+ "reserveBRaw": "10",
+ "maxAmountARaw": "4",
+ "maxAmountBRaw": "20"
+ }
+ var form = createForm()
+ form.flowState = flowState(quote)
+ wait(0)
+ compare(form.amountA, "20")
+ compare(form.amountB, "4")
+
+ quoteRequestedSpy.target = form
+ quoteRequestedSpy.clear()
+ form.editActiveAmount("A", "5")
+ compare(form.amountA, "5")
+ compare(form.amountB, "4")
+ compare(quoteRequestedSpy.count, 0)
+
+ form.finishActiveAmount("A", "5")
+ compare(form.amountB, "1")
+ compare(quoteRequestedSpy.count, 1)
+
+ var built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.maxAmountARaw, "1")
+ compare(built.request.maxAmountBRaw, "5")
+
+ form.finishActiveAmount("B", "1.1234567")
+ compare(form.amountB, "1.1234567")
+ verify(!form.buildQuoteRequest().ok)
+ }
+
+ function test_activePoolEditRecoversAfterInvalidQuote() {
+ var form = createForm()
+ form.flowState = flowState({
+ "status": "ok",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "poolStatus": "active_pool",
+ "poolFeeBps": 30,
+ "reserveARaw": "2",
+ "reserveBRaw": "10",
+ "maxAmountARaw": "4",
+ "maxAmountBRaw": "20"
+ })
+ wait(0)
+
+ form.amountA = "0"
+ form.amountB = "0"
+ form.flowState = flowState({
+ "status": "error",
+ "code": "value_must_be_positive",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow
+ })
+ wait(0)
+
+ compare(form.poolFeeBps, 30)
+ form.finishActiveAmount("B", "1")
+ compare(form.amountA, "5")
+
+ var built = form.buildQuoteRequest()
+ verify(built.ok)
+ compare(built.request.maxAmountARaw, "1")
+ compare(built.request.maxAmountBRaw, "5")
+ }
+
+ function test_quoteStateChangeDoesNotRequestAnotherQuote() {
+ var form = createForm()
+ quoteRequestedSpy.target = form
+ quoteRequestedSpy.clear()
+
+ form.flowState = {
+ "quote": ({}),
+ "contextLoading": false,
+ "quoteLoading": true,
+ "quoteStale": true,
+ "submitting": false
+ }
+ wait(0)
+
+ compare(quoteRequestedSpy.count, 0)
+ }
+
+ function test_existingPoolFeeCorrectionKeepsQuoteRequestValid() {
+ var form = createForm()
+ quoteRequestedSpy.target = form
+ quoteRequestedSpy.clear()
+
+ form.flowState = flowState({
+ "status": "error",
+ "code": "fee_tier_mismatch",
+ "tokenAId": tokenHigh,
+ "tokenBId": tokenLow,
+ "errors": [{
+ "code": "fee_tier_mismatch",
+ "details": { "poolFeeBps": "5" }
+ }]
+ })
+ wait(0)
+
+ compare(form.selectedFeeBps, 5)
+ compare(quoteRequestedSpy.count, 1)
+ verify(quoteRequestedSpy.signalArguments[0][1].ok)
+ compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountARaw, "5000000000")
+ compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountBRaw, "1000")
+ }
+
+ function test_contextFailureFinishesTokenResolution() {
+ var form = createForm()
+ form.resolvingTokenId = tokenThird
+ form.resolvingTokenSide = "A"
+
+ form.newPositionContext = {
+ "status": "error",
+ "code": "config_unavailable",
+ "tokens": []
+ }
+ form.finishTokenResolution(true)
+ wait(0)
+
+ compare(form.resolvingTokenId, "")
+ compare(form.resolvingTokenSide, "")
+ compare(form.tokenResolutionError, form.issueText("config_unavailable"))
+ }
+
+ function test_staleContextDoesNotFinishNewerTokenResolution() {
+ var form = createForm()
+ form.resolvingTokenId = tokenThird
+ form.resolvingTokenSide = "B"
+
+ form.newPositionContext = {
+ "status": "ready",
+ "tokens": [{
+ "definitionId": tokenLow,
+ "name": "Earlier token",
+ "selectable": true
+ }]
+ }
+ wait(0)
+
+ compare(form.resolvingTokenId, tokenThird)
+ compare(form.resolvingTokenSide, "B")
+ compare(form.tokenResolutionError, "")
+ }
+
+ function test_replacingSelectedTokensClearsPairDraft() {
+ var form = createForm()
+ form.amountA = "12"
+ form.amountB = "34"
+ form.minimumAmountARaw = "12"
+ form.minimumAmountBRaw = "34"
+ form.confirmedPoolStatus = "active_pool"
+
+ form.newPositionContext = {
+ "status": "ready",
+ "tokens": [
+ {
+ "definitionId": tokenHigh,
+ "name": "High",
+ "totalSupplyRaw": "1000000000000",
+ "balanceRaw": "5000000000",
+ "selectable": true
+ },
+ {
+ "definitionId": tokenThird,
+ "name": "Third",
+ "totalSupplyRaw": "1000000",
+ "balanceRaw": "100",
+ "selectable": true
+ }
+ ]
+ }
+ wait(0)
+
+ compare(form.selectedTokenAId, tokenHigh)
+ compare(form.selectedTokenBId, tokenThird)
+ compare(form.amountA, "")
+ compare(form.amountB, "")
+ compare(form.minimumAmountARaw, "")
+ compare(form.minimumAmountBRaw, "")
+ compare(form.confirmedPoolStatus, "")
+ }
+
+ function test_networkFailurePreservesPairDraft() {
+ var form = createForm()
+ form.amountA = "12"
+ form.amountB = "34"
+ form.confirmedPoolStatus = "active_pool"
+
+ form.newPositionContext = {
+ "status": "network_mismatch",
+ "tokens": []
+ }
+ wait(0)
+
+ compare(form.selectedTokenAId, tokenLow)
+ compare(form.selectedTokenBId, tokenHigh)
+ compare(form.amountA, "12")
+ compare(form.amountB, "34")
+ compare(form.confirmedPoolStatus, "active_pool")
+ verify(form.contextBlocksForm())
+ }
+
+ function test_submittedBase58TransactionIdIsCopied() {
+ var state = flowState(({}))
+ state.transactionId = submittedTransactionId
+ var form = createTemporaryObject(formComponent, testCase, {
+ "flowState": state
+ })
+ var sink = createTemporaryObject(clipboardSinkComponent, testCase)
+ verify(form)
+ verify(sink)
+ wait(0)
+
+ compare(form.transactionId, submittedTransactionId)
+ var copyButton = findChild(form, "copySubmittedTransactionButton")
+ verify(copyButton)
+ copyButton.clicked()
+ verify(copyButton.copied)
+ sink.paste()
+ tryCompare(sink, "text", submittedTransactionId)
+ }
+}
diff --git a/apps/amm/tests/qml/tst_ResponsivePopups.qml b/apps/amm/tests/qml/tst_ResponsivePopups.qml
new file mode 100644
index 00000000..0c56b7fe
--- /dev/null
+++ b/apps/amm/tests/qml/tst_ResponsivePopups.qml
@@ -0,0 +1,47 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtTest
+
+import "../../qml/components/liquidity" as Liquidity
+
+TestCase {
+ id: testCase
+
+ name: "ResponsivePopups"
+
+ Liquidity.AmmTheme {
+ id: theme
+ }
+
+ Component {
+ id: viewportComponent
+
+ Item {
+ width: 320
+ height: 300
+ }
+ }
+
+ Component {
+ id: tokenSelectorComponent
+
+ Liquidity.TokenSelectorModal {
+ theme: theme
+ }
+ }
+
+ function test_tokenSelectorStaysInsideShortViewport() {
+ var viewport = createTemporaryObject(viewportComponent, testCase)
+ var selector = createTemporaryObject(tokenSelectorComponent, viewport, {
+ "parent": viewport
+ })
+ verify(viewport)
+ verify(selector)
+
+ verify(selector.x >= 0)
+ verify(selector.y >= 0)
+ verify(selector.x + selector.width <= viewport.width)
+ verify(selector.y + selector.height <= viewport.height)
+ }
+}
diff --git a/apps/amm/tests/qml/tst_TokenAmountInput.qml b/apps/amm/tests/qml/tst_TokenAmountInput.qml
new file mode 100644
index 00000000..7a10ae33
--- /dev/null
+++ b/apps/amm/tests/qml/tst_TokenAmountInput.qml
@@ -0,0 +1,280 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtTest
+
+import "../../qml/components/liquidity" as Liquidity
+
+TestCase {
+ id: testCase
+
+ name: "TokenAmountInput"
+ readonly property string enabledId: "22222222222222222222222222222222"
+ readonly property string disabledId: "33333333333333333333333333333333"
+
+ Component {
+ id: inputComponent
+
+ Liquidity.TokenAmountInput {
+ visible: false
+ width: 400
+ theme: inputTheme
+ tokens: [
+ {
+ "definitionId": testCase.disabledId,
+ "name": "Disabled",
+ "selectable": false,
+ "code": "token_not_fungible"
+ },
+ {
+ "definitionId": testCase.enabledId,
+ "name": "Enabled",
+ "selectable": true
+ }
+ ]
+
+ Liquidity.AmmTheme {
+ id: inputTheme
+ }
+ }
+ }
+
+ SignalSpy {
+ id: commitSpy
+ signalName: "editingCommitted"
+ }
+
+ SignalSpy {
+ id: selectedSpy
+ signalName: "tokenSelected"
+ }
+
+ SignalSpy {
+ id: enteredSpy
+ signalName: "tokenEntered"
+ }
+
+ function test_inactivityCommitsLatestEditOnce() {
+ var input = createTemporaryObject(inputComponent, testCase)
+ verify(input)
+ commitSpy.target = input
+ commitSpy.clear()
+
+ input.amountEdited("1")
+ wait(150)
+ compare(commitSpy.count, 0)
+
+ input.amountEdited("12")
+ wait(200)
+ compare(commitSpy.count, 0)
+ tryCompare(commitSpy, "count", 1, 150)
+ compare(commitSpy.signalArguments[0][0], "12")
+ }
+
+ function test_editingFinishedCommitsWithoutDuplicate() {
+ var input = createTemporaryObject(inputComponent, testCase)
+ verify(input)
+ commitSpy.target = input
+ commitSpy.clear()
+
+ input.amountEdited("7")
+ input.amountEditingFinished("7")
+ compare(commitSpy.count, 1)
+ compare(commitSpy.signalArguments[0][0], "7")
+
+ wait(300)
+ compare(commitSpy.count, 1)
+ }
+
+ function test_compactWidthShrinksTokenAccessory() {
+ var input = createTemporaryObject(inputComponent, testCase, { "width": 296 })
+ verify(input)
+ compare(input.accessoryWidth, 132)
+
+ input.width = 416
+ compare(input.accessoryWidth, 180)
+ }
+
+ function test_disabledTokenIsRejectedByTypedInput() {
+ var input = createTemporaryObject(inputComponent, testCase)
+ verify(input)
+ selectedSpy.target = input
+ enteredSpy.target = input
+ selectedSpy.clear()
+ enteredSpy.clear()
+
+ input.acceptInput("Disabled")
+
+ compare(selectedSpy.count, 0)
+ compare(enteredSpy.count, 1)
+ compare(enteredSpy.signalArguments[0][0], disabledId)
+ }
+
+ function test_enabledTokenIsSelectedByTypedInput() {
+ var input = createTemporaryObject(inputComponent, testCase)
+ verify(input)
+ selectedSpy.target = input
+ enteredSpy.target = input
+ selectedSpy.clear()
+ enteredSpy.clear()
+
+ input.acceptInput("Enabled")
+
+ compare(selectedSpy.count, 1)
+ compare(selectedSpy.signalArguments[0][0], enabledId)
+ compare(enteredSpy.count, 0)
+ }
+
+ function test_partialNameSelectsSoleMatchInsteadOfCustomAddress() {
+ var input = createTemporaryObject(inputComponent, testCase)
+ verify(input)
+ selectedSpy.target = input
+ enteredSpy.target = input
+ selectedSpy.clear()
+ enteredSpy.clear()
+ input.query = "Enabl"
+ tryCompare(input.rows, "length", 1)
+
+ input.acceptInput("Enabl")
+
+ compare(selectedSpy.count, 1)
+ compare(selectedSpy.signalArguments[0][0], enabledId)
+ compare(enteredSpy.count, 0)
+ }
+
+ function test_disabledTokenCanExplainWhyItIsUnavailable() {
+ var input = createTemporaryObject(inputComponent, testCase, {
+ "visible": true,
+ "width": 320
+ })
+ verify(input)
+
+ input.popup.open()
+ tryCompare(input.popup, "visible", true)
+ var tokenList = findChild(input, "tokenList")
+ verify(tokenList)
+ tryCompare(tokenList, "count", 2)
+ tryVerify(function() { return tokenList.itemAtIndex(0) !== null })
+ var option = tokenList.itemAtIndex(0)
+
+ mouseMove(option, option.width / 2, option.height / 2)
+ tryCompare(option, "pointerHovered", true)
+ compare(option.disabledReasonVisible, true)
+
+ selectedSpy.target = input
+ enteredSpy.target = input
+ selectedSpy.clear()
+ enteredSpy.clear()
+ mouseClick(option, option.width / 2, option.height / 2)
+ compare(selectedSpy.count, 0)
+ compare(enteredSpy.count, 0)
+ }
+
+ function test_tokenListSupportsKeyboardSelection() {
+ var input = createTemporaryObject(inputComponent, testCase, {
+ "visible": true,
+ "width": 320
+ })
+ verify(input)
+ selectedSpy.target = input
+ selectedSpy.clear()
+
+ input.popup.open()
+ tryCompare(input.popup, "visible", true)
+ var tokenList = findChild(input, "tokenList")
+ verify(tokenList)
+ tryVerify(function() { return tokenList.itemAtIndex(1) !== null })
+ var option = tokenList.itemAtIndex(1)
+ option.forceActiveFocus()
+ tryCompare(option, "activeFocus", true)
+
+ keyClick(Qt.Key_Space)
+
+ compare(selectedSpy.count, 1)
+ compare(selectedSpy.signalArguments[0][0], enabledId)
+ }
+
+ function test_disabledReasonAppearsOnKeyboardFocus() {
+ var input = createTemporaryObject(inputComponent, testCase, {
+ "visible": true,
+ "width": 320
+ })
+ verify(input)
+
+ input.popup.open()
+ tryCompare(input.popup, "visible", true)
+ var tokenList = findChild(input, "tokenList")
+ verify(tokenList)
+ tryVerify(function() { return tokenList.itemAtIndex(0) !== null })
+ var option = tokenList.itemAtIndex(0)
+ option.forceActiveFocus()
+
+ tryCompare(option, "activeFocus", true)
+ compare(option.disabledReasonVisible, true)
+ }
+
+ function test_searchModelContainsOnlyMatchingRows() {
+ var input = createTemporaryObject(inputComponent, testCase)
+ verify(input)
+ compare(input.rows.length, 2)
+
+ input.query = "Enabled"
+
+ tryVerify(function() { return input.rows.length === 1 })
+ compare(input.rows[0].definitionId, enabledId)
+ }
+
+ function test_typingKeepsSearchTextWhileModelFilters() {
+ var input = createTemporaryObject(inputComponent, testCase, {
+ "visible": true,
+ "width": 320
+ })
+ verify(input)
+ input.popup.open()
+ tryCompare(input.popup, "visible", true)
+ var editor = findChild(input, "tokenSearchField")
+ verify(editor)
+
+ tryCompare(editor, "activeFocus", true)
+ keyClick(Qt.Key_E)
+ keyClick(Qt.Key_N)
+ keyClick(Qt.Key_A)
+ keyClick(Qt.Key_B)
+ keyClick(Qt.Key_L)
+ keyClick(Qt.Key_E)
+ keyClick(Qt.Key_D)
+
+ compare(editor.text.toLowerCase(), "enabled")
+ compare(input.rows.length, 1)
+ compare(input.rows[0].definitionId, enabledId)
+ }
+
+ function test_clickOpensSharedTokenModal() {
+ var input = createTemporaryObject(inputComponent, testCase, {
+ "visible": true,
+ "width": 320
+ })
+ verify(input)
+ var button = findChild(input, "tokenSelectButton")
+ verify(button)
+
+ button.click()
+
+ tryCompare(input.popup, "visible", true)
+ verify(findChild(input, "tokenSearchField"))
+ verify(findChild(input, "tokenList"))
+ }
+
+ function test_unlistedAddressCanBeEntered() {
+ var input = createTemporaryObject(inputComponent, testCase)
+ verify(input)
+ enteredSpy.target = input
+ enteredSpy.clear()
+ var unlistedId = "44444444444444444444444444444444"
+
+ input.acceptInput(unlistedId)
+
+ compare(enteredSpy.count, 1)
+ compare(enteredSpy.signalArguments[0][0], unlistedId)
+ }
+}
diff --git a/flake.nix b/flake.nix
index 15fc2816..61f36614 100644
--- a/flake.nix
+++ b/flake.nix
@@ -98,10 +98,36 @@
'';
}
);
+
+ # Second AMM client crate (apps/amm/client) — the new-position/pool
+ # flow's protocol lib. Built alongside amm_client_ffi (they coexist:
+ # symbols are prefixed amm_* vs amm_client_*). TODO: consolidate the two
+ # into a single AMM client FFI.
+ ammClientArgs = commonArgs // {
+ pname = "amm_client";
+ cargoExtraArgs = "-p amm_client";
+ };
+ ammClient = craneLib.buildPackage (
+ ammClientArgs
+ // {
+ cargoArtifacts = craneLib.buildDepsOnly ammClientArgs;
+ postInstall =
+ ''
+ mkdir -p $out/include
+ cp apps/amm/client/include/amm_client.h $out/include/
+ ''
+ + pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
+ if [ -f $out/lib/libamm_client.dylib ]; then
+ install_name_tool -id "$out/lib/libamm_client.dylib" $out/lib/libamm_client.dylib
+ fi
+ '';
+ }
+ );
in
{
packages.default = ammClientFfi;
packages.amm_client_ffi = ammClientFfi;
+ packages.amm_client = ammClient;
}
);
@@ -117,6 +143,7 @@
flakeInputs = inputs;
externalLibInputs = {
amm_client_ffi = { input = self; packages.default = "amm_client_ffi"; };
+ amm_client = { input = self; packages.default = "amm_client"; };
};
# The AMM UI links the shared C++ wallet access lib and bundles the
# Logos.Wallet QML module (apps/shared/wallet). apps/amm/flake.nix wires
@@ -160,10 +187,11 @@
let
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; };
ammFfi = crateOutputs.packages.${system}.amm_client_ffi;
+ ammClient = crateOutputs.packages.${system}.amm_client;
in
app // {
program = "${pkgs.writeShellScript "run-amm-ui" ''
- export DYLD_FALLBACK_LIBRARY_PATH="${ammFfi}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}"
+ export DYLD_FALLBACK_LIBRARY_PATH="${ammFfi}/lib:${ammClient}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}"
exec ${app.program} "$@"
''}";
};
From 6cadc86b5484c64a5d52730fe43d88d7330f525c Mon Sep 17 00:00:00 2001
From: Ricardo Guilherme Schmidt <3esmit@gmail.com>
Date: Wed, 15 Jul 2026 21:24:56 -0300
Subject: [PATCH 05/10] fix(amm-ui): require explicit liquidity inputs
Keep wallet assets as token choices without automatically selecting a pair. Keep pool-probe amounts internal so active-pool quotes do not populate the form before user input.
---
.../components/liquidity/NewPositionForm.qml | 61 +++++++++----------
apps/amm/tests/qml/tst_NewPositionForm.qml | 46 ++++++++------
2 files changed, 55 insertions(+), 52 deletions(-)
diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml
index c70000fc..9cd98968 100644
--- a/apps/amm/qml/components/liquidity/NewPositionForm.qml
+++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml
@@ -109,13 +109,13 @@ AmmActionCard {
implicitHeight: content.implicitHeight + root.contentPadding * 2
implicitWidth: 480
- Component.onCompleted: Qt.callLater(root.ensurePair)
+ Component.onCompleted: Qt.callLater(root.reconcileSelection)
onNewPositionContextChanged: Qt.callLater(root.applyContextChange)
function applyContextChange() {
if (root.resolvingToken)
root.finishTokenResolution()
else
- root.ensurePair()
+ root.reconcileSelection()
}
onQuotePayloadChanged: {
if (root.quoteStale)
@@ -786,26 +786,23 @@ AmmActionCard {
root.tokenResolutionErrorSide = side
}
- function ensurePair() {
+ function reconcileSelection() {
var status = String(root.newPositionContext.status || "")
if (status !== "ready" && status !== "no_wallet")
return
var previousA = root.selectedTokenAId
var previousB = root.selectedTokenBId
var selectable = root.selectableTokenIds()
- if (selectable.length === 0) {
+ if (root.selectedTokenAId.length > 0
+ && selectable.indexOf(root.selectedTokenAId) < 0) {
root.selectedTokenAId = ""
- root.selectedTokenBId = ""
- if (previousA.length > 0 || previousB.length > 0)
- root.resetPairDraft()
- return
}
- if (selectable.indexOf(root.selectedTokenAId) < 0)
- root.selectedTokenAId = selectable[0]
- if (selectable.indexOf(root.selectedTokenBId) < 0
- || root.selectedTokenBId === root.selectedTokenAId) {
- root.selectedTokenBId = selectable.length > 1 ? selectable[1] : ""
+ if (root.selectedTokenBId.length > 0
+ && selectable.indexOf(root.selectedTokenBId) < 0) {
+ root.selectedTokenBId = ""
}
+ if (root.selectedTokenBId === root.selectedTokenAId)
+ root.selectedTokenBId = ""
if (root.selectedTokenAId !== previousA || root.selectedTokenBId !== previousB)
root.resetPairDraft()
else if (root.hasPair)
@@ -987,14 +984,7 @@ AmmActionCard {
return { "ok": false, "errors": errors, "request": ({}) }
}
- var canonicalAId = root.displayIsCanonical ? root.selectedTokenAId : root.selectedTokenBId
- var canonicalBId = root.displayIsCanonical ? root.selectedTokenBId : root.selectedTokenAId
- var request = {
- "schema": "new-position.v1",
- "tokenAId": canonicalAId,
- "tokenBId": canonicalBId,
- "feeBps": root.selectedFeeBps
- }
+ var request = root.pairRequest()
if (root.activePool) {
var parsedA = AmountMath.parseHuman(root.amountA, root.decimalsA)
@@ -1085,6 +1075,17 @@ AmmActionCard {
return { "ok": errors.length === 0, "errors": errors, "request": request }
}
+ function pairRequest() {
+ return {
+ "schema": "new-position.v1",
+ "tokenAId": root.displayIsCanonical
+ ? root.selectedTokenAId : root.selectedTokenBId,
+ "tokenBId": root.displayIsCanonical
+ ? root.selectedTokenBId : root.selectedTokenAId,
+ "feeBps": root.selectedFeeBps
+ }
+ }
+
function requestQuote(immediate) {
if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) {
root.localErrors = []
@@ -1327,24 +1328,18 @@ AmmActionCard {
return
if (root.poolFeeBps > 0 && root.selectedFeeBps !== root.poolFeeBps) {
root.selectedFeeBps = root.poolFeeBps
- if (root.amountA.length === 0 && root.amountB.length === 0) {
- root.amountA = AmountMath.formatRaw(
- root.probeRaw(root.tokenA, root.decimalsA), root.decimalsA)
- root.amountB = AmountMath.formatRaw(
- root.probeRaw(root.tokenB, root.decimalsB), root.decimalsB)
- }
- root.requestQuote(true)
+ root.localErrors = []
+ root.quoteRequested(true, {
+ "ok": true,
+ "errors": [],
+ "request": root.poolProbeRequest(root.pairRequest())
+ })
return
}
if (root.quotePayload.status !== "ok")
return
- if (root.activePool && root.amountA.length === 0 && root.amountB.length === 0) {
- root.amountA = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "A"), root.decimalsA)
- root.amountB = AmountMath.formatRaw(root.displayRaw("maxAmountARaw", "maxAmountBRaw", "B"), root.decimalsB)
- }
-
if (root.missingPool) {
var rawA = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "A")
var rawB = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "B")
diff --git a/apps/amm/tests/qml/tst_NewPositionForm.qml b/apps/amm/tests/qml/tst_NewPositionForm.qml
index 083be1fa..d2a81928 100644
--- a/apps/amm/tests/qml/tst_NewPositionForm.qml
+++ b/apps/amm/tests/qml/tst_NewPositionForm.qml
@@ -91,17 +91,33 @@ TestCase {
}
}
- function createForm() {
+ function createEmptyForm(context) {
var form = createTemporaryObject(formComponent, testCase, {
- "flowState": flowState(({}))
+ "flowState": flowState(({})),
+ "newPositionContext": context || readyContext()
})
verify(form)
wait(0)
+ return form
+ }
+
+ function createForm(context) {
+ var form = createEmptyForm(context)
+ form.selectToken("A", tokenLow)
+ form.selectToken("B", tokenHigh)
compare(form.selectedTokenAId, tokenLow)
compare(form.selectedTokenBId, tokenHigh)
return form
}
+ function test_walletTokensDoNotPrefillPosition() {
+ var form = createEmptyForm()
+ compare(form.selectedTokenAId, "")
+ compare(form.selectedTokenBId, "")
+ compare(form.amountA, "")
+ compare(form.amountB, "")
+ }
+
function test_displayOrderMapsToCanonicalRequestAfterSwap() {
var form = createForm()
var built = form.buildQuoteRequest()
@@ -159,12 +175,7 @@ TestCase {
}
function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() {
- var form = createTemporaryObject(formComponent, testCase, {
- "flowState": flowState(({})),
- "newPositionContext": rawAmountsContext()
- })
- verify(form)
- wait(0)
+ var form = createForm(rawAmountsContext())
form.priceAmountA = "15"
form.priceAmountB = "10"
form.flowState = flowState({
@@ -199,12 +210,7 @@ TestCase {
}
function test_missingPoolRoundsPairedRawAmounts() {
- var form = createTemporaryObject(formComponent, testCase, {
- "flowState": flowState(({})),
- "newPositionContext": rawAmountsContext()
- })
- verify(form)
- wait(0)
+ var form = createForm(rawAmountsContext())
form.priceAmountA = "15"
form.priceAmountB = "10"
form.flowState = flowState({
@@ -415,14 +421,14 @@ TestCase {
var form = createForm()
form.flowState = flowState(quote)
wait(0)
- compare(form.amountA, "20")
- compare(form.amountB, "4")
+ compare(form.amountA, "")
+ compare(form.amountB, "")
quoteRequestedSpy.target = form
quoteRequestedSpy.clear()
form.editActiveAmount("A", "5")
compare(form.amountA, "5")
- compare(form.amountB, "4")
+ compare(form.amountB, "")
compare(quoteRequestedSpy.count, 0)
form.finishActiveAmount("A", "5")
@@ -509,6 +515,8 @@ TestCase {
wait(0)
compare(form.selectedFeeBps, 5)
+ compare(form.amountA, "")
+ compare(form.amountB, "")
compare(quoteRequestedSpy.count, 1)
verify(quoteRequestedSpy.signalArguments[0][1].ok)
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountARaw, "5000000000")
@@ -582,8 +590,8 @@ TestCase {
}
wait(0)
- compare(form.selectedTokenAId, tokenHigh)
- compare(form.selectedTokenBId, tokenThird)
+ compare(form.selectedTokenAId, "")
+ compare(form.selectedTokenBId, tokenHigh)
compare(form.amountA, "")
compare(form.amountB, "")
compare(form.minimumAmountARaw, "")
From 81d20cef5bedc6ee2b09e1600d7b38ea551d8eea Mon Sep 17 00:00:00 2001
From: Ricardo Guilherme Schmidt <3esmit@gmail.com>
Date: Thu, 16 Jul 2026 11:40:40 -0300
Subject: [PATCH 06/10] fix(amm): align execution zone dependencies
---
apps/amm/MACOS.md | 173 ++++
apps/amm/README.md | 11 +-
apps/amm/flake.lock | 888 +++++++++---------
apps/amm/scripts/check-macos-prerequisites.sh | 117 +++
4 files changed, 729 insertions(+), 460 deletions(-)
create mode 100644 apps/amm/MACOS.md
create mode 100755 apps/amm/scripts/check-macos-prerequisites.sh
diff --git a/apps/amm/MACOS.md b/apps/amm/MACOS.md
new file mode 100644
index 00000000..0036bc9e
--- /dev/null
+++ b/apps/amm/MACOS.md
@@ -0,0 +1,173 @@
+# AMM UI on macOS
+
+The AMM flake exposes native packages for Apple Silicon and Intel macOS. Nix
+provides the Rust, C++, Qt, and Apple SDK build dependencies. The only required
+host toolchain is Apple's Metal compiler, which Apple distributes through
+Xcode rather than through a redistributable package.
+
+The standalone build needs no input overrides, temporary source pins, or
+`DYLD_LIBRARY_PATH`.
+
+## One-time prerequisites
+
+### Nix
+
+Install Nix in multi-user mode, make sure its daemon is available, and enable
+flakes:
+
+```sh
+mkdir -p ~/.config/nix
+printf '%s\n' 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
+nix --version
+nix store info
+```
+
+Nix normally has `sandbox = false` on macOS. This build must retain that
+setting because RISC Zero invokes Apple's host-installed Metal toolchain. The
+preflight script below reports an actionable error if the daemon is configured
+otherwise.
+
+### Xcode and the Metal Toolchain
+
+Apple Command Line Tools alone do not include `metal` and `metallib`. Install
+the newest full Xcode release supported by the installed macOS from the App
+Store or [Apple Developer Downloads](https://developer.apple.com/download/all/).
+This requires an Apple account and cannot be automated by the repository.
+
+After installation:
+
+```sh
+sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
+sudo xcodebuild -license accept
+sudo xcodebuild -runFirstLaunch
+xcodebuild -downloadComponent MetalToolchain
+xcrun --sdk macosx --find metal
+xcrun --sdk macosx --find metallib
+```
+
+The last two commands must print executable paths. Do not use
+`RISC0_SKIP_BUILD_KERNELS=1`: the pinned dependency graph requires the real
+Metal kernels.
+
+### Disk space and preflight
+
+Leave at least 60 GiB free before downloading Xcode. Once Xcode is installed,
+keep at least 40 GiB free for the first Nix build. Later builds reuse the Nix
+store.
+
+Run the complete machine check from `apps/amm`:
+
+```sh
+bash scripts/check-macos-prerequisites.sh
+```
+
+## Build and run
+
+From `apps/amm`:
+
+```sh
+nix build .
+nix run .
+```
+
+The first cold build is large because it includes Qt, the wallet, the AMM
+client, and the RISC Zero Metal kernels. The first start opens without a
+wallet; select **Connect** to create or open `~/.lee/wallet/`.
+
+## Why the flake is structured this way
+
+Nix 2.25 rejects committed lock entries for unlocked relative flake inputs such
+as `path:../shared/wallet` and `path:../..`. Newer Nix versions may accept that
+layout, which is why it can appear to work on one developer's Mac and fail on
+another.
+
+The portable layout avoids both relative flake inputs:
+
+- the shared wallet is passed to CMake as an ordinary Nix source path;
+- the repository-root AMM client package is imported directly with the same
+ locked `nixpkgs` and Crane inputs;
+- `flake.lock` contains only immutable upstream sources.
+
+The app output uses the builder's full-library QML package layout. The current
+`qt-plugin` layout copies the UI plugin and replica factory but omits sibling
+external libraries such as `libamm_client`. The module metadata still selects
+the C++ backend, so this packaging choice does not change process isolation or
+runtime behavior.
+
+Do not replace these with a pull-request URL, self-reference, or local
+`path:` flake input. A normal `nix flake lock`, `nix build`, and `nix run`
+must work without overrides.
+
+## Known-good validation host
+
+This configuration has been exercised on:
+
+- Apple Silicon (`arm64`)
+- macOS 26.5.2
+- Xcode 26.6 (build 17F113)
+- Metal Toolchain 32023.883
+- Nix 2.25.3
+
+The flake also evaluates its complete package and app outputs for
+`x86_64-darwin`; an Intel Mac is still required for native runtime validation.
+
+## Recovery
+
+### Nix is unavailable
+
+Open a new terminal after installing Nix. For the standard multi-user
+installation, check the store and daemon before trying to bootstrap it again:
+
+```sh
+test -x /nix/var/nix/profiles/default/bin/nix
+nix store info
+sudo launchctl print system/org.nixos.nix-daemon
+```
+
+`launchctl bootstrap` can report an I/O error when a service is already loaded
+or in an inconsistent state; that message alone does not prove the daemon is
+stopped.
+
+### `xcrun` cannot find `metal`
+
+The selected developer directory points at Command Line Tools, or Xcode lacks
+the separately downloaded Metal Toolchain component. Repeat the Xcode setup
+and verification commands above.
+
+Apple does not distribute the Metal compiler as a standalone redistributable
+SDK, so Nix cannot install this host component.
+
+### The build reports that the macOS sandbox blocks Metal
+
+Confirm the active setting:
+
+```sh
+nix config show sandbox
+```
+
+For this native macOS build it must be `false`. If a machine-wide policy set it
+to `true`, update that Nix installation's daemon configuration and restart the
+daemon before retrying.
+
+### The first build runs out of space
+
+Remove only disposable build outputs, then collect dead Nix store paths:
+
+```sh
+rm -rf /private/tmp/amm-build
+nix-store --gc
+```
+
+Do not remove `/nix/store` directly.
+
+### A lock error mentions an unlocked input
+
+Restore the committed app lock and verify it without rewriting it:
+
+```sh
+git restore flake.lock
+nix flake metadata --no-update-lock-file .
+```
+
+If the error still names `path:../shared/wallet` or `path:../..`, the checkout
+does not contain the portable flake fix described above.
diff --git a/apps/amm/README.md b/apps/amm/README.md
index 7219a3a5..97e7044f 100644
--- a/apps/amm/README.md
+++ b/apps/amm/README.md
@@ -50,6 +50,9 @@ nix profile install 'github:logos-co/logos-package-manager#cli'
This makes `lgpm` available as a global command.
+For macOS prerequisites, the Metal toolchain requirement, and common Nix
+recovery steps, see [macOS setup](MACOS.md).
+
## Running the UI standalone
The app is built from the **repository-root** flake (which also provides the
@@ -82,9 +85,11 @@ nix build '.#lgx' --out-link result-lgx
# Portable variant (self-contained, works without nix)
nix build '.#lgx-portable' --out-link result-lgx-portable
-# The core wallet module it depends on. Use the same rev this app pins as the
-# `logos_execution_zone` input in flake.nix so the ImageID/ABI match.
-nix build 'github:logos-blockchain/logos-execution-zone-module?rev=d2e9400ac06c3cdbfc2405b4f153fff9841a453c#lgx' \
+# The core wallet module it depends on. These are the same immutable upstream
+# revisions used by this app's flake, including the merged macOS Metal fix.
+nix build 'github:logos-blockchain/logos-execution-zone-module?rev=d70225ced646934d2294fd9e8f8b03615c104b80#lgx' \
+ --override-input logos-execution-zone \
+ 'github:logos-blockchain/logos-execution-zone?rev=a7e06a660940a00093b1760560d37ff84aff5a05' \
--out-link result-core
```
diff --git a/apps/amm/flake.lock b/apps/amm/flake.lock
index f5272d3d..c272fd5b 100644
--- a/apps/amm/flake.lock
+++ b/apps/amm/flake.lock
@@ -1,31 +1,17 @@
{
"nodes": {
- "amm_client": {
- "inputs": {
- "crane": "crane",
- "nixpkgs": "nixpkgs"
- },
- "locked": {
- "path": "../..",
- "type": "path"
- },
- "original": {
- "path": "../..",
- "type": "path"
- },
- "parent": []
- },
"crane": {
"locked": {
- "lastModified": 1783203018,
- "narHash": "sha256-G6R9IT/xwFuu+CYBWDUAok6AdC4ERC4ZfPPFtEpxnZE=",
+ "lastModified": 1779041105,
+ "narHash": "sha256-nnGD2f8OlAZT2i5OfwikJsw+ifWfiA4d6A8BWlgOXV0=",
"owner": "ipetkov",
"repo": "crane",
- "rev": "80db5bdc391be8a1794f6d8a2d56e3a84ebcede2",
+ "rev": "10e6e3cb966f7cfcc789fe5eee7a85f3188ce08b",
"type": "github"
},
"original": {
"owner": "ipetkov",
+ "ref": "v0.23.4",
"repo": "crane",
"type": "github"
}
@@ -106,7 +92,7 @@
},
"logos-blockchain-circuits": {
"inputs": {
- "nixpkgs": "nixpkgs_125"
+ "nixpkgs": "nixpkgs_124"
},
"locked": {
"lastModified": 1781004244,
@@ -2268,17 +2254,17 @@
"rust-rapidsnark": "rust-rapidsnark"
},
"locked": {
- "lastModified": 1782428741,
- "narHash": "sha256-ltLcysXUdVUXAe25Tl8x7e7ZsTzj1sHlyS3glp97TAo=",
+ "lastModified": 1784202021,
+ "narHash": "sha256-BG94Ob5tbJyw7GclNBdnOWqcBLRld8sc+xeOU8LI5RQ=",
"owner": "logos-blockchain",
"repo": "logos-execution-zone",
- "rev": "e37876a64028a335eb693198a1ed6a0e875ec5b4",
+ "rev": "a7e06a660940a00093b1760560d37ff84aff5a05",
"type": "github"
},
"original": {
"owner": "logos-blockchain",
"repo": "logos-execution-zone",
- "rev": "e37876a64028a335eb693198a1ed6a0e875ec5b4",
+ "rev": "a7e06a660940a00093b1760560d37ff84aff5a05",
"type": "github"
}
},
@@ -4793,7 +4779,7 @@
},
"logos-nix": {
"inputs": {
- "nixpkgs": "nixpkgs_2"
+ "nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1774455309,
@@ -4811,7 +4797,7 @@
},
"logos-nix_10": {
"inputs": {
- "nixpkgs": "nixpkgs_11"
+ "nixpkgs": "nixpkgs_10"
},
"locked": {
"lastModified": 1774455309,
@@ -4829,7 +4815,7 @@
},
"logos-nix_100": {
"inputs": {
- "nixpkgs": "nixpkgs_101"
+ "nixpkgs": "nixpkgs_100"
},
"locked": {
"lastModified": 1773955630,
@@ -4847,7 +4833,7 @@
},
"logos-nix_101": {
"inputs": {
- "nixpkgs": "nixpkgs_102"
+ "nixpkgs": "nixpkgs_101"
},
"locked": {
"lastModified": 1773955630,
@@ -4865,7 +4851,7 @@
},
"logos-nix_102": {
"inputs": {
- "nixpkgs": "nixpkgs_103"
+ "nixpkgs": "nixpkgs_102"
},
"locked": {
"lastModified": 1773955630,
@@ -4883,7 +4869,7 @@
},
"logos-nix_103": {
"inputs": {
- "nixpkgs": "nixpkgs_104"
+ "nixpkgs": "nixpkgs_103"
},
"locked": {
"lastModified": 1773955630,
@@ -4901,7 +4887,7 @@
},
"logos-nix_104": {
"inputs": {
- "nixpkgs": "nixpkgs_105"
+ "nixpkgs": "nixpkgs_104"
},
"locked": {
"lastModified": 1773955630,
@@ -4919,7 +4905,7 @@
},
"logos-nix_105": {
"inputs": {
- "nixpkgs": "nixpkgs_106"
+ "nixpkgs": "nixpkgs_105"
},
"locked": {
"lastModified": 1774455309,
@@ -4937,7 +4923,7 @@
},
"logos-nix_106": {
"inputs": {
- "nixpkgs": "nixpkgs_107"
+ "nixpkgs": "nixpkgs_106"
},
"locked": {
"lastModified": 1773955630,
@@ -4955,7 +4941,7 @@
},
"logos-nix_107": {
"inputs": {
- "nixpkgs": "nixpkgs_108"
+ "nixpkgs": "nixpkgs_107"
},
"locked": {
"lastModified": 1774455309,
@@ -4973,7 +4959,7 @@
},
"logos-nix_108": {
"inputs": {
- "nixpkgs": "nixpkgs_109"
+ "nixpkgs": "nixpkgs_108"
},
"locked": {
"lastModified": 1774455309,
@@ -4991,7 +4977,7 @@
},
"logos-nix_109": {
"inputs": {
- "nixpkgs": "nixpkgs_110"
+ "nixpkgs": "nixpkgs_109"
},
"locked": {
"lastModified": 1773955630,
@@ -5009,7 +4995,7 @@
},
"logos-nix_11": {
"inputs": {
- "nixpkgs": "nixpkgs_12"
+ "nixpkgs": "nixpkgs_11"
},
"locked": {
"lastModified": 1773955630,
@@ -5027,7 +5013,7 @@
},
"logos-nix_110": {
"inputs": {
- "nixpkgs": "nixpkgs_111"
+ "nixpkgs": "nixpkgs_110"
},
"locked": {
"lastModified": 1773955630,
@@ -5045,7 +5031,7 @@
},
"logos-nix_111": {
"inputs": {
- "nixpkgs": "nixpkgs_112"
+ "nixpkgs": "nixpkgs_111"
},
"locked": {
"lastModified": 1774455309,
@@ -5063,7 +5049,7 @@
},
"logos-nix_112": {
"inputs": {
- "nixpkgs": "nixpkgs_113"
+ "nixpkgs": "nixpkgs_112"
},
"locked": {
"lastModified": 1774455309,
@@ -5081,7 +5067,7 @@
},
"logos-nix_113": {
"inputs": {
- "nixpkgs": "nixpkgs_114"
+ "nixpkgs": "nixpkgs_113"
},
"locked": {
"lastModified": 1773955630,
@@ -5099,7 +5085,7 @@
},
"logos-nix_114": {
"inputs": {
- "nixpkgs": "nixpkgs_115"
+ "nixpkgs": "nixpkgs_114"
},
"locked": {
"lastModified": 1773955630,
@@ -5117,7 +5103,7 @@
},
"logos-nix_115": {
"inputs": {
- "nixpkgs": "nixpkgs_116"
+ "nixpkgs": "nixpkgs_115"
},
"locked": {
"lastModified": 1774455309,
@@ -5135,7 +5121,7 @@
},
"logos-nix_116": {
"inputs": {
- "nixpkgs": "nixpkgs_117"
+ "nixpkgs": "nixpkgs_116"
},
"locked": {
"lastModified": 1774455309,
@@ -5153,7 +5139,7 @@
},
"logos-nix_117": {
"inputs": {
- "nixpkgs": "nixpkgs_118"
+ "nixpkgs": "nixpkgs_117"
},
"locked": {
"lastModified": 1773955630,
@@ -5171,7 +5157,7 @@
},
"logos-nix_118": {
"inputs": {
- "nixpkgs": "nixpkgs_119"
+ "nixpkgs": "nixpkgs_118"
},
"locked": {
"lastModified": 1773955630,
@@ -5189,7 +5175,7 @@
},
"logos-nix_119": {
"inputs": {
- "nixpkgs": "nixpkgs_120"
+ "nixpkgs": "nixpkgs_119"
},
"locked": {
"lastModified": 1773955630,
@@ -5207,7 +5193,7 @@
},
"logos-nix_12": {
"inputs": {
- "nixpkgs": "nixpkgs_13"
+ "nixpkgs": "nixpkgs_12"
},
"locked": {
"lastModified": 1774455309,
@@ -5225,7 +5211,7 @@
},
"logos-nix_120": {
"inputs": {
- "nixpkgs": "nixpkgs_121"
+ "nixpkgs": "nixpkgs_120"
},
"locked": {
"lastModified": 1773955630,
@@ -5243,7 +5229,7 @@
},
"logos-nix_121": {
"inputs": {
- "nixpkgs": "nixpkgs_122"
+ "nixpkgs": "nixpkgs_121"
},
"locked": {
"lastModified": 1774455309,
@@ -5261,7 +5247,7 @@
},
"logos-nix_122": {
"inputs": {
- "nixpkgs": "nixpkgs_123"
+ "nixpkgs": "nixpkgs_122"
},
"locked": {
"lastModified": 1773955630,
@@ -5279,7 +5265,7 @@
},
"logos-nix_123": {
"inputs": {
- "nixpkgs": "nixpkgs_124"
+ "nixpkgs": "nixpkgs_123"
},
"locked": {
"lastModified": 1773955630,
@@ -5297,7 +5283,7 @@
},
"logos-nix_124": {
"inputs": {
- "nixpkgs": "nixpkgs_126"
+ "nixpkgs": "nixpkgs_125"
},
"locked": {
"lastModified": 1774455309,
@@ -5315,7 +5301,7 @@
},
"logos-nix_125": {
"inputs": {
- "nixpkgs": "nixpkgs_127"
+ "nixpkgs": "nixpkgs_126"
},
"locked": {
"lastModified": 1774455309,
@@ -5333,7 +5319,7 @@
},
"logos-nix_126": {
"inputs": {
- "nixpkgs": "nixpkgs_128"
+ "nixpkgs": "nixpkgs_127"
},
"locked": {
"lastModified": 1774455309,
@@ -5351,7 +5337,7 @@
},
"logos-nix_127": {
"inputs": {
- "nixpkgs": "nixpkgs_129"
+ "nixpkgs": "nixpkgs_128"
},
"locked": {
"lastModified": 1774455309,
@@ -5369,7 +5355,7 @@
},
"logos-nix_128": {
"inputs": {
- "nixpkgs": "nixpkgs_130"
+ "nixpkgs": "nixpkgs_129"
},
"locked": {
"lastModified": 1773955630,
@@ -5387,7 +5373,7 @@
},
"logos-nix_129": {
"inputs": {
- "nixpkgs": "nixpkgs_131"
+ "nixpkgs": "nixpkgs_130"
},
"locked": {
"lastModified": 1774455309,
@@ -5405,7 +5391,7 @@
},
"logos-nix_13": {
"inputs": {
- "nixpkgs": "nixpkgs_14"
+ "nixpkgs": "nixpkgs_13"
},
"locked": {
"lastModified": 1773955630,
@@ -5423,7 +5409,7 @@
},
"logos-nix_130": {
"inputs": {
- "nixpkgs": "nixpkgs_132"
+ "nixpkgs": "nixpkgs_131"
},
"locked": {
"lastModified": 1774455309,
@@ -5441,7 +5427,7 @@
},
"logos-nix_131": {
"inputs": {
- "nixpkgs": "nixpkgs_133"
+ "nixpkgs": "nixpkgs_132"
},
"locked": {
"lastModified": 1774455309,
@@ -5459,7 +5445,7 @@
},
"logos-nix_132": {
"inputs": {
- "nixpkgs": "nixpkgs_134"
+ "nixpkgs": "nixpkgs_133"
},
"locked": {
"lastModified": 1774455309,
@@ -5477,7 +5463,7 @@
},
"logos-nix_133": {
"inputs": {
- "nixpkgs": "nixpkgs_135"
+ "nixpkgs": "nixpkgs_134"
},
"locked": {
"lastModified": 1774455309,
@@ -5495,7 +5481,7 @@
},
"logos-nix_134": {
"inputs": {
- "nixpkgs": "nixpkgs_136"
+ "nixpkgs": "nixpkgs_135"
},
"locked": {
"lastModified": 1774455309,
@@ -5513,7 +5499,7 @@
},
"logos-nix_135": {
"inputs": {
- "nixpkgs": "nixpkgs_137"
+ "nixpkgs": "nixpkgs_136"
},
"locked": {
"lastModified": 1773955630,
@@ -5531,7 +5517,7 @@
},
"logos-nix_136": {
"inputs": {
- "nixpkgs": "nixpkgs_138"
+ "nixpkgs": "nixpkgs_137"
},
"locked": {
"lastModified": 1774455309,
@@ -5549,7 +5535,7 @@
},
"logos-nix_137": {
"inputs": {
- "nixpkgs": "nixpkgs_139"
+ "nixpkgs": "nixpkgs_138"
},
"locked": {
"lastModified": 1773955630,
@@ -5567,7 +5553,7 @@
},
"logos-nix_138": {
"inputs": {
- "nixpkgs": "nixpkgs_140"
+ "nixpkgs": "nixpkgs_139"
},
"locked": {
"lastModified": 1774455309,
@@ -5585,7 +5571,7 @@
},
"logos-nix_139": {
"inputs": {
- "nixpkgs": "nixpkgs_141"
+ "nixpkgs": "nixpkgs_140"
},
"locked": {
"lastModified": 1773955630,
@@ -5603,7 +5589,7 @@
},
"logos-nix_14": {
"inputs": {
- "nixpkgs": "nixpkgs_15"
+ "nixpkgs": "nixpkgs_14"
},
"locked": {
"lastModified": 1774455309,
@@ -5621,7 +5607,7 @@
},
"logos-nix_140": {
"inputs": {
- "nixpkgs": "nixpkgs_142"
+ "nixpkgs": "nixpkgs_141"
},
"locked": {
"lastModified": 1774455309,
@@ -5639,7 +5625,7 @@
},
"logos-nix_141": {
"inputs": {
- "nixpkgs": "nixpkgs_143"
+ "nixpkgs": "nixpkgs_142"
},
"locked": {
"lastModified": 1774455309,
@@ -5657,7 +5643,7 @@
},
"logos-nix_142": {
"inputs": {
- "nixpkgs": "nixpkgs_144"
+ "nixpkgs": "nixpkgs_143"
},
"locked": {
"lastModified": 1773955630,
@@ -5675,7 +5661,7 @@
},
"logos-nix_143": {
"inputs": {
- "nixpkgs": "nixpkgs_145"
+ "nixpkgs": "nixpkgs_144"
},
"locked": {
"lastModified": 1774455309,
@@ -5693,7 +5679,7 @@
},
"logos-nix_144": {
"inputs": {
- "nixpkgs": "nixpkgs_146"
+ "nixpkgs": "nixpkgs_145"
},
"locked": {
"lastModified": 1773955630,
@@ -5711,7 +5697,7 @@
},
"logos-nix_145": {
"inputs": {
- "nixpkgs": "nixpkgs_147"
+ "nixpkgs": "nixpkgs_146"
},
"locked": {
"lastModified": 1774455309,
@@ -5729,7 +5715,7 @@
},
"logos-nix_146": {
"inputs": {
- "nixpkgs": "nixpkgs_148"
+ "nixpkgs": "nixpkgs_147"
},
"locked": {
"lastModified": 1773955630,
@@ -5747,7 +5733,7 @@
},
"logos-nix_147": {
"inputs": {
- "nixpkgs": "nixpkgs_149"
+ "nixpkgs": "nixpkgs_148"
},
"locked": {
"lastModified": 1774455309,
@@ -5765,7 +5751,7 @@
},
"logos-nix_148": {
"inputs": {
- "nixpkgs": "nixpkgs_150"
+ "nixpkgs": "nixpkgs_149"
},
"locked": {
"lastModified": 1773955630,
@@ -5783,7 +5769,7 @@
},
"logos-nix_149": {
"inputs": {
- "nixpkgs": "nixpkgs_151"
+ "nixpkgs": "nixpkgs_150"
},
"locked": {
"lastModified": 1773955630,
@@ -5801,7 +5787,7 @@
},
"logos-nix_15": {
"inputs": {
- "nixpkgs": "nixpkgs_16"
+ "nixpkgs": "nixpkgs_15"
},
"locked": {
"lastModified": 1773955630,
@@ -5819,7 +5805,7 @@
},
"logos-nix_150": {
"inputs": {
- "nixpkgs": "nixpkgs_152"
+ "nixpkgs": "nixpkgs_151"
},
"locked": {
"lastModified": 1774455309,
@@ -5837,7 +5823,7 @@
},
"logos-nix_151": {
"inputs": {
- "nixpkgs": "nixpkgs_153"
+ "nixpkgs": "nixpkgs_152"
},
"locked": {
"lastModified": 1773955630,
@@ -5855,7 +5841,7 @@
},
"logos-nix_152": {
"inputs": {
- "nixpkgs": "nixpkgs_154"
+ "nixpkgs": "nixpkgs_153"
},
"locked": {
"lastModified": 1773955630,
@@ -5873,7 +5859,7 @@
},
"logos-nix_153": {
"inputs": {
- "nixpkgs": "nixpkgs_155"
+ "nixpkgs": "nixpkgs_154"
},
"locked": {
"lastModified": 1773955630,
@@ -5891,7 +5877,7 @@
},
"logos-nix_154": {
"inputs": {
- "nixpkgs": "nixpkgs_156"
+ "nixpkgs": "nixpkgs_155"
},
"locked": {
"lastModified": 1773955630,
@@ -5909,7 +5895,7 @@
},
"logos-nix_155": {
"inputs": {
- "nixpkgs": "nixpkgs_157"
+ "nixpkgs": "nixpkgs_156"
},
"locked": {
"lastModified": 1774455309,
@@ -5927,7 +5913,7 @@
},
"logos-nix_156": {
"inputs": {
- "nixpkgs": "nixpkgs_158"
+ "nixpkgs": "nixpkgs_157"
},
"locked": {
"lastModified": 1773955630,
@@ -5945,7 +5931,7 @@
},
"logos-nix_157": {
"inputs": {
- "nixpkgs": "nixpkgs_159"
+ "nixpkgs": "nixpkgs_158"
},
"locked": {
"lastModified": 1774455309,
@@ -5963,7 +5949,7 @@
},
"logos-nix_158": {
"inputs": {
- "nixpkgs": "nixpkgs_160"
+ "nixpkgs": "nixpkgs_159"
},
"locked": {
"lastModified": 1774455309,
@@ -5981,7 +5967,7 @@
},
"logos-nix_159": {
"inputs": {
- "nixpkgs": "nixpkgs_161"
+ "nixpkgs": "nixpkgs_160"
},
"locked": {
"lastModified": 1773955630,
@@ -5999,7 +5985,7 @@
},
"logos-nix_16": {
"inputs": {
- "nixpkgs": "nixpkgs_17"
+ "nixpkgs": "nixpkgs_16"
},
"locked": {
"lastModified": 1774455309,
@@ -6017,7 +6003,7 @@
},
"logos-nix_160": {
"inputs": {
- "nixpkgs": "nixpkgs_162"
+ "nixpkgs": "nixpkgs_161"
},
"locked": {
"lastModified": 1773955630,
@@ -6035,7 +6021,7 @@
},
"logos-nix_161": {
"inputs": {
- "nixpkgs": "nixpkgs_163"
+ "nixpkgs": "nixpkgs_162"
},
"locked": {
"lastModified": 1773955630,
@@ -6053,7 +6039,7 @@
},
"logos-nix_162": {
"inputs": {
- "nixpkgs": "nixpkgs_164"
+ "nixpkgs": "nixpkgs_163"
},
"locked": {
"lastModified": 1773955630,
@@ -6071,7 +6057,7 @@
},
"logos-nix_163": {
"inputs": {
- "nixpkgs": "nixpkgs_165"
+ "nixpkgs": "nixpkgs_164"
},
"locked": {
"lastModified": 1773955630,
@@ -6089,7 +6075,7 @@
},
"logos-nix_164": {
"inputs": {
- "nixpkgs": "nixpkgs_166"
+ "nixpkgs": "nixpkgs_165"
},
"locked": {
"lastModified": 1774455309,
@@ -6107,7 +6093,7 @@
},
"logos-nix_165": {
"inputs": {
- "nixpkgs": "nixpkgs_167"
+ "nixpkgs": "nixpkgs_166"
},
"locked": {
"lastModified": 1773955630,
@@ -6125,7 +6111,7 @@
},
"logos-nix_166": {
"inputs": {
- "nixpkgs": "nixpkgs_168"
+ "nixpkgs": "nixpkgs_167"
},
"locked": {
"lastModified": 1774455309,
@@ -6143,7 +6129,7 @@
},
"logos-nix_167": {
"inputs": {
- "nixpkgs": "nixpkgs_169"
+ "nixpkgs": "nixpkgs_168"
},
"locked": {
"lastModified": 1774455309,
@@ -6161,7 +6147,7 @@
},
"logos-nix_168": {
"inputs": {
- "nixpkgs": "nixpkgs_170"
+ "nixpkgs": "nixpkgs_169"
},
"locked": {
"lastModified": 1773955630,
@@ -6179,7 +6165,7 @@
},
"logos-nix_169": {
"inputs": {
- "nixpkgs": "nixpkgs_171"
+ "nixpkgs": "nixpkgs_170"
},
"locked": {
"lastModified": 1773955630,
@@ -6197,7 +6183,7 @@
},
"logos-nix_17": {
"inputs": {
- "nixpkgs": "nixpkgs_18"
+ "nixpkgs": "nixpkgs_17"
},
"locked": {
"lastModified": 1773955630,
@@ -6215,7 +6201,7 @@
},
"logos-nix_170": {
"inputs": {
- "nixpkgs": "nixpkgs_172"
+ "nixpkgs": "nixpkgs_171"
},
"locked": {
"lastModified": 1774455309,
@@ -6233,7 +6219,7 @@
},
"logos-nix_171": {
"inputs": {
- "nixpkgs": "nixpkgs_173"
+ "nixpkgs": "nixpkgs_172"
},
"locked": {
"lastModified": 1774455309,
@@ -6251,7 +6237,7 @@
},
"logos-nix_172": {
"inputs": {
- "nixpkgs": "nixpkgs_174"
+ "nixpkgs": "nixpkgs_173"
},
"locked": {
"lastModified": 1773955630,
@@ -6269,7 +6255,7 @@
},
"logos-nix_173": {
"inputs": {
- "nixpkgs": "nixpkgs_175"
+ "nixpkgs": "nixpkgs_174"
},
"locked": {
"lastModified": 1773955630,
@@ -6287,7 +6273,7 @@
},
"logos-nix_174": {
"inputs": {
- "nixpkgs": "nixpkgs_176"
+ "nixpkgs": "nixpkgs_175"
},
"locked": {
"lastModified": 1774455309,
@@ -6305,7 +6291,7 @@
},
"logos-nix_175": {
"inputs": {
- "nixpkgs": "nixpkgs_177"
+ "nixpkgs": "nixpkgs_176"
},
"locked": {
"lastModified": 1774455309,
@@ -6323,7 +6309,7 @@
},
"logos-nix_176": {
"inputs": {
- "nixpkgs": "nixpkgs_178"
+ "nixpkgs": "nixpkgs_177"
},
"locked": {
"lastModified": 1773955630,
@@ -6341,7 +6327,7 @@
},
"logos-nix_177": {
"inputs": {
- "nixpkgs": "nixpkgs_179"
+ "nixpkgs": "nixpkgs_178"
},
"locked": {
"lastModified": 1773955630,
@@ -6359,7 +6345,7 @@
},
"logos-nix_178": {
"inputs": {
- "nixpkgs": "nixpkgs_180"
+ "nixpkgs": "nixpkgs_179"
},
"locked": {
"lastModified": 1773955630,
@@ -6377,7 +6363,7 @@
},
"logos-nix_179": {
"inputs": {
- "nixpkgs": "nixpkgs_181"
+ "nixpkgs": "nixpkgs_180"
},
"locked": {
"lastModified": 1773955630,
@@ -6395,7 +6381,7 @@
},
"logos-nix_18": {
"inputs": {
- "nixpkgs": "nixpkgs_19"
+ "nixpkgs": "nixpkgs_18"
},
"locked": {
"lastModified": 1773955630,
@@ -6413,7 +6399,7 @@
},
"logos-nix_180": {
"inputs": {
- "nixpkgs": "nixpkgs_182"
+ "nixpkgs": "nixpkgs_181"
},
"locked": {
"lastModified": 1774455309,
@@ -6431,7 +6417,7 @@
},
"logos-nix_181": {
"inputs": {
- "nixpkgs": "nixpkgs_183"
+ "nixpkgs": "nixpkgs_182"
},
"locked": {
"lastModified": 1773955630,
@@ -6449,7 +6435,7 @@
},
"logos-nix_182": {
"inputs": {
- "nixpkgs": "nixpkgs_184"
+ "nixpkgs": "nixpkgs_183"
},
"locked": {
"lastModified": 1773955630,
@@ -6467,7 +6453,7 @@
},
"logos-nix_183": {
"inputs": {
- "nixpkgs": "nixpkgs_185"
+ "nixpkgs": "nixpkgs_184"
},
"locked": {
"lastModified": 1774455309,
@@ -6485,7 +6471,7 @@
},
"logos-nix_184": {
"inputs": {
- "nixpkgs": "nixpkgs_186"
+ "nixpkgs": "nixpkgs_185"
},
"locked": {
"lastModified": 1773955630,
@@ -6503,7 +6489,7 @@
},
"logos-nix_185": {
"inputs": {
- "nixpkgs": "nixpkgs_187"
+ "nixpkgs": "nixpkgs_186"
},
"locked": {
"lastModified": 1774455309,
@@ -6521,7 +6507,7 @@
},
"logos-nix_186": {
"inputs": {
- "nixpkgs": "nixpkgs_188"
+ "nixpkgs": "nixpkgs_187"
},
"locked": {
"lastModified": 1773955630,
@@ -6539,7 +6525,7 @@
},
"logos-nix_187": {
"inputs": {
- "nixpkgs": "nixpkgs_189"
+ "nixpkgs": "nixpkgs_188"
},
"locked": {
"lastModified": 1774455309,
@@ -6557,7 +6543,7 @@
},
"logos-nix_188": {
"inputs": {
- "nixpkgs": "nixpkgs_190"
+ "nixpkgs": "nixpkgs_189"
},
"locked": {
"lastModified": 1773955630,
@@ -6575,7 +6561,7 @@
},
"logos-nix_189": {
"inputs": {
- "nixpkgs": "nixpkgs_191"
+ "nixpkgs": "nixpkgs_190"
},
"locked": {
"lastModified": 1774455309,
@@ -6593,7 +6579,7 @@
},
"logos-nix_19": {
"inputs": {
- "nixpkgs": "nixpkgs_20"
+ "nixpkgs": "nixpkgs_19"
},
"locked": {
"lastModified": 1774455309,
@@ -6611,7 +6597,7 @@
},
"logos-nix_190": {
"inputs": {
- "nixpkgs": "nixpkgs_192"
+ "nixpkgs": "nixpkgs_191"
},
"locked": {
"lastModified": 1773955630,
@@ -6629,7 +6615,7 @@
},
"logos-nix_191": {
"inputs": {
- "nixpkgs": "nixpkgs_193"
+ "nixpkgs": "nixpkgs_192"
},
"locked": {
"lastModified": 1774455309,
@@ -6647,7 +6633,7 @@
},
"logos-nix_192": {
"inputs": {
- "nixpkgs": "nixpkgs_194"
+ "nixpkgs": "nixpkgs_193"
},
"locked": {
"lastModified": 1773955630,
@@ -6665,7 +6651,7 @@
},
"logos-nix_193": {
"inputs": {
- "nixpkgs": "nixpkgs_195"
+ "nixpkgs": "nixpkgs_194"
},
"locked": {
"lastModified": 1773955630,
@@ -6683,7 +6669,7 @@
},
"logos-nix_194": {
"inputs": {
- "nixpkgs": "nixpkgs_196"
+ "nixpkgs": "nixpkgs_195"
},
"locked": {
"lastModified": 1774455309,
@@ -6701,7 +6687,7 @@
},
"logos-nix_195": {
"inputs": {
- "nixpkgs": "nixpkgs_197"
+ "nixpkgs": "nixpkgs_196"
},
"locked": {
"lastModified": 1773955630,
@@ -6719,7 +6705,7 @@
},
"logos-nix_196": {
"inputs": {
- "nixpkgs": "nixpkgs_198"
+ "nixpkgs": "nixpkgs_197"
},
"locked": {
"lastModified": 1773955630,
@@ -6737,7 +6723,7 @@
},
"logos-nix_197": {
"inputs": {
- "nixpkgs": "nixpkgs_199"
+ "nixpkgs": "nixpkgs_198"
},
"locked": {
"lastModified": 1773955630,
@@ -6755,7 +6741,7 @@
},
"logos-nix_198": {
"inputs": {
- "nixpkgs": "nixpkgs_200"
+ "nixpkgs": "nixpkgs_199"
},
"locked": {
"lastModified": 1773955630,
@@ -6773,7 +6759,7 @@
},
"logos-nix_199": {
"inputs": {
- "nixpkgs": "nixpkgs_201"
+ "nixpkgs": "nixpkgs_200"
},
"locked": {
"lastModified": 1774455309,
@@ -6791,7 +6777,7 @@
},
"logos-nix_2": {
"inputs": {
- "nixpkgs": "nixpkgs_3"
+ "nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1773955630,
@@ -6809,7 +6795,7 @@
},
"logos-nix_20": {
"inputs": {
- "nixpkgs": "nixpkgs_21"
+ "nixpkgs": "nixpkgs_20"
},
"locked": {
"lastModified": 1773955630,
@@ -6827,7 +6813,7 @@
},
"logos-nix_200": {
"inputs": {
- "nixpkgs": "nixpkgs_202"
+ "nixpkgs": "nixpkgs_201"
},
"locked": {
"lastModified": 1773955630,
@@ -6845,7 +6831,7 @@
},
"logos-nix_201": {
"inputs": {
- "nixpkgs": "nixpkgs_203"
+ "nixpkgs": "nixpkgs_202"
},
"locked": {
"lastModified": 1774455309,
@@ -6863,7 +6849,7 @@
},
"logos-nix_202": {
"inputs": {
- "nixpkgs": "nixpkgs_204"
+ "nixpkgs": "nixpkgs_203"
},
"locked": {
"lastModified": 1774455309,
@@ -6881,7 +6867,7 @@
},
"logos-nix_203": {
"inputs": {
- "nixpkgs": "nixpkgs_205"
+ "nixpkgs": "nixpkgs_204"
},
"locked": {
"lastModified": 1773955630,
@@ -6899,7 +6885,7 @@
},
"logos-nix_204": {
"inputs": {
- "nixpkgs": "nixpkgs_206"
+ "nixpkgs": "nixpkgs_205"
},
"locked": {
"lastModified": 1773955630,
@@ -6917,7 +6903,7 @@
},
"logos-nix_205": {
"inputs": {
- "nixpkgs": "nixpkgs_207"
+ "nixpkgs": "nixpkgs_206"
},
"locked": {
"lastModified": 1773955630,
@@ -6935,7 +6921,7 @@
},
"logos-nix_206": {
"inputs": {
- "nixpkgs": "nixpkgs_208"
+ "nixpkgs": "nixpkgs_207"
},
"locked": {
"lastModified": 1773955630,
@@ -6953,7 +6939,7 @@
},
"logos-nix_207": {
"inputs": {
- "nixpkgs": "nixpkgs_209"
+ "nixpkgs": "nixpkgs_208"
},
"locked": {
"lastModified": 1773955630,
@@ -6971,7 +6957,7 @@
},
"logos-nix_208": {
"inputs": {
- "nixpkgs": "nixpkgs_210"
+ "nixpkgs": "nixpkgs_209"
},
"locked": {
"lastModified": 1774455309,
@@ -6989,7 +6975,7 @@
},
"logos-nix_209": {
"inputs": {
- "nixpkgs": "nixpkgs_211"
+ "nixpkgs": "nixpkgs_210"
},
"locked": {
"lastModified": 1773955630,
@@ -7007,7 +6993,7 @@
},
"logos-nix_21": {
"inputs": {
- "nixpkgs": "nixpkgs_22"
+ "nixpkgs": "nixpkgs_21"
},
"locked": {
"lastModified": 1773955630,
@@ -7025,7 +7011,7 @@
},
"logos-nix_210": {
"inputs": {
- "nixpkgs": "nixpkgs_212"
+ "nixpkgs": "nixpkgs_211"
},
"locked": {
"lastModified": 1774455309,
@@ -7043,7 +7029,7 @@
},
"logos-nix_211": {
"inputs": {
- "nixpkgs": "nixpkgs_213"
+ "nixpkgs": "nixpkgs_212"
},
"locked": {
"lastModified": 1774455309,
@@ -7061,7 +7047,7 @@
},
"logos-nix_212": {
"inputs": {
- "nixpkgs": "nixpkgs_214"
+ "nixpkgs": "nixpkgs_213"
},
"locked": {
"lastModified": 1773955630,
@@ -7079,7 +7065,7 @@
},
"logos-nix_213": {
"inputs": {
- "nixpkgs": "nixpkgs_215"
+ "nixpkgs": "nixpkgs_214"
},
"locked": {
"lastModified": 1773955630,
@@ -7097,7 +7083,7 @@
},
"logos-nix_214": {
"inputs": {
- "nixpkgs": "nixpkgs_216"
+ "nixpkgs": "nixpkgs_215"
},
"locked": {
"lastModified": 1774455309,
@@ -7115,7 +7101,7 @@
},
"logos-nix_215": {
"inputs": {
- "nixpkgs": "nixpkgs_217"
+ "nixpkgs": "nixpkgs_216"
},
"locked": {
"lastModified": 1774455309,
@@ -7133,7 +7119,7 @@
},
"logos-nix_216": {
"inputs": {
- "nixpkgs": "nixpkgs_218"
+ "nixpkgs": "nixpkgs_217"
},
"locked": {
"lastModified": 1773955630,
@@ -7151,7 +7137,7 @@
},
"logos-nix_217": {
"inputs": {
- "nixpkgs": "nixpkgs_219"
+ "nixpkgs": "nixpkgs_218"
},
"locked": {
"lastModified": 1773955630,
@@ -7169,7 +7155,7 @@
},
"logos-nix_218": {
"inputs": {
- "nixpkgs": "nixpkgs_220"
+ "nixpkgs": "nixpkgs_219"
},
"locked": {
"lastModified": 1774455309,
@@ -7187,7 +7173,7 @@
},
"logos-nix_219": {
"inputs": {
- "nixpkgs": "nixpkgs_221"
+ "nixpkgs": "nixpkgs_220"
},
"locked": {
"lastModified": 1774455309,
@@ -7205,7 +7191,7 @@
},
"logos-nix_22": {
"inputs": {
- "nixpkgs": "nixpkgs_23"
+ "nixpkgs": "nixpkgs_22"
},
"locked": {
"lastModified": 1773955630,
@@ -7223,7 +7209,7 @@
},
"logos-nix_220": {
"inputs": {
- "nixpkgs": "nixpkgs_222"
+ "nixpkgs": "nixpkgs_221"
},
"locked": {
"lastModified": 1773955630,
@@ -7241,7 +7227,7 @@
},
"logos-nix_221": {
"inputs": {
- "nixpkgs": "nixpkgs_223"
+ "nixpkgs": "nixpkgs_222"
},
"locked": {
"lastModified": 1773955630,
@@ -7259,7 +7245,7 @@
},
"logos-nix_222": {
"inputs": {
- "nixpkgs": "nixpkgs_224"
+ "nixpkgs": "nixpkgs_223"
},
"locked": {
"lastModified": 1773955630,
@@ -7277,7 +7263,7 @@
},
"logos-nix_223": {
"inputs": {
- "nixpkgs": "nixpkgs_225"
+ "nixpkgs": "nixpkgs_224"
},
"locked": {
"lastModified": 1773955630,
@@ -7295,7 +7281,7 @@
},
"logos-nix_224": {
"inputs": {
- "nixpkgs": "nixpkgs_226"
+ "nixpkgs": "nixpkgs_225"
},
"locked": {
"lastModified": 1774455309,
@@ -7313,7 +7299,7 @@
},
"logos-nix_225": {
"inputs": {
- "nixpkgs": "nixpkgs_227"
+ "nixpkgs": "nixpkgs_226"
},
"locked": {
"lastModified": 1773955630,
@@ -7331,7 +7317,7 @@
},
"logos-nix_226": {
"inputs": {
- "nixpkgs": "nixpkgs_228"
+ "nixpkgs": "nixpkgs_227"
},
"locked": {
"lastModified": 1773955630,
@@ -7349,7 +7335,7 @@
},
"logos-nix_227": {
"inputs": {
- "nixpkgs": "nixpkgs_229"
+ "nixpkgs": "nixpkgs_228"
},
"locked": {
"lastModified": 1774455309,
@@ -7367,7 +7353,7 @@
},
"logos-nix_228": {
"inputs": {
- "nixpkgs": "nixpkgs_230"
+ "nixpkgs": "nixpkgs_229"
},
"locked": {
"lastModified": 1773955630,
@@ -7385,7 +7371,7 @@
},
"logos-nix_229": {
"inputs": {
- "nixpkgs": "nixpkgs_231"
+ "nixpkgs": "nixpkgs_230"
},
"locked": {
"lastModified": 1774455309,
@@ -7403,7 +7389,7 @@
},
"logos-nix_23": {
"inputs": {
- "nixpkgs": "nixpkgs_24"
+ "nixpkgs": "nixpkgs_23"
},
"locked": {
"lastModified": 1773955630,
@@ -7421,7 +7407,7 @@
},
"logos-nix_230": {
"inputs": {
- "nixpkgs": "nixpkgs_232"
+ "nixpkgs": "nixpkgs_231"
},
"locked": {
"lastModified": 1774455309,
@@ -7439,7 +7425,7 @@
},
"logos-nix_231": {
"inputs": {
- "nixpkgs": "nixpkgs_233"
+ "nixpkgs": "nixpkgs_232"
},
"locked": {
"lastModified": 1773955630,
@@ -7457,7 +7443,7 @@
},
"logos-nix_232": {
"inputs": {
- "nixpkgs": "nixpkgs_234"
+ "nixpkgs": "nixpkgs_233"
},
"locked": {
"lastModified": 1773955630,
@@ -7475,7 +7461,7 @@
},
"logos-nix_233": {
"inputs": {
- "nixpkgs": "nixpkgs_235"
+ "nixpkgs": "nixpkgs_234"
},
"locked": {
"lastModified": 1773955630,
@@ -7493,7 +7479,7 @@
},
"logos-nix_234": {
"inputs": {
- "nixpkgs": "nixpkgs_236"
+ "nixpkgs": "nixpkgs_235"
},
"locked": {
"lastModified": 1773955630,
@@ -7511,7 +7497,7 @@
},
"logos-nix_235": {
"inputs": {
- "nixpkgs": "nixpkgs_237"
+ "nixpkgs": "nixpkgs_236"
},
"locked": {
"lastModified": 1773955630,
@@ -7529,7 +7515,7 @@
},
"logos-nix_236": {
"inputs": {
- "nixpkgs": "nixpkgs_238"
+ "nixpkgs": "nixpkgs_237"
},
"locked": {
"lastModified": 1774455309,
@@ -7547,7 +7533,7 @@
},
"logos-nix_237": {
"inputs": {
- "nixpkgs": "nixpkgs_239"
+ "nixpkgs": "nixpkgs_238"
},
"locked": {
"lastModified": 1773955630,
@@ -7565,7 +7551,7 @@
},
"logos-nix_238": {
"inputs": {
- "nixpkgs": "nixpkgs_240"
+ "nixpkgs": "nixpkgs_239"
},
"locked": {
"lastModified": 1774455309,
@@ -7583,7 +7569,7 @@
},
"logos-nix_239": {
"inputs": {
- "nixpkgs": "nixpkgs_241"
+ "nixpkgs": "nixpkgs_240"
},
"locked": {
"lastModified": 1774455309,
@@ -7601,7 +7587,7 @@
},
"logos-nix_24": {
"inputs": {
- "nixpkgs": "nixpkgs_25"
+ "nixpkgs": "nixpkgs_24"
},
"locked": {
"lastModified": 1774455309,
@@ -7619,7 +7605,7 @@
},
"logos-nix_240": {
"inputs": {
- "nixpkgs": "nixpkgs_242"
+ "nixpkgs": "nixpkgs_241"
},
"locked": {
"lastModified": 1773955630,
@@ -7637,7 +7623,7 @@
},
"logos-nix_241": {
"inputs": {
- "nixpkgs": "nixpkgs_243"
+ "nixpkgs": "nixpkgs_242"
},
"locked": {
"lastModified": 1773955630,
@@ -7655,7 +7641,7 @@
},
"logos-nix_242": {
"inputs": {
- "nixpkgs": "nixpkgs_244"
+ "nixpkgs": "nixpkgs_243"
},
"locked": {
"lastModified": 1774455309,
@@ -7673,7 +7659,7 @@
},
"logos-nix_243": {
"inputs": {
- "nixpkgs": "nixpkgs_245"
+ "nixpkgs": "nixpkgs_244"
},
"locked": {
"lastModified": 1774455309,
@@ -7691,7 +7677,7 @@
},
"logos-nix_244": {
"inputs": {
- "nixpkgs": "nixpkgs_246"
+ "nixpkgs": "nixpkgs_245"
},
"locked": {
"lastModified": 1773955630,
@@ -7709,7 +7695,7 @@
},
"logos-nix_245": {
"inputs": {
- "nixpkgs": "nixpkgs_247"
+ "nixpkgs": "nixpkgs_246"
},
"locked": {
"lastModified": 1773955630,
@@ -7727,7 +7713,7 @@
},
"logos-nix_246": {
"inputs": {
- "nixpkgs": "nixpkgs_248"
+ "nixpkgs": "nixpkgs_247"
},
"locked": {
"lastModified": 1774455309,
@@ -7745,7 +7731,7 @@
},
"logos-nix_247": {
"inputs": {
- "nixpkgs": "nixpkgs_249"
+ "nixpkgs": "nixpkgs_248"
},
"locked": {
"lastModified": 1774455309,
@@ -7763,7 +7749,7 @@
},
"logos-nix_248": {
"inputs": {
- "nixpkgs": "nixpkgs_250"
+ "nixpkgs": "nixpkgs_249"
},
"locked": {
"lastModified": 1773955630,
@@ -7781,7 +7767,7 @@
},
"logos-nix_249": {
"inputs": {
- "nixpkgs": "nixpkgs_251"
+ "nixpkgs": "nixpkgs_250"
},
"locked": {
"lastModified": 1773955630,
@@ -7799,7 +7785,7 @@
},
"logos-nix_25": {
"inputs": {
- "nixpkgs": "nixpkgs_26"
+ "nixpkgs": "nixpkgs_25"
},
"locked": {
"lastModified": 1773955630,
@@ -7817,7 +7803,7 @@
},
"logos-nix_250": {
"inputs": {
- "nixpkgs": "nixpkgs_252"
+ "nixpkgs": "nixpkgs_251"
},
"locked": {
"lastModified": 1773955630,
@@ -7835,7 +7821,7 @@
},
"logos-nix_251": {
"inputs": {
- "nixpkgs": "nixpkgs_253"
+ "nixpkgs": "nixpkgs_252"
},
"locked": {
"lastModified": 1773955630,
@@ -7853,7 +7839,7 @@
},
"logos-nix_252": {
"inputs": {
- "nixpkgs": "nixpkgs_254"
+ "nixpkgs": "nixpkgs_253"
},
"locked": {
"lastModified": 1774455309,
@@ -7871,7 +7857,7 @@
},
"logos-nix_253": {
"inputs": {
- "nixpkgs": "nixpkgs_255"
+ "nixpkgs": "nixpkgs_254"
},
"locked": {
"lastModified": 1773955630,
@@ -7889,7 +7875,7 @@
},
"logos-nix_254": {
"inputs": {
- "nixpkgs": "nixpkgs_256"
+ "nixpkgs": "nixpkgs_255"
},
"locked": {
"lastModified": 1773955630,
@@ -7907,7 +7893,7 @@
},
"logos-nix_255": {
"inputs": {
- "nixpkgs": "nixpkgs_257"
+ "nixpkgs": "nixpkgs_256"
},
"locked": {
"lastModified": 1774455309,
@@ -7925,7 +7911,7 @@
},
"logos-nix_256": {
"inputs": {
- "nixpkgs": "nixpkgs_258"
+ "nixpkgs": "nixpkgs_257"
},
"locked": {
"lastModified": 1774455309,
@@ -7943,7 +7929,7 @@
},
"logos-nix_257": {
"inputs": {
- "nixpkgs": "nixpkgs_259"
+ "nixpkgs": "nixpkgs_258"
},
"locked": {
"lastModified": 1773955630,
@@ -7961,7 +7947,7 @@
},
"logos-nix_258": {
"inputs": {
- "nixpkgs": "nixpkgs_260"
+ "nixpkgs": "nixpkgs_259"
},
"locked": {
"lastModified": 1774455309,
@@ -7979,7 +7965,7 @@
},
"logos-nix_259": {
"inputs": {
- "nixpkgs": "nixpkgs_261"
+ "nixpkgs": "nixpkgs_260"
},
"locked": {
"lastModified": 1774455309,
@@ -7997,7 +7983,7 @@
},
"logos-nix_26": {
"inputs": {
- "nixpkgs": "nixpkgs_27"
+ "nixpkgs": "nixpkgs_26"
},
"locked": {
"lastModified": 1774455309,
@@ -8015,7 +8001,7 @@
},
"logos-nix_260": {
"inputs": {
- "nixpkgs": "nixpkgs_262"
+ "nixpkgs": "nixpkgs_261"
},
"locked": {
"lastModified": 1774455309,
@@ -8033,7 +8019,7 @@
},
"logos-nix_261": {
"inputs": {
- "nixpkgs": "nixpkgs_263"
+ "nixpkgs": "nixpkgs_262"
},
"locked": {
"lastModified": 1774455309,
@@ -8051,7 +8037,7 @@
},
"logos-nix_262": {
"inputs": {
- "nixpkgs": "nixpkgs_264"
+ "nixpkgs": "nixpkgs_263"
},
"locked": {
"lastModified": 1773955630,
@@ -8069,7 +8055,7 @@
},
"logos-nix_263": {
"inputs": {
- "nixpkgs": "nixpkgs_265"
+ "nixpkgs": "nixpkgs_264"
},
"locked": {
"lastModified": 1773955630,
@@ -8087,7 +8073,7 @@
},
"logos-nix_264": {
"inputs": {
- "nixpkgs": "nixpkgs_266"
+ "nixpkgs": "nixpkgs_265"
},
"locked": {
"lastModified": 1773955630,
@@ -8105,7 +8091,7 @@
},
"logos-nix_265": {
"inputs": {
- "nixpkgs": "nixpkgs_267"
+ "nixpkgs": "nixpkgs_266"
},
"locked": {
"lastModified": 1773955630,
@@ -8123,7 +8109,7 @@
},
"logos-nix_266": {
"inputs": {
- "nixpkgs": "nixpkgs_268"
+ "nixpkgs": "nixpkgs_267"
},
"locked": {
"lastModified": 1774455309,
@@ -8141,7 +8127,7 @@
},
"logos-nix_267": {
"inputs": {
- "nixpkgs": "nixpkgs_269"
+ "nixpkgs": "nixpkgs_268"
},
"locked": {
"lastModified": 1774455309,
@@ -8159,7 +8145,7 @@
},
"logos-nix_268": {
"inputs": {
- "nixpkgs": "nixpkgs_270"
+ "nixpkgs": "nixpkgs_269"
},
"locked": {
"lastModified": 1773955630,
@@ -8177,7 +8163,7 @@
},
"logos-nix_269": {
"inputs": {
- "nixpkgs": "nixpkgs_272"
+ "nixpkgs": "nixpkgs_271"
},
"locked": {
"lastModified": 1774455309,
@@ -8195,7 +8181,7 @@
},
"logos-nix_27": {
"inputs": {
- "nixpkgs": "nixpkgs_28"
+ "nixpkgs": "nixpkgs_27"
},
"locked": {
"lastModified": 1774455309,
@@ -8213,7 +8199,7 @@
},
"logos-nix_270": {
"inputs": {
- "nixpkgs": "nixpkgs_273"
+ "nixpkgs": "nixpkgs_272"
},
"locked": {
"lastModified": 1773955630,
@@ -8231,7 +8217,7 @@
},
"logos-nix_271": {
"inputs": {
- "nixpkgs": "nixpkgs_274"
+ "nixpkgs": "nixpkgs_273"
},
"locked": {
"lastModified": 1774455309,
@@ -8249,7 +8235,7 @@
},
"logos-nix_272": {
"inputs": {
- "nixpkgs": "nixpkgs_275"
+ "nixpkgs": "nixpkgs_274"
},
"locked": {
"lastModified": 1773955630,
@@ -8267,7 +8253,7 @@
},
"logos-nix_273": {
"inputs": {
- "nixpkgs": "nixpkgs_276"
+ "nixpkgs": "nixpkgs_275"
},
"locked": {
"lastModified": 1774455309,
@@ -8285,7 +8271,7 @@
},
"logos-nix_274": {
"inputs": {
- "nixpkgs": "nixpkgs_277"
+ "nixpkgs": "nixpkgs_276"
},
"locked": {
"lastModified": 1773955630,
@@ -8303,7 +8289,7 @@
},
"logos-nix_275": {
"inputs": {
- "nixpkgs": "nixpkgs_278"
+ "nixpkgs": "nixpkgs_277"
},
"locked": {
"lastModified": 1774455309,
@@ -8321,7 +8307,7 @@
},
"logos-nix_276": {
"inputs": {
- "nixpkgs": "nixpkgs_279"
+ "nixpkgs": "nixpkgs_278"
},
"locked": {
"lastModified": 1774455309,
@@ -8339,7 +8325,7 @@
},
"logos-nix_277": {
"inputs": {
- "nixpkgs": "nixpkgs_280"
+ "nixpkgs": "nixpkgs_279"
},
"locked": {
"lastModified": 1774455309,
@@ -8357,7 +8343,7 @@
},
"logos-nix_278": {
"inputs": {
- "nixpkgs": "nixpkgs_281"
+ "nixpkgs": "nixpkgs_280"
},
"locked": {
"lastModified": 1774455309,
@@ -8375,7 +8361,7 @@
},
"logos-nix_279": {
"inputs": {
- "nixpkgs": "nixpkgs_282"
+ "nixpkgs": "nixpkgs_281"
},
"locked": {
"lastModified": 1773955630,
@@ -8393,7 +8379,7 @@
},
"logos-nix_28": {
"inputs": {
- "nixpkgs": "nixpkgs_29"
+ "nixpkgs": "nixpkgs_28"
},
"locked": {
"lastModified": 1773955630,
@@ -8411,7 +8397,7 @@
},
"logos-nix_280": {
"inputs": {
- "nixpkgs": "nixpkgs_283"
+ "nixpkgs": "nixpkgs_282"
},
"locked": {
"lastModified": 1774455309,
@@ -8429,7 +8415,7 @@
},
"logos-nix_281": {
"inputs": {
- "nixpkgs": "nixpkgs_284"
+ "nixpkgs": "nixpkgs_283"
},
"locked": {
"lastModified": 1773955630,
@@ -8447,7 +8433,7 @@
},
"logos-nix_282": {
"inputs": {
- "nixpkgs": "nixpkgs_285"
+ "nixpkgs": "nixpkgs_284"
},
"locked": {
"lastModified": 1774455309,
@@ -8465,7 +8451,7 @@
},
"logos-nix_283": {
"inputs": {
- "nixpkgs": "nixpkgs_286"
+ "nixpkgs": "nixpkgs_285"
},
"locked": {
"lastModified": 1773955630,
@@ -8483,7 +8469,7 @@
},
"logos-nix_284": {
"inputs": {
- "nixpkgs": "nixpkgs_287"
+ "nixpkgs": "nixpkgs_286"
},
"locked": {
"lastModified": 1774455309,
@@ -8501,7 +8487,7 @@
},
"logos-nix_285": {
"inputs": {
- "nixpkgs": "nixpkgs_288"
+ "nixpkgs": "nixpkgs_287"
},
"locked": {
"lastModified": 1773955630,
@@ -8519,7 +8505,7 @@
},
"logos-nix_286": {
"inputs": {
- "nixpkgs": "nixpkgs_289"
+ "nixpkgs": "nixpkgs_288"
},
"locked": {
"lastModified": 1773955630,
@@ -8537,7 +8523,7 @@
},
"logos-nix_287": {
"inputs": {
- "nixpkgs": "nixpkgs_290"
+ "nixpkgs": "nixpkgs_289"
},
"locked": {
"lastModified": 1774455309,
@@ -8555,7 +8541,7 @@
},
"logos-nix_288": {
"inputs": {
- "nixpkgs": "nixpkgs_291"
+ "nixpkgs": "nixpkgs_290"
},
"locked": {
"lastModified": 1773955630,
@@ -8573,7 +8559,7 @@
},
"logos-nix_289": {
"inputs": {
- "nixpkgs": "nixpkgs_292"
+ "nixpkgs": "nixpkgs_291"
},
"locked": {
"lastModified": 1773955630,
@@ -8591,7 +8577,7 @@
},
"logos-nix_29": {
"inputs": {
- "nixpkgs": "nixpkgs_30"
+ "nixpkgs": "nixpkgs_29"
},
"locked": {
"lastModified": 1773955630,
@@ -8609,7 +8595,7 @@
},
"logos-nix_290": {
"inputs": {
- "nixpkgs": "nixpkgs_293"
+ "nixpkgs": "nixpkgs_292"
},
"locked": {
"lastModified": 1773955630,
@@ -8627,7 +8613,7 @@
},
"logos-nix_291": {
"inputs": {
- "nixpkgs": "nixpkgs_294"
+ "nixpkgs": "nixpkgs_293"
},
"locked": {
"lastModified": 1773955630,
@@ -8645,7 +8631,7 @@
},
"logos-nix_292": {
"inputs": {
- "nixpkgs": "nixpkgs_295"
+ "nixpkgs": "nixpkgs_294"
},
"locked": {
"lastModified": 1774455309,
@@ -8663,7 +8649,7 @@
},
"logos-nix_293": {
"inputs": {
- "nixpkgs": "nixpkgs_296"
+ "nixpkgs": "nixpkgs_295"
},
"locked": {
"lastModified": 1773955630,
@@ -8681,7 +8667,7 @@
},
"logos-nix_294": {
"inputs": {
- "nixpkgs": "nixpkgs_297"
+ "nixpkgs": "nixpkgs_296"
},
"locked": {
"lastModified": 1774455309,
@@ -8699,7 +8685,7 @@
},
"logos-nix_295": {
"inputs": {
- "nixpkgs": "nixpkgs_298"
+ "nixpkgs": "nixpkgs_297"
},
"locked": {
"lastModified": 1774455309,
@@ -8717,7 +8703,7 @@
},
"logos-nix_296": {
"inputs": {
- "nixpkgs": "nixpkgs_299"
+ "nixpkgs": "nixpkgs_298"
},
"locked": {
"lastModified": 1773955630,
@@ -8735,7 +8721,7 @@
},
"logos-nix_297": {
"inputs": {
- "nixpkgs": "nixpkgs_300"
+ "nixpkgs": "nixpkgs_299"
},
"locked": {
"lastModified": 1773955630,
@@ -8753,7 +8739,7 @@
},
"logos-nix_298": {
"inputs": {
- "nixpkgs": "nixpkgs_301"
+ "nixpkgs": "nixpkgs_300"
},
"locked": {
"lastModified": 1773955630,
@@ -8771,7 +8757,7 @@
},
"logos-nix_299": {
"inputs": {
- "nixpkgs": "nixpkgs_302"
+ "nixpkgs": "nixpkgs_301"
},
"locked": {
"lastModified": 1773955630,
@@ -8789,7 +8775,7 @@
},
"logos-nix_3": {
"inputs": {
- "nixpkgs": "nixpkgs_4"
+ "nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1774455309,
@@ -8807,7 +8793,7 @@
},
"logos-nix_30": {
"inputs": {
- "nixpkgs": "nixpkgs_31"
+ "nixpkgs": "nixpkgs_30"
},
"locked": {
"lastModified": 1773955630,
@@ -8825,7 +8811,7 @@
},
"logos-nix_300": {
"inputs": {
- "nixpkgs": "nixpkgs_303"
+ "nixpkgs": "nixpkgs_302"
},
"locked": {
"lastModified": 1773955630,
@@ -8843,7 +8829,7 @@
},
"logos-nix_301": {
"inputs": {
- "nixpkgs": "nixpkgs_304"
+ "nixpkgs": "nixpkgs_303"
},
"locked": {
"lastModified": 1774455309,
@@ -8861,7 +8847,7 @@
},
"logos-nix_302": {
"inputs": {
- "nixpkgs": "nixpkgs_305"
+ "nixpkgs": "nixpkgs_304"
},
"locked": {
"lastModified": 1773955630,
@@ -8879,7 +8865,7 @@
},
"logos-nix_303": {
"inputs": {
- "nixpkgs": "nixpkgs_306"
+ "nixpkgs": "nixpkgs_305"
},
"locked": {
"lastModified": 1774455309,
@@ -8897,7 +8883,7 @@
},
"logos-nix_304": {
"inputs": {
- "nixpkgs": "nixpkgs_307"
+ "nixpkgs": "nixpkgs_306"
},
"locked": {
"lastModified": 1774455309,
@@ -8915,7 +8901,7 @@
},
"logos-nix_305": {
"inputs": {
- "nixpkgs": "nixpkgs_308"
+ "nixpkgs": "nixpkgs_307"
},
"locked": {
"lastModified": 1773955630,
@@ -8933,7 +8919,7 @@
},
"logos-nix_306": {
"inputs": {
- "nixpkgs": "nixpkgs_309"
+ "nixpkgs": "nixpkgs_308"
},
"locked": {
"lastModified": 1773955630,
@@ -8951,7 +8937,7 @@
},
"logos-nix_307": {
"inputs": {
- "nixpkgs": "nixpkgs_310"
+ "nixpkgs": "nixpkgs_309"
},
"locked": {
"lastModified": 1774455309,
@@ -8969,7 +8955,7 @@
},
"logos-nix_308": {
"inputs": {
- "nixpkgs": "nixpkgs_311"
+ "nixpkgs": "nixpkgs_310"
},
"locked": {
"lastModified": 1774455309,
@@ -8987,7 +8973,7 @@
},
"logos-nix_309": {
"inputs": {
- "nixpkgs": "nixpkgs_312"
+ "nixpkgs": "nixpkgs_311"
},
"locked": {
"lastModified": 1773955630,
@@ -9005,7 +8991,7 @@
},
"logos-nix_31": {
"inputs": {
- "nixpkgs": "nixpkgs_32"
+ "nixpkgs": "nixpkgs_31"
},
"locked": {
"lastModified": 1773955630,
@@ -9023,7 +9009,7 @@
},
"logos-nix_310": {
"inputs": {
- "nixpkgs": "nixpkgs_313"
+ "nixpkgs": "nixpkgs_312"
},
"locked": {
"lastModified": 1773955630,
@@ -9041,7 +9027,7 @@
},
"logos-nix_311": {
"inputs": {
- "nixpkgs": "nixpkgs_314"
+ "nixpkgs": "nixpkgs_313"
},
"locked": {
"lastModified": 1774455309,
@@ -9059,7 +9045,7 @@
},
"logos-nix_312": {
"inputs": {
- "nixpkgs": "nixpkgs_315"
+ "nixpkgs": "nixpkgs_314"
},
"locked": {
"lastModified": 1774455309,
@@ -9077,7 +9063,7 @@
},
"logos-nix_313": {
"inputs": {
- "nixpkgs": "nixpkgs_316"
+ "nixpkgs": "nixpkgs_315"
},
"locked": {
"lastModified": 1773955630,
@@ -9095,7 +9081,7 @@
},
"logos-nix_314": {
"inputs": {
- "nixpkgs": "nixpkgs_317"
+ "nixpkgs": "nixpkgs_316"
},
"locked": {
"lastModified": 1773955630,
@@ -9113,7 +9099,7 @@
},
"logos-nix_315": {
"inputs": {
- "nixpkgs": "nixpkgs_318"
+ "nixpkgs": "nixpkgs_317"
},
"locked": {
"lastModified": 1773955630,
@@ -9131,7 +9117,7 @@
},
"logos-nix_316": {
"inputs": {
- "nixpkgs": "nixpkgs_319"
+ "nixpkgs": "nixpkgs_318"
},
"locked": {
"lastModified": 1773955630,
@@ -9149,7 +9135,7 @@
},
"logos-nix_317": {
"inputs": {
- "nixpkgs": "nixpkgs_320"
+ "nixpkgs": "nixpkgs_319"
},
"locked": {
"lastModified": 1774455309,
@@ -9167,7 +9153,7 @@
},
"logos-nix_318": {
"inputs": {
- "nixpkgs": "nixpkgs_321"
+ "nixpkgs": "nixpkgs_320"
},
"locked": {
"lastModified": 1773955630,
@@ -9185,7 +9171,7 @@
},
"logos-nix_319": {
"inputs": {
- "nixpkgs": "nixpkgs_322"
+ "nixpkgs": "nixpkgs_321"
},
"locked": {
"lastModified": 1773955630,
@@ -9203,7 +9189,7 @@
},
"logos-nix_32": {
"inputs": {
- "nixpkgs": "nixpkgs_33"
+ "nixpkgs": "nixpkgs_32"
},
"locked": {
"lastModified": 1773955630,
@@ -9221,7 +9207,7 @@
},
"logos-nix_320": {
"inputs": {
- "nixpkgs": "nixpkgs_323"
+ "nixpkgs": "nixpkgs_322"
},
"locked": {
"lastModified": 1774455309,
@@ -9239,7 +9225,7 @@
},
"logos-nix_321": {
"inputs": {
- "nixpkgs": "nixpkgs_324"
+ "nixpkgs": "nixpkgs_323"
},
"locked": {
"lastModified": 1773955630,
@@ -9257,7 +9243,7 @@
},
"logos-nix_322": {
"inputs": {
- "nixpkgs": "nixpkgs_325"
+ "nixpkgs": "nixpkgs_324"
},
"locked": {
"lastModified": 1774455309,
@@ -9275,7 +9261,7 @@
},
"logos-nix_323": {
"inputs": {
- "nixpkgs": "nixpkgs_326"
+ "nixpkgs": "nixpkgs_325"
},
"locked": {
"lastModified": 1773955630,
@@ -9293,7 +9279,7 @@
},
"logos-nix_324": {
"inputs": {
- "nixpkgs": "nixpkgs_327"
+ "nixpkgs": "nixpkgs_326"
},
"locked": {
"lastModified": 1774455309,
@@ -9311,7 +9297,7 @@
},
"logos-nix_325": {
"inputs": {
- "nixpkgs": "nixpkgs_328"
+ "nixpkgs": "nixpkgs_327"
},
"locked": {
"lastModified": 1773955630,
@@ -9329,7 +9315,7 @@
},
"logos-nix_326": {
"inputs": {
- "nixpkgs": "nixpkgs_329"
+ "nixpkgs": "nixpkgs_328"
},
"locked": {
"lastModified": 1774455309,
@@ -9347,7 +9333,7 @@
},
"logos-nix_327": {
"inputs": {
- "nixpkgs": "nixpkgs_330"
+ "nixpkgs": "nixpkgs_329"
},
"locked": {
"lastModified": 1773955630,
@@ -9365,7 +9351,7 @@
},
"logos-nix_328": {
"inputs": {
- "nixpkgs": "nixpkgs_331"
+ "nixpkgs": "nixpkgs_330"
},
"locked": {
"lastModified": 1774455309,
@@ -9383,7 +9369,7 @@
},
"logos-nix_329": {
"inputs": {
- "nixpkgs": "nixpkgs_332"
+ "nixpkgs": "nixpkgs_331"
},
"locked": {
"lastModified": 1773955630,
@@ -9401,7 +9387,7 @@
},
"logos-nix_33": {
"inputs": {
- "nixpkgs": "nixpkgs_34"
+ "nixpkgs": "nixpkgs_33"
},
"locked": {
"lastModified": 1774455309,
@@ -9419,7 +9405,7 @@
},
"logos-nix_330": {
"inputs": {
- "nixpkgs": "nixpkgs_333"
+ "nixpkgs": "nixpkgs_332"
},
"locked": {
"lastModified": 1773955630,
@@ -9437,7 +9423,7 @@
},
"logos-nix_331": {
"inputs": {
- "nixpkgs": "nixpkgs_334"
+ "nixpkgs": "nixpkgs_333"
},
"locked": {
"lastModified": 1774455309,
@@ -9455,7 +9441,7 @@
},
"logos-nix_332": {
"inputs": {
- "nixpkgs": "nixpkgs_335"
+ "nixpkgs": "nixpkgs_334"
},
"locked": {
"lastModified": 1773955630,
@@ -9473,7 +9459,7 @@
},
"logos-nix_333": {
"inputs": {
- "nixpkgs": "nixpkgs_336"
+ "nixpkgs": "nixpkgs_335"
},
"locked": {
"lastModified": 1773955630,
@@ -9491,7 +9477,7 @@
},
"logos-nix_334": {
"inputs": {
- "nixpkgs": "nixpkgs_337"
+ "nixpkgs": "nixpkgs_336"
},
"locked": {
"lastModified": 1773955630,
@@ -9509,7 +9495,7 @@
},
"logos-nix_335": {
"inputs": {
- "nixpkgs": "nixpkgs_338"
+ "nixpkgs": "nixpkgs_337"
},
"locked": {
"lastModified": 1773955630,
@@ -9527,7 +9513,7 @@
},
"logos-nix_336": {
"inputs": {
- "nixpkgs": "nixpkgs_339"
+ "nixpkgs": "nixpkgs_338"
},
"locked": {
"lastModified": 1774455309,
@@ -9545,7 +9531,7 @@
},
"logos-nix_337": {
"inputs": {
- "nixpkgs": "nixpkgs_340"
+ "nixpkgs": "nixpkgs_339"
},
"locked": {
"lastModified": 1773955630,
@@ -9563,7 +9549,7 @@
},
"logos-nix_338": {
"inputs": {
- "nixpkgs": "nixpkgs_341"
+ "nixpkgs": "nixpkgs_340"
},
"locked": {
"lastModified": 1774455309,
@@ -9581,7 +9567,7 @@
},
"logos-nix_339": {
"inputs": {
- "nixpkgs": "nixpkgs_342"
+ "nixpkgs": "nixpkgs_341"
},
"locked": {
"lastModified": 1774455309,
@@ -9599,7 +9585,7 @@
},
"logos-nix_34": {
"inputs": {
- "nixpkgs": "nixpkgs_35"
+ "nixpkgs": "nixpkgs_34"
},
"locked": {
"lastModified": 1773955630,
@@ -9617,7 +9603,7 @@
},
"logos-nix_340": {
"inputs": {
- "nixpkgs": "nixpkgs_343"
+ "nixpkgs": "nixpkgs_342"
},
"locked": {
"lastModified": 1773955630,
@@ -9635,7 +9621,7 @@
},
"logos-nix_341": {
"inputs": {
- "nixpkgs": "nixpkgs_344"
+ "nixpkgs": "nixpkgs_343"
},
"locked": {
"lastModified": 1773955630,
@@ -9653,7 +9639,7 @@
},
"logos-nix_342": {
"inputs": {
- "nixpkgs": "nixpkgs_345"
+ "nixpkgs": "nixpkgs_344"
},
"locked": {
"lastModified": 1773955630,
@@ -9671,7 +9657,7 @@
},
"logos-nix_343": {
"inputs": {
- "nixpkgs": "nixpkgs_346"
+ "nixpkgs": "nixpkgs_345"
},
"locked": {
"lastModified": 1773955630,
@@ -9689,7 +9675,7 @@
},
"logos-nix_344": {
"inputs": {
- "nixpkgs": "nixpkgs_347"
+ "nixpkgs": "nixpkgs_346"
},
"locked": {
"lastModified": 1773955630,
@@ -9707,7 +9693,7 @@
},
"logos-nix_345": {
"inputs": {
- "nixpkgs": "nixpkgs_348"
+ "nixpkgs": "nixpkgs_347"
},
"locked": {
"lastModified": 1774455309,
@@ -9725,7 +9711,7 @@
},
"logos-nix_346": {
"inputs": {
- "nixpkgs": "nixpkgs_349"
+ "nixpkgs": "nixpkgs_348"
},
"locked": {
"lastModified": 1773955630,
@@ -9743,7 +9729,7 @@
},
"logos-nix_347": {
"inputs": {
- "nixpkgs": "nixpkgs_350"
+ "nixpkgs": "nixpkgs_349"
},
"locked": {
"lastModified": 1774455309,
@@ -9761,7 +9747,7 @@
},
"logos-nix_348": {
"inputs": {
- "nixpkgs": "nixpkgs_351"
+ "nixpkgs": "nixpkgs_350"
},
"locked": {
"lastModified": 1774455309,
@@ -9779,7 +9765,7 @@
},
"logos-nix_349": {
"inputs": {
- "nixpkgs": "nixpkgs_352"
+ "nixpkgs": "nixpkgs_351"
},
"locked": {
"lastModified": 1773955630,
@@ -9797,7 +9783,7 @@
},
"logos-nix_35": {
"inputs": {
- "nixpkgs": "nixpkgs_36"
+ "nixpkgs": "nixpkgs_35"
},
"locked": {
"lastModified": 1774455309,
@@ -9815,7 +9801,7 @@
},
"logos-nix_350": {
"inputs": {
- "nixpkgs": "nixpkgs_353"
+ "nixpkgs": "nixpkgs_352"
},
"locked": {
"lastModified": 1773955630,
@@ -9833,7 +9819,7 @@
},
"logos-nix_351": {
"inputs": {
- "nixpkgs": "nixpkgs_354"
+ "nixpkgs": "nixpkgs_353"
},
"locked": {
"lastModified": 1774455309,
@@ -9851,7 +9837,7 @@
},
"logos-nix_352": {
"inputs": {
- "nixpkgs": "nixpkgs_355"
+ "nixpkgs": "nixpkgs_354"
},
"locked": {
"lastModified": 1774455309,
@@ -9869,7 +9855,7 @@
},
"logos-nix_353": {
"inputs": {
- "nixpkgs": "nixpkgs_356"
+ "nixpkgs": "nixpkgs_355"
},
"locked": {
"lastModified": 1773955630,
@@ -9887,7 +9873,7 @@
},
"logos-nix_354": {
"inputs": {
- "nixpkgs": "nixpkgs_357"
+ "nixpkgs": "nixpkgs_356"
},
"locked": {
"lastModified": 1773955630,
@@ -9905,7 +9891,7 @@
},
"logos-nix_355": {
"inputs": {
- "nixpkgs": "nixpkgs_358"
+ "nixpkgs": "nixpkgs_357"
},
"locked": {
"lastModified": 1774455309,
@@ -9923,7 +9909,7 @@
},
"logos-nix_356": {
"inputs": {
- "nixpkgs": "nixpkgs_359"
+ "nixpkgs": "nixpkgs_358"
},
"locked": {
"lastModified": 1774455309,
@@ -9941,7 +9927,7 @@
},
"logos-nix_357": {
"inputs": {
- "nixpkgs": "nixpkgs_360"
+ "nixpkgs": "nixpkgs_359"
},
"locked": {
"lastModified": 1773955630,
@@ -9959,7 +9945,7 @@
},
"logos-nix_358": {
"inputs": {
- "nixpkgs": "nixpkgs_361"
+ "nixpkgs": "nixpkgs_360"
},
"locked": {
"lastModified": 1773955630,
@@ -9977,7 +9963,7 @@
},
"logos-nix_359": {
"inputs": {
- "nixpkgs": "nixpkgs_362"
+ "nixpkgs": "nixpkgs_361"
},
"locked": {
"lastModified": 1773955630,
@@ -9995,7 +9981,7 @@
},
"logos-nix_36": {
"inputs": {
- "nixpkgs": "nixpkgs_37"
+ "nixpkgs": "nixpkgs_36"
},
"locked": {
"lastModified": 1774455309,
@@ -10013,7 +9999,7 @@
},
"logos-nix_360": {
"inputs": {
- "nixpkgs": "nixpkgs_363"
+ "nixpkgs": "nixpkgs_362"
},
"locked": {
"lastModified": 1773955630,
@@ -10031,7 +10017,7 @@
},
"logos-nix_361": {
"inputs": {
- "nixpkgs": "nixpkgs_364"
+ "nixpkgs": "nixpkgs_363"
},
"locked": {
"lastModified": 1774455309,
@@ -10049,7 +10035,7 @@
},
"logos-nix_362": {
"inputs": {
- "nixpkgs": "nixpkgs_365"
+ "nixpkgs": "nixpkgs_364"
},
"locked": {
"lastModified": 1773955630,
@@ -10067,7 +10053,7 @@
},
"logos-nix_363": {
"inputs": {
- "nixpkgs": "nixpkgs_366"
+ "nixpkgs": "nixpkgs_365"
},
"locked": {
"lastModified": 1773955630,
@@ -10085,7 +10071,7 @@
},
"logos-nix_364": {
"inputs": {
- "nixpkgs": "nixpkgs_367"
+ "nixpkgs": "nixpkgs_366"
},
"locked": {
"lastModified": 1774455309,
@@ -10103,7 +10089,7 @@
},
"logos-nix_365": {
"inputs": {
- "nixpkgs": "nixpkgs_368"
+ "nixpkgs": "nixpkgs_367"
},
"locked": {
"lastModified": 1773955630,
@@ -10121,7 +10107,7 @@
},
"logos-nix_366": {
"inputs": {
- "nixpkgs": "nixpkgs_369"
+ "nixpkgs": "nixpkgs_368"
},
"locked": {
"lastModified": 1774455309,
@@ -10139,7 +10125,7 @@
},
"logos-nix_367": {
"inputs": {
- "nixpkgs": "nixpkgs_370"
+ "nixpkgs": "nixpkgs_369"
},
"locked": {
"lastModified": 1774455309,
@@ -10157,7 +10143,7 @@
},
"logos-nix_368": {
"inputs": {
- "nixpkgs": "nixpkgs_371"
+ "nixpkgs": "nixpkgs_370"
},
"locked": {
"lastModified": 1773955630,
@@ -10175,7 +10161,7 @@
},
"logos-nix_369": {
"inputs": {
- "nixpkgs": "nixpkgs_372"
+ "nixpkgs": "nixpkgs_371"
},
"locked": {
"lastModified": 1773955630,
@@ -10193,7 +10179,7 @@
},
"logos-nix_37": {
"inputs": {
- "nixpkgs": "nixpkgs_38"
+ "nixpkgs": "nixpkgs_37"
},
"locked": {
"lastModified": 1773955630,
@@ -10211,7 +10197,7 @@
},
"logos-nix_370": {
"inputs": {
- "nixpkgs": "nixpkgs_373"
+ "nixpkgs": "nixpkgs_372"
},
"locked": {
"lastModified": 1773955630,
@@ -10229,7 +10215,7 @@
},
"logos-nix_371": {
"inputs": {
- "nixpkgs": "nixpkgs_374"
+ "nixpkgs": "nixpkgs_373"
},
"locked": {
"lastModified": 1773955630,
@@ -10247,7 +10233,7 @@
},
"logos-nix_372": {
"inputs": {
- "nixpkgs": "nixpkgs_375"
+ "nixpkgs": "nixpkgs_374"
},
"locked": {
"lastModified": 1774455309,
@@ -10265,7 +10251,7 @@
},
"logos-nix_373": {
"inputs": {
- "nixpkgs": "nixpkgs_376"
+ "nixpkgs": "nixpkgs_375"
},
"locked": {
"lastModified": 1774455309,
@@ -10283,7 +10269,7 @@
},
"logos-nix_374": {
"inputs": {
- "nixpkgs": "nixpkgs_377"
+ "nixpkgs": "nixpkgs_376"
},
"locked": {
"lastModified": 1773955630,
@@ -10301,7 +10287,7 @@
},
"logos-nix_375": {
"inputs": {
- "nixpkgs": "nixpkgs_378"
+ "nixpkgs": "nixpkgs_377"
},
"locked": {
"lastModified": 1774455309,
@@ -10319,7 +10305,7 @@
},
"logos-nix_376": {
"inputs": {
- "nixpkgs": "nixpkgs_379"
+ "nixpkgs": "nixpkgs_378"
},
"locked": {
"lastModified": 1773955630,
@@ -10337,7 +10323,7 @@
},
"logos-nix_377": {
"inputs": {
- "nixpkgs": "nixpkgs_380"
+ "nixpkgs": "nixpkgs_379"
},
"locked": {
"lastModified": 1774455309,
@@ -10355,7 +10341,7 @@
},
"logos-nix_378": {
"inputs": {
- "nixpkgs": "nixpkgs_381"
+ "nixpkgs": "nixpkgs_380"
},
"locked": {
"lastModified": 1774455309,
@@ -10373,7 +10359,7 @@
},
"logos-nix_379": {
"inputs": {
- "nixpkgs": "nixpkgs_382"
+ "nixpkgs": "nixpkgs_381"
},
"locked": {
"lastModified": 1773955630,
@@ -10391,7 +10377,7 @@
},
"logos-nix_38": {
"inputs": {
- "nixpkgs": "nixpkgs_39"
+ "nixpkgs": "nixpkgs_38"
},
"locked": {
"lastModified": 1773955630,
@@ -10409,7 +10395,7 @@
},
"logos-nix_380": {
"inputs": {
- "nixpkgs": "nixpkgs_383"
+ "nixpkgs": "nixpkgs_382"
},
"locked": {
"lastModified": 1773955630,
@@ -10427,7 +10413,7 @@
},
"logos-nix_381": {
"inputs": {
- "nixpkgs": "nixpkgs_384"
+ "nixpkgs": "nixpkgs_383"
},
"locked": {
"lastModified": 1774455309,
@@ -10445,7 +10431,7 @@
},
"logos-nix_382": {
"inputs": {
- "nixpkgs": "nixpkgs_385"
+ "nixpkgs": "nixpkgs_384"
},
"locked": {
"lastModified": 1774455309,
@@ -10463,7 +10449,7 @@
},
"logos-nix_383": {
"inputs": {
- "nixpkgs": "nixpkgs_386"
+ "nixpkgs": "nixpkgs_385"
},
"locked": {
"lastModified": 1773955630,
@@ -10481,7 +10467,7 @@
},
"logos-nix_384": {
"inputs": {
- "nixpkgs": "nixpkgs_387"
+ "nixpkgs": "nixpkgs_386"
},
"locked": {
"lastModified": 1773955630,
@@ -10499,7 +10485,7 @@
},
"logos-nix_385": {
"inputs": {
- "nixpkgs": "nixpkgs_388"
+ "nixpkgs": "nixpkgs_387"
},
"locked": {
"lastModified": 1774455309,
@@ -10517,7 +10503,7 @@
},
"logos-nix_386": {
"inputs": {
- "nixpkgs": "nixpkgs_389"
+ "nixpkgs": "nixpkgs_388"
},
"locked": {
"lastModified": 1774455309,
@@ -10535,7 +10521,7 @@
},
"logos-nix_387": {
"inputs": {
- "nixpkgs": "nixpkgs_390"
+ "nixpkgs": "nixpkgs_389"
},
"locked": {
"lastModified": 1773955630,
@@ -10553,7 +10539,7 @@
},
"logos-nix_388": {
"inputs": {
- "nixpkgs": "nixpkgs_391"
+ "nixpkgs": "nixpkgs_390"
},
"locked": {
"lastModified": 1773955630,
@@ -10571,7 +10557,7 @@
},
"logos-nix_389": {
"inputs": {
- "nixpkgs": "nixpkgs_392"
+ "nixpkgs": "nixpkgs_391"
},
"locked": {
"lastModified": 1773955630,
@@ -10589,7 +10575,7 @@
},
"logos-nix_39": {
"inputs": {
- "nixpkgs": "nixpkgs_40"
+ "nixpkgs": "nixpkgs_39"
},
"locked": {
"lastModified": 1774455309,
@@ -10607,7 +10593,7 @@
},
"logos-nix_390": {
"inputs": {
- "nixpkgs": "nixpkgs_393"
+ "nixpkgs": "nixpkgs_392"
},
"locked": {
"lastModified": 1773955630,
@@ -10625,7 +10611,7 @@
},
"logos-nix_391": {
"inputs": {
- "nixpkgs": "nixpkgs_394"
+ "nixpkgs": "nixpkgs_393"
},
"locked": {
"lastModified": 1774455309,
@@ -10643,7 +10629,7 @@
},
"logos-nix_392": {
"inputs": {
- "nixpkgs": "nixpkgs_395"
+ "nixpkgs": "nixpkgs_394"
},
"locked": {
"lastModified": 1773955630,
@@ -10661,7 +10647,7 @@
},
"logos-nix_393": {
"inputs": {
- "nixpkgs": "nixpkgs_396"
+ "nixpkgs": "nixpkgs_395"
},
"locked": {
"lastModified": 1773955630,
@@ -10679,7 +10665,7 @@
},
"logos-nix_394": {
"inputs": {
- "nixpkgs": "nixpkgs_397"
+ "nixpkgs": "nixpkgs_396"
},
"locked": {
"lastModified": 1774455309,
@@ -10697,7 +10683,7 @@
},
"logos-nix_395": {
"inputs": {
- "nixpkgs": "nixpkgs_398"
+ "nixpkgs": "nixpkgs_397"
},
"locked": {
"lastModified": 1773955630,
@@ -10715,7 +10701,7 @@
},
"logos-nix_396": {
"inputs": {
- "nixpkgs": "nixpkgs_399"
+ "nixpkgs": "nixpkgs_398"
},
"locked": {
"lastModified": 1773955630,
@@ -10733,7 +10719,7 @@
},
"logos-nix_4": {
"inputs": {
- "nixpkgs": "nixpkgs_5"
+ "nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1773955630,
@@ -10751,7 +10737,7 @@
},
"logos-nix_40": {
"inputs": {
- "nixpkgs": "nixpkgs_41"
+ "nixpkgs": "nixpkgs_40"
},
"locked": {
"lastModified": 1774455309,
@@ -10769,7 +10755,7 @@
},
"logos-nix_41": {
"inputs": {
- "nixpkgs": "nixpkgs_42"
+ "nixpkgs": "nixpkgs_41"
},
"locked": {
"lastModified": 1773955630,
@@ -10787,7 +10773,7 @@
},
"logos-nix_42": {
"inputs": {
- "nixpkgs": "nixpkgs_43"
+ "nixpkgs": "nixpkgs_42"
},
"locked": {
"lastModified": 1773955630,
@@ -10805,7 +10791,7 @@
},
"logos-nix_43": {
"inputs": {
- "nixpkgs": "nixpkgs_44"
+ "nixpkgs": "nixpkgs_43"
},
"locked": {
"lastModified": 1774455309,
@@ -10823,7 +10809,7 @@
},
"logos-nix_44": {
"inputs": {
- "nixpkgs": "nixpkgs_45"
+ "nixpkgs": "nixpkgs_44"
},
"locked": {
"lastModified": 1774455309,
@@ -10841,7 +10827,7 @@
},
"logos-nix_45": {
"inputs": {
- "nixpkgs": "nixpkgs_46"
+ "nixpkgs": "nixpkgs_45"
},
"locked": {
"lastModified": 1773955630,
@@ -10859,7 +10845,7 @@
},
"logos-nix_46": {
"inputs": {
- "nixpkgs": "nixpkgs_47"
+ "nixpkgs": "nixpkgs_46"
},
"locked": {
"lastModified": 1773955630,
@@ -10877,7 +10863,7 @@
},
"logos-nix_47": {
"inputs": {
- "nixpkgs": "nixpkgs_48"
+ "nixpkgs": "nixpkgs_47"
},
"locked": {
"lastModified": 1773955630,
@@ -10895,7 +10881,7 @@
},
"logos-nix_48": {
"inputs": {
- "nixpkgs": "nixpkgs_49"
+ "nixpkgs": "nixpkgs_48"
},
"locked": {
"lastModified": 1773955630,
@@ -10913,7 +10899,7 @@
},
"logos-nix_49": {
"inputs": {
- "nixpkgs": "nixpkgs_50"
+ "nixpkgs": "nixpkgs_49"
},
"locked": {
"lastModified": 1774455309,
@@ -10931,7 +10917,7 @@
},
"logos-nix_5": {
"inputs": {
- "nixpkgs": "nixpkgs_6"
+ "nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1774455309,
@@ -10949,7 +10935,7 @@
},
"logos-nix_50": {
"inputs": {
- "nixpkgs": "nixpkgs_51"
+ "nixpkgs": "nixpkgs_50"
},
"locked": {
"lastModified": 1773955630,
@@ -10967,7 +10953,7 @@
},
"logos-nix_51": {
"inputs": {
- "nixpkgs": "nixpkgs_52"
+ "nixpkgs": "nixpkgs_51"
},
"locked": {
"lastModified": 1773955630,
@@ -10985,7 +10971,7 @@
},
"logos-nix_52": {
"inputs": {
- "nixpkgs": "nixpkgs_53"
+ "nixpkgs": "nixpkgs_52"
},
"locked": {
"lastModified": 1774455309,
@@ -11003,7 +10989,7 @@
},
"logos-nix_53": {
"inputs": {
- "nixpkgs": "nixpkgs_54"
+ "nixpkgs": "nixpkgs_53"
},
"locked": {
"lastModified": 1773955630,
@@ -11021,7 +11007,7 @@
},
"logos-nix_54": {
"inputs": {
- "nixpkgs": "nixpkgs_55"
+ "nixpkgs": "nixpkgs_54"
},
"locked": {
"lastModified": 1774455309,
@@ -11039,7 +11025,7 @@
},
"logos-nix_55": {
"inputs": {
- "nixpkgs": "nixpkgs_56"
+ "nixpkgs": "nixpkgs_55"
},
"locked": {
"lastModified": 1773955630,
@@ -11057,7 +11043,7 @@
},
"logos-nix_56": {
"inputs": {
- "nixpkgs": "nixpkgs_57"
+ "nixpkgs": "nixpkgs_56"
},
"locked": {
"lastModified": 1774455309,
@@ -11075,7 +11061,7 @@
},
"logos-nix_57": {
"inputs": {
- "nixpkgs": "nixpkgs_58"
+ "nixpkgs": "nixpkgs_57"
},
"locked": {
"lastModified": 1773955630,
@@ -11093,7 +11079,7 @@
},
"logos-nix_58": {
"inputs": {
- "nixpkgs": "nixpkgs_59"
+ "nixpkgs": "nixpkgs_58"
},
"locked": {
"lastModified": 1774455309,
@@ -11111,7 +11097,7 @@
},
"logos-nix_59": {
"inputs": {
- "nixpkgs": "nixpkgs_60"
+ "nixpkgs": "nixpkgs_59"
},
"locked": {
"lastModified": 1773955630,
@@ -11129,7 +11115,7 @@
},
"logos-nix_6": {
"inputs": {
- "nixpkgs": "nixpkgs_7"
+ "nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1773955630,
@@ -11147,7 +11133,7 @@
},
"logos-nix_60": {
"inputs": {
- "nixpkgs": "nixpkgs_61"
+ "nixpkgs": "nixpkgs_60"
},
"locked": {
"lastModified": 1774455309,
@@ -11165,7 +11151,7 @@
},
"logos-nix_61": {
"inputs": {
- "nixpkgs": "nixpkgs_62"
+ "nixpkgs": "nixpkgs_61"
},
"locked": {
"lastModified": 1773955630,
@@ -11183,7 +11169,7 @@
},
"logos-nix_62": {
"inputs": {
- "nixpkgs": "nixpkgs_63"
+ "nixpkgs": "nixpkgs_62"
},
"locked": {
"lastModified": 1773955630,
@@ -11201,7 +11187,7 @@
},
"logos-nix_63": {
"inputs": {
- "nixpkgs": "nixpkgs_64"
+ "nixpkgs": "nixpkgs_63"
},
"locked": {
"lastModified": 1774455309,
@@ -11219,7 +11205,7 @@
},
"logos-nix_64": {
"inputs": {
- "nixpkgs": "nixpkgs_65"
+ "nixpkgs": "nixpkgs_64"
},
"locked": {
"lastModified": 1773955630,
@@ -11237,7 +11223,7 @@
},
"logos-nix_65": {
"inputs": {
- "nixpkgs": "nixpkgs_66"
+ "nixpkgs": "nixpkgs_65"
},
"locked": {
"lastModified": 1773955630,
@@ -11255,7 +11241,7 @@
},
"logos-nix_66": {
"inputs": {
- "nixpkgs": "nixpkgs_67"
+ "nixpkgs": "nixpkgs_66"
},
"locked": {
"lastModified": 1773955630,
@@ -11273,7 +11259,7 @@
},
"logos-nix_67": {
"inputs": {
- "nixpkgs": "nixpkgs_68"
+ "nixpkgs": "nixpkgs_67"
},
"locked": {
"lastModified": 1773955630,
@@ -11291,7 +11277,7 @@
},
"logos-nix_68": {
"inputs": {
- "nixpkgs": "nixpkgs_69"
+ "nixpkgs": "nixpkgs_68"
},
"locked": {
"lastModified": 1774455309,
@@ -11309,7 +11295,7 @@
},
"logos-nix_69": {
"inputs": {
- "nixpkgs": "nixpkgs_70"
+ "nixpkgs": "nixpkgs_69"
},
"locked": {
"lastModified": 1773955630,
@@ -11327,7 +11313,7 @@
},
"logos-nix_7": {
"inputs": {
- "nixpkgs": "nixpkgs_8"
+ "nixpkgs": "nixpkgs_7"
},
"locked": {
"lastModified": 1774455309,
@@ -11345,7 +11331,7 @@
},
"logos-nix_70": {
"inputs": {
- "nixpkgs": "nixpkgs_71"
+ "nixpkgs": "nixpkgs_70"
},
"locked": {
"lastModified": 1774455309,
@@ -11363,7 +11349,7 @@
},
"logos-nix_71": {
"inputs": {
- "nixpkgs": "nixpkgs_72"
+ "nixpkgs": "nixpkgs_71"
},
"locked": {
"lastModified": 1774455309,
@@ -11381,7 +11367,7 @@
},
"logos-nix_72": {
"inputs": {
- "nixpkgs": "nixpkgs_73"
+ "nixpkgs": "nixpkgs_72"
},
"locked": {
"lastModified": 1773955630,
@@ -11399,7 +11385,7 @@
},
"logos-nix_73": {
"inputs": {
- "nixpkgs": "nixpkgs_74"
+ "nixpkgs": "nixpkgs_73"
},
"locked": {
"lastModified": 1773955630,
@@ -11417,7 +11403,7 @@
},
"logos-nix_74": {
"inputs": {
- "nixpkgs": "nixpkgs_75"
+ "nixpkgs": "nixpkgs_74"
},
"locked": {
"lastModified": 1773955630,
@@ -11435,7 +11421,7 @@
},
"logos-nix_75": {
"inputs": {
- "nixpkgs": "nixpkgs_76"
+ "nixpkgs": "nixpkgs_75"
},
"locked": {
"lastModified": 1773955630,
@@ -11453,7 +11439,7 @@
},
"logos-nix_76": {
"inputs": {
- "nixpkgs": "nixpkgs_77"
+ "nixpkgs": "nixpkgs_76"
},
"locked": {
"lastModified": 1773955630,
@@ -11471,7 +11457,7 @@
},
"logos-nix_77": {
"inputs": {
- "nixpkgs": "nixpkgs_78"
+ "nixpkgs": "nixpkgs_77"
},
"locked": {
"lastModified": 1774455309,
@@ -11489,7 +11475,7 @@
},
"logos-nix_78": {
"inputs": {
- "nixpkgs": "nixpkgs_79"
+ "nixpkgs": "nixpkgs_78"
},
"locked": {
"lastModified": 1773955630,
@@ -11507,7 +11493,7 @@
},
"logos-nix_79": {
"inputs": {
- "nixpkgs": "nixpkgs_80"
+ "nixpkgs": "nixpkgs_79"
},
"locked": {
"lastModified": 1774455309,
@@ -11525,7 +11511,7 @@
},
"logos-nix_8": {
"inputs": {
- "nixpkgs": "nixpkgs_9"
+ "nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1774455309,
@@ -11543,7 +11529,7 @@
},
"logos-nix_80": {
"inputs": {
- "nixpkgs": "nixpkgs_81"
+ "nixpkgs": "nixpkgs_80"
},
"locked": {
"lastModified": 1774455309,
@@ -11561,7 +11547,7 @@
},
"logos-nix_81": {
"inputs": {
- "nixpkgs": "nixpkgs_82"
+ "nixpkgs": "nixpkgs_81"
},
"locked": {
"lastModified": 1773955630,
@@ -11579,7 +11565,7 @@
},
"logos-nix_82": {
"inputs": {
- "nixpkgs": "nixpkgs_83"
+ "nixpkgs": "nixpkgs_82"
},
"locked": {
"lastModified": 1773955630,
@@ -11597,7 +11583,7 @@
},
"logos-nix_83": {
"inputs": {
- "nixpkgs": "nixpkgs_84"
+ "nixpkgs": "nixpkgs_83"
},
"locked": {
"lastModified": 1774455309,
@@ -11615,7 +11601,7 @@
},
"logos-nix_84": {
"inputs": {
- "nixpkgs": "nixpkgs_85"
+ "nixpkgs": "nixpkgs_84"
},
"locked": {
"lastModified": 1774455309,
@@ -11633,7 +11619,7 @@
},
"logos-nix_85": {
"inputs": {
- "nixpkgs": "nixpkgs_86"
+ "nixpkgs": "nixpkgs_85"
},
"locked": {
"lastModified": 1773955630,
@@ -11651,7 +11637,7 @@
},
"logos-nix_86": {
"inputs": {
- "nixpkgs": "nixpkgs_87"
+ "nixpkgs": "nixpkgs_86"
},
"locked": {
"lastModified": 1773955630,
@@ -11669,7 +11655,7 @@
},
"logos-nix_87": {
"inputs": {
- "nixpkgs": "nixpkgs_88"
+ "nixpkgs": "nixpkgs_87"
},
"locked": {
"lastModified": 1774455309,
@@ -11687,7 +11673,7 @@
},
"logos-nix_88": {
"inputs": {
- "nixpkgs": "nixpkgs_89"
+ "nixpkgs": "nixpkgs_88"
},
"locked": {
"lastModified": 1774455309,
@@ -11705,7 +11691,7 @@
},
"logos-nix_89": {
"inputs": {
- "nixpkgs": "nixpkgs_90"
+ "nixpkgs": "nixpkgs_89"
},
"locked": {
"lastModified": 1773955630,
@@ -11723,7 +11709,7 @@
},
"logos-nix_9": {
"inputs": {
- "nixpkgs": "nixpkgs_10"
+ "nixpkgs": "nixpkgs_9"
},
"locked": {
"lastModified": 1774455309,
@@ -11741,7 +11727,7 @@
},
"logos-nix_90": {
"inputs": {
- "nixpkgs": "nixpkgs_91"
+ "nixpkgs": "nixpkgs_90"
},
"locked": {
"lastModified": 1773955630,
@@ -11759,7 +11745,7 @@
},
"logos-nix_91": {
"inputs": {
- "nixpkgs": "nixpkgs_92"
+ "nixpkgs": "nixpkgs_91"
},
"locked": {
"lastModified": 1773955630,
@@ -11777,7 +11763,7 @@
},
"logos-nix_92": {
"inputs": {
- "nixpkgs": "nixpkgs_93"
+ "nixpkgs": "nixpkgs_92"
},
"locked": {
"lastModified": 1773955630,
@@ -11795,7 +11781,7 @@
},
"logos-nix_93": {
"inputs": {
- "nixpkgs": "nixpkgs_94"
+ "nixpkgs": "nixpkgs_93"
},
"locked": {
"lastModified": 1774455309,
@@ -11813,7 +11799,7 @@
},
"logos-nix_94": {
"inputs": {
- "nixpkgs": "nixpkgs_95"
+ "nixpkgs": "nixpkgs_94"
},
"locked": {
"lastModified": 1773955630,
@@ -11831,7 +11817,7 @@
},
"logos-nix_95": {
"inputs": {
- "nixpkgs": "nixpkgs_96"
+ "nixpkgs": "nixpkgs_95"
},
"locked": {
"lastModified": 1773955630,
@@ -11849,7 +11835,7 @@
},
"logos-nix_96": {
"inputs": {
- "nixpkgs": "nixpkgs_97"
+ "nixpkgs": "nixpkgs_96"
},
"locked": {
"lastModified": 1774455309,
@@ -11867,7 +11853,7 @@
},
"logos-nix_97": {
"inputs": {
- "nixpkgs": "nixpkgs_98"
+ "nixpkgs": "nixpkgs_97"
},
"locked": {
"lastModified": 1773955630,
@@ -11885,7 +11871,7 @@
},
"logos-nix_98": {
"inputs": {
- "nixpkgs": "nixpkgs_99"
+ "nixpkgs": "nixpkgs_98"
},
"locked": {
"lastModified": 1774455309,
@@ -11903,7 +11889,7 @@
},
"logos-nix_99": {
"inputs": {
- "nixpkgs": "nixpkgs_100"
+ "nixpkgs": "nixpkgs_99"
},
"locked": {
"lastModified": 1774455309,
@@ -20128,11 +20114,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1783776592,
- "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
+ "lastModified": 1759036355,
+ "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
+ "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
@@ -20576,32 +20562,32 @@
},
"nixpkgs_124": {
"locked": {
- "lastModified": 1759036355,
- "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
+ "lastModified": 1767313136,
+ "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
+ "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-unstable",
+ "ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_125": {
"locked": {
- "lastModified": 1767313136,
- "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
+ "lastModified": 1759036355,
+ "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
+ "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-25.05",
+ "ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
@@ -23168,32 +23154,32 @@
},
"nixpkgs_270": {
"locked": {
- "lastModified": 1759036355,
- "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
+ "lastModified": 1767313136,
+ "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
+ "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-unstable",
+ "ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_271": {
"locked": {
- "lastModified": 1767313136,
- "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
+ "lastModified": 1759036355,
+ "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
+ "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-25.05",
+ "ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
@@ -25440,16 +25426,16 @@
},
"nixpkgs_399": {
"locked": {
- "lastModified": 1759036355,
- "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
+ "lastModified": 1782841183,
+ "narHash": "sha256-Ndt/5R7UN4rBdhFR1lxHZZZ42cD6vGlnuxC2VvvsKE4=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
+ "rev": "c9bfd86ed684d27e63b0ff9ebb18699f84f27a3b",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-unstable",
+ "ref": "nixos-25.11-small",
"repo": "nixpkgs",
"type": "github"
}
@@ -26808,10 +26794,10 @@
},
"root": {
"inputs": {
- "amm_client": "amm_client",
+ "crane": "crane",
"logos-module-builder": "logos-module-builder",
"logos_execution_zone": "logos_execution_zone",
- "shared_wallet": "shared_wallet"
+ "nixpkgs": "nixpkgs_399"
}
},
"rust-overlay": {
@@ -26881,7 +26867,7 @@
},
"rust-rapidsnark": {
"inputs": {
- "nixpkgs": "nixpkgs_271"
+ "nixpkgs": "nixpkgs_270"
},
"locked": {
"lastModified": 1781090841,
@@ -26897,18 +26883,6 @@
"rev": "e91187f8ccb5bbfc7bb00dac88169112428da78f",
"type": "github"
}
- },
- "shared_wallet": {
- "flake": false,
- "locked": {
- "path": "../shared/wallet",
- "type": "path"
- },
- "original": {
- "path": "../shared/wallet",
- "type": "path"
- },
- "parent": []
}
},
"root": "root",
diff --git a/apps/amm/scripts/check-macos-prerequisites.sh b/apps/amm/scripts/check-macos-prerequisites.sh
new file mode 100755
index 00000000..b2e48281
--- /dev/null
+++ b/apps/amm/scripts/check-macos-prerequisites.sh
@@ -0,0 +1,117 @@
+#!/usr/bin/env bash
+
+# Verify the host requirements for a supported native macOS AMM build.
+
+set -euo pipefail
+
+failures=0
+
+pass() {
+ printf 'ok: %s\n' "$1"
+}
+
+fail() {
+ printf 'missing: %s\n' "$1" >&2
+ failures=$((failures + 1))
+}
+
+if [[ "$(uname -s)" != "Darwin" ]]; then
+ printf 'missing: this script must run on macOS\n' >&2
+ exit 1
+fi
+
+architecture="$(uname -m)"
+case "$architecture" in
+ arm64|x86_64)
+ pass "supported architecture $architecture"
+ ;;
+ *)
+ fail "unsupported architecture $architecture"
+ ;;
+esac
+
+developer_dir="$(xcode-select -p 2>/dev/null || true)"
+if [[ "$developer_dir" == *.app/Contents/Developer ]]; then
+ pass "full Xcode selected at $developer_dir"
+ required_gib=40
+else
+ fail "full Xcode is not selected (current developer directory: ${developer_dir:-none})"
+ required_gib=60
+fi
+
+if xcode_version="$(xcodebuild -version 2>/dev/null)"; then
+ pass "$(printf '%s' "$xcode_version" | tr '\n' ' ')"
+else
+ fail "xcodebuild is unavailable; install Xcode.app and select it with xcode-select"
+fi
+
+if xcodebuild -checkFirstLaunchStatus >/dev/null 2>&1; then
+ pass "Xcode first-launch setup and licence are complete"
+else
+ fail "Xcode setup is incomplete; run sudo xcodebuild -license accept and -runFirstLaunch"
+fi
+
+for tool in metal metallib; do
+ if tool_path="$(xcrun --sdk macosx --find "$tool" 2>/dev/null)" &&
+ [[ -x "$tool_path" ]]; then
+ pass "$tool at $tool_path"
+ else
+ fail "$tool is unavailable; run xcodebuild -downloadComponent MetalToolchain"
+ fi
+done
+
+nix_bin="$(command -v nix 2>/dev/null || true)"
+if [[ -z "$nix_bin" && -x /nix/var/nix/profiles/default/bin/nix ]]; then
+ nix_bin=/nix/var/nix/profiles/default/bin/nix
+fi
+
+if [[ -n "$nix_bin" ]]; then
+ pass "Nix at $nix_bin ($("$nix_bin" --version))"
+
+ configured_features="$(
+ "$nix_bin" config show experimental-features 2>/dev/null ||
+ "$nix_bin" show-config 2>/dev/null |
+ awk -F ' = ' '$1 == "experimental-features" { print $2 }'
+ )"
+ if [[ " $configured_features " == *" nix-command "* &&
+ " $configured_features " == *" flakes "* ]]; then
+ pass "Nix flakes and nix-command are enabled"
+ else
+ fail "enable experimental-features = nix-command flakes in nix.conf"
+ fi
+
+ if "$nix_bin" --extra-experimental-features 'nix-command flakes' \
+ store info >/dev/null 2>&1; then
+ pass "Nix daemon and store are available"
+ else
+ fail "Nix cannot contact its daemon or store"
+ fi
+
+ sandbox_setting="$("$nix_bin" config show sandbox 2>/dev/null || true)"
+ if [[ "$sandbox_setting" == "false" ]]; then
+ pass "Nix macOS sandbox is disabled for the host Metal toolchain"
+ else
+ fail "Nix sandbox must be false for the host Metal toolchain (current: ${sandbox_setting:-unknown})"
+ fi
+else
+ fail "Nix is unavailable; install multi-user Nix and start its daemon"
+fi
+
+if [[ -d /nix ]]; then
+ available_kib="$(df -Pk /nix | awk 'NR == 2 { print $4 }')"
+ required_kib=$((required_gib * 1024 * 1024))
+ if [[ "${available_kib:-0}" -ge "$required_kib" ]]; then
+ pass "at least ${required_gib} GiB free on the Nix volume"
+ else
+ fail "less than ${required_gib} GiB free on the Nix volume"
+ fi
+else
+ fail "/nix is not mounted"
+fi
+
+if (( failures > 0 )); then
+ printf '\nSee MACOS.md for installation and recovery instructions.\n' >&2
+ exit 1
+fi
+
+printf '\nAll macOS AMM prerequisites are available.\n'
From e01878faf7805e27e3d1d7eea5ce52877378b994 Mon Sep 17 00:00:00 2001
From: r4bbit <445106+0x-r4bbit@users.noreply.github.com>
Date: Fri, 24 Jul 2026 14:45:15 +0200
Subject: [PATCH 07/10] chore: remove unnecessary macos prerequisites docs
---
apps/amm/MACOS.md | 173 ------------------
apps/amm/README.md | 3 -
apps/amm/scripts/check-macos-prerequisites.sh | 117 ------------
3 files changed, 293 deletions(-)
delete mode 100644 apps/amm/MACOS.md
delete mode 100755 apps/amm/scripts/check-macos-prerequisites.sh
diff --git a/apps/amm/MACOS.md b/apps/amm/MACOS.md
deleted file mode 100644
index 0036bc9e..00000000
--- a/apps/amm/MACOS.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# AMM UI on macOS
-
-The AMM flake exposes native packages for Apple Silicon and Intel macOS. Nix
-provides the Rust, C++, Qt, and Apple SDK build dependencies. The only required
-host toolchain is Apple's Metal compiler, which Apple distributes through
-Xcode rather than through a redistributable package.
-
-The standalone build needs no input overrides, temporary source pins, or
-`DYLD_LIBRARY_PATH`.
-
-## One-time prerequisites
-
-### Nix
-
-Install Nix in multi-user mode, make sure its daemon is available, and enable
-flakes:
-
-```sh
-mkdir -p ~/.config/nix
-printf '%s\n' 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
-nix --version
-nix store info
-```
-
-Nix normally has `sandbox = false` on macOS. This build must retain that
-setting because RISC Zero invokes Apple's host-installed Metal toolchain. The
-preflight script below reports an actionable error if the daemon is configured
-otherwise.
-
-### Xcode and the Metal Toolchain
-
-Apple Command Line Tools alone do not include `metal` and `metallib`. Install
-the newest full Xcode release supported by the installed macOS from the App
-Store or [Apple Developer Downloads](https://developer.apple.com/download/all/).
-This requires an Apple account and cannot be automated by the repository.
-
-After installation:
-
-```sh
-sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
-sudo xcodebuild -license accept
-sudo xcodebuild -runFirstLaunch
-xcodebuild -downloadComponent MetalToolchain
-xcrun --sdk macosx --find metal
-xcrun --sdk macosx --find metallib
-```
-
-The last two commands must print executable paths. Do not use
-`RISC0_SKIP_BUILD_KERNELS=1`: the pinned dependency graph requires the real
-Metal kernels.
-
-### Disk space and preflight
-
-Leave at least 60 GiB free before downloading Xcode. Once Xcode is installed,
-keep at least 40 GiB free for the first Nix build. Later builds reuse the Nix
-store.
-
-Run the complete machine check from `apps/amm`:
-
-```sh
-bash scripts/check-macos-prerequisites.sh
-```
-
-## Build and run
-
-From `apps/amm`:
-
-```sh
-nix build .
-nix run .
-```
-
-The first cold build is large because it includes Qt, the wallet, the AMM
-client, and the RISC Zero Metal kernels. The first start opens without a
-wallet; select **Connect** to create or open `~/.lee/wallet/`.
-
-## Why the flake is structured this way
-
-Nix 2.25 rejects committed lock entries for unlocked relative flake inputs such
-as `path:../shared/wallet` and `path:../..`. Newer Nix versions may accept that
-layout, which is why it can appear to work on one developer's Mac and fail on
-another.
-
-The portable layout avoids both relative flake inputs:
-
-- the shared wallet is passed to CMake as an ordinary Nix source path;
-- the repository-root AMM client package is imported directly with the same
- locked `nixpkgs` and Crane inputs;
-- `flake.lock` contains only immutable upstream sources.
-
-The app output uses the builder's full-library QML package layout. The current
-`qt-plugin` layout copies the UI plugin and replica factory but omits sibling
-external libraries such as `libamm_client`. The module metadata still selects
-the C++ backend, so this packaging choice does not change process isolation or
-runtime behavior.
-
-Do not replace these with a pull-request URL, self-reference, or local
-`path:` flake input. A normal `nix flake lock`, `nix build`, and `nix run`
-must work without overrides.
-
-## Known-good validation host
-
-This configuration has been exercised on:
-
-- Apple Silicon (`arm64`)
-- macOS 26.5.2
-- Xcode 26.6 (build 17F113)
-- Metal Toolchain 32023.883
-- Nix 2.25.3
-
-The flake also evaluates its complete package and app outputs for
-`x86_64-darwin`; an Intel Mac is still required for native runtime validation.
-
-## Recovery
-
-### Nix is unavailable
-
-Open a new terminal after installing Nix. For the standard multi-user
-installation, check the store and daemon before trying to bootstrap it again:
-
-```sh
-test -x /nix/var/nix/profiles/default/bin/nix
-nix store info
-sudo launchctl print system/org.nixos.nix-daemon
-```
-
-`launchctl bootstrap` can report an I/O error when a service is already loaded
-or in an inconsistent state; that message alone does not prove the daemon is
-stopped.
-
-### `xcrun` cannot find `metal`
-
-The selected developer directory points at Command Line Tools, or Xcode lacks
-the separately downloaded Metal Toolchain component. Repeat the Xcode setup
-and verification commands above.
-
-Apple does not distribute the Metal compiler as a standalone redistributable
-SDK, so Nix cannot install this host component.
-
-### The build reports that the macOS sandbox blocks Metal
-
-Confirm the active setting:
-
-```sh
-nix config show sandbox
-```
-
-For this native macOS build it must be `false`. If a machine-wide policy set it
-to `true`, update that Nix installation's daemon configuration and restart the
-daemon before retrying.
-
-### The first build runs out of space
-
-Remove only disposable build outputs, then collect dead Nix store paths:
-
-```sh
-rm -rf /private/tmp/amm-build
-nix-store --gc
-```
-
-Do not remove `/nix/store` directly.
-
-### A lock error mentions an unlocked input
-
-Restore the committed app lock and verify it without rewriting it:
-
-```sh
-git restore flake.lock
-nix flake metadata --no-update-lock-file .
-```
-
-If the error still names `path:../shared/wallet` or `path:../..`, the checkout
-does not contain the portable flake fix described above.
diff --git a/apps/amm/README.md b/apps/amm/README.md
index 97e7044f..126e0f86 100644
--- a/apps/amm/README.md
+++ b/apps/amm/README.md
@@ -50,9 +50,6 @@ nix profile install 'github:logos-co/logos-package-manager#cli'
This makes `lgpm` available as a global command.
-For macOS prerequisites, the Metal toolchain requirement, and common Nix
-recovery steps, see [macOS setup](MACOS.md).
-
## Running the UI standalone
The app is built from the **repository-root** flake (which also provides the
diff --git a/apps/amm/scripts/check-macos-prerequisites.sh b/apps/amm/scripts/check-macos-prerequisites.sh
deleted file mode 100755
index b2e48281..00000000
--- a/apps/amm/scripts/check-macos-prerequisites.sh
+++ /dev/null
@@ -1,117 +0,0 @@
-#!/usr/bin/env bash
-
-# Verify the host requirements for a supported native macOS AMM build.
-
-set -euo pipefail
-
-failures=0
-
-pass() {
- printf 'ok: %s\n' "$1"
-}
-
-fail() {
- printf 'missing: %s\n' "$1" >&2
- failures=$((failures + 1))
-}
-
-if [[ "$(uname -s)" != "Darwin" ]]; then
- printf 'missing: this script must run on macOS\n' >&2
- exit 1
-fi
-
-architecture="$(uname -m)"
-case "$architecture" in
- arm64|x86_64)
- pass "supported architecture $architecture"
- ;;
- *)
- fail "unsupported architecture $architecture"
- ;;
-esac
-
-developer_dir="$(xcode-select -p 2>/dev/null || true)"
-if [[ "$developer_dir" == *.app/Contents/Developer ]]; then
- pass "full Xcode selected at $developer_dir"
- required_gib=40
-else
- fail "full Xcode is not selected (current developer directory: ${developer_dir:-none})"
- required_gib=60
-fi
-
-if xcode_version="$(xcodebuild -version 2>/dev/null)"; then
- pass "$(printf '%s' "$xcode_version" | tr '\n' ' ')"
-else
- fail "xcodebuild is unavailable; install Xcode.app and select it with xcode-select"
-fi
-
-if xcodebuild -checkFirstLaunchStatus >/dev/null 2>&1; then
- pass "Xcode first-launch setup and licence are complete"
-else
- fail "Xcode setup is incomplete; run sudo xcodebuild -license accept and -runFirstLaunch"
-fi
-
-for tool in metal metallib; do
- if tool_path="$(xcrun --sdk macosx --find "$tool" 2>/dev/null)" &&
- [[ -x "$tool_path" ]]; then
- pass "$tool at $tool_path"
- else
- fail "$tool is unavailable; run xcodebuild -downloadComponent MetalToolchain"
- fi
-done
-
-nix_bin="$(command -v nix 2>/dev/null || true)"
-if [[ -z "$nix_bin" && -x /nix/var/nix/profiles/default/bin/nix ]]; then
- nix_bin=/nix/var/nix/profiles/default/bin/nix
-fi
-
-if [[ -n "$nix_bin" ]]; then
- pass "Nix at $nix_bin ($("$nix_bin" --version))"
-
- configured_features="$(
- "$nix_bin" config show experimental-features 2>/dev/null ||
- "$nix_bin" show-config 2>/dev/null |
- awk -F ' = ' '$1 == "experimental-features" { print $2 }'
- )"
- if [[ " $configured_features " == *" nix-command "* &&
- " $configured_features " == *" flakes "* ]]; then
- pass "Nix flakes and nix-command are enabled"
- else
- fail "enable experimental-features = nix-command flakes in nix.conf"
- fi
-
- if "$nix_bin" --extra-experimental-features 'nix-command flakes' \
- store info >/dev/null 2>&1; then
- pass "Nix daemon and store are available"
- else
- fail "Nix cannot contact its daemon or store"
- fi
-
- sandbox_setting="$("$nix_bin" config show sandbox 2>/dev/null || true)"
- if [[ "$sandbox_setting" == "false" ]]; then
- pass "Nix macOS sandbox is disabled for the host Metal toolchain"
- else
- fail "Nix sandbox must be false for the host Metal toolchain (current: ${sandbox_setting:-unknown})"
- fi
-else
- fail "Nix is unavailable; install multi-user Nix and start its daemon"
-fi
-
-if [[ -d /nix ]]; then
- available_kib="$(df -Pk /nix | awk 'NR == 2 { print $4 }')"
- required_kib=$((required_gib * 1024 * 1024))
- if [[ "${available_kib:-0}" -ge "$required_kib" ]]; then
- pass "at least ${required_gib} GiB free on the Nix volume"
- else
- fail "less than ${required_gib} GiB free on the Nix volume"
- fi
-else
- fail "/nix is not mounted"
-fi
-
-if (( failures > 0 )); then
- printf '\nSee MACOS.md for installation and recovery instructions.\n' >&2
- exit 1
-fi
-
-printf '\nAll macOS AMM prerequisites are available.\n'
From 44b355ac7daae8dedb5f195837c1b24500b73878 Mon Sep 17 00:00:00 2001
From: r4bbit <445106+0x-r4bbit@users.noreply.github.com>
Date: Fri, 24 Jul 2026 17:33:37 +0200
Subject: [PATCH 08/10] factor(amm-ui): source new-position network context
from AMM_PROGRAM_BIN/TOKENS_CONFIG/wallet
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The create-pool / new-position flow carried its own network layer:
AMM_UI_NETWORK + AMM_UI_DEVNET_FILE (a devnet.json) or a bundled
config/networks.json supplied the AMM program id and token set, and a
JSON-RPC channel/checkpoint "identity probe" gated the flow to a verified
network. Main already exposes all of this the way the Swap view consumes it,
so collapse onto those sources instead of a parallel system:
- ammProgramId <- $AMM_PROGRAM_BIN (derived like swapExactInput's program id;
doubles as the quote's network fingerprint so a quote can't
be replayed against a different deployment)
- tokenIds <- $TOKENS_CONFIG (amm-tokens.json), same as the Swap picker
- sequencer <- the wallet config (already surfaced via syncWalletState)
networkSnapshot() builds the ActiveNetworkSnapshot from those; status is
"ready"/"config_missing", gated to "loading" until wallet state resolves so no
module reads happen during construction. The channel probe is gone — submit
needs no channelId (the wallet module supplies the channel via
submitPublicTransaction), so the whole verification apparatus was overhead.
Removes: AMM_UI_NETWORK / AMM_UI_DEVNET_FILE, devnet.json / networks.json, the
ActiveNetwork class (+ its test) and its QNetwork channel probe, and Qt6Network.
ActiveNetwork.h keeps only the ActiveNetworkSnapshot struct. Run command drops
the AMM_UI_* vars:
```
LEE_WALLET_HOME_DIR=… AMM_PROGRAM_BIN=… TOKENS_CONFIG=… nix run .#amm-ui
```
---
apps/amm/CMakeLists.txt | 17 ---
apps/amm/README.md | 16 ++-
apps/amm/config/networks.json | 18 ---
apps/amm/src/ActiveNetwork.cpp | 141 -------------------
apps/amm/src/ActiveNetwork.h | 29 +---
apps/amm/src/AmmUiBackend.cpp | 165 ++++++++---------------
apps/amm/src/AmmUiBackend.h | 16 ++-
apps/amm/tests/cpp/ActiveNetworkTest.cpp | 65 ---------
8 files changed, 83 insertions(+), 384 deletions(-)
delete mode 100644 apps/amm/config/networks.json
delete mode 100644 apps/amm/src/ActiveNetwork.cpp
delete mode 100644 apps/amm/tests/cpp/ActiveNetworkTest.cpp
diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt
index 037867cb..6e8e4370 100644
--- a/apps/amm/CMakeLists.txt
+++ b/apps/amm/CMakeLists.txt
@@ -37,17 +37,14 @@ logos_module(
src/AmmUiBackend.h
src/AmmUiBackend.cpp
src/ActiveNetwork.h
- src/ActiveNetwork.cpp
src/AmmClient.h
src/AmmClient.cpp
src/NewPositionRuntime.h
src/NewPositionRuntime.cpp
FIND_PACKAGES
Qt6Gui
- Qt6Network
LINK_LIBRARIES
Qt6::Gui
- Qt6::Network
PkgConfig::BASE58
LINK_TARGETS
logos_wallet_access
@@ -56,21 +53,7 @@ logos_module(
amm_client
)
-qt_add_resources(amm_ui_module_plugin amm_ui_config
- PREFIX "/amm"
- FILES
- config/networks.json
-)
-
if(BUILD_TESTING)
- add_executable(amm_active_network_test
- tests/cpp/ActiveNetworkTest.cpp
- src/ActiveNetwork.cpp
- )
- target_include_directories(amm_active_network_test PRIVATE src)
- target_link_libraries(amm_active_network_test PRIVATE Qt6::Core)
- add_test(NAME amm_active_network COMMAND amm_active_network_test)
-
add_executable(amm_new_position_runtime_test
tests/cpp/NewPositionRuntimeTest.cpp
src/NewPositionRuntime.cpp
diff --git a/apps/amm/README.md b/apps/amm/README.md
index 126e0f86..b33841a0 100644
--- a/apps/amm/README.md
+++ b/apps/amm/README.md
@@ -135,10 +135,12 @@ core module from `result-core/` and the UI plugin from `result-lgx/`:
4. Choose the core module `.lgx` from `result-core/`, then the UI plugin `.lgx`
from `result-lgx/`
-To actually use the Swap view you must also set `AMM_PROGRAM_BIN` and
-`TOKENS_CONFIG` (both explained below). Run this **from the repo root** — use
-absolute paths (`$(pwd)/…`), because `nix run` may not preserve the working
-directory, so relative paths won't resolve:
+To actually use the on-chain views (**Swap** and **Liquidity**) you must also
+set `AMM_PROGRAM_BIN` and `TOKENS_CONFIG` (both explained below). Both views read
+the same two — the AMM program id is derived from `AMM_PROGRAM_BIN`, the token
+set from `TOKENS_CONFIG`, and the sequencer from the wallet config. Run this
+**from the repo root** — use absolute paths (`$(pwd)/…`), because `nix run` may
+not preserve the working directory, so relative paths won't resolve:
```bash
AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \
@@ -146,10 +148,10 @@ TOKENS_CONFIG=$(pwd)/amm-tokens.json \
nix run .#amm-ui
```
-Without `AMM_PROGRAM_BIN` the Swap view stays disabled; without `TOKENS_CONFIG`
-the token picker is empty. Each is detailed below.
+Without `AMM_PROGRAM_BIN` the Swap and Liquidity views stay disabled; without
+`TOKENS_CONFIG` the token picker is empty. Each is detailed below.
-### AMM program binary (required for swaps)
+### AMM program binary (required for swaps and liquidity)
To execute a swap, the app must submit a transaction against the **exact AMM
program you deployed** (its ELF determines the program id, and therefore every
diff --git a/apps/amm/config/networks.json b/apps/amm/config/networks.json
deleted file mode 100644
index 75e03eba..00000000
--- a/apps/amm/config/networks.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "testnet": {
- "checkpointHash": "0d25d71fca70d7008a892f6b3f768a4c66badbcd64e67d79ca595b92f1db544a",
- "ammProgramId": "77eeaa23668ad2675fb768cd7ecb1893387be464b9a51f16756006c1d307db07",
- "tokenDefinitionIds": [
- "7b464ff9dd0d3bc07f7e2e0b0667ccd066d85ad12be4c79fc55687a863910aa6",
- "48c81cf032e601ca367fc9816b957dbf5c0e4c11cf7008e8f4581ec1a67aab42",
- "159caef810ea545951b3bd913efe625ee45008c80865c330e72a72ed48b61649",
- "75f33110b185717209e3955f228d4a4448801d0ce8ba438a4a268050eeff3f44",
- "fbd107ca4bb66bc58f59ac2d32a759be3ee0fb453f8fecd1991c11837d9660c7",
- "5547fcb72644d95a385d313b887a96be41ff263bce6150b49fd87276839822bf",
- "fa43e74a97d79c5f907ff3edabda5ad89bfbd3b0922572e675d4ad3c7b6029c7",
- "4f3231d8a01e1d79f163bc27fce0c860a4a2f6890280e9d135eafbde0d68ed79",
- "fa32f354408857006f8ea396b0419823bd04436eadb2d273d2618a46b4793ed8",
- "00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb"
- ]
- }
-}
diff --git a/apps/amm/src/ActiveNetwork.cpp b/apps/amm/src/ActiveNetwork.cpp
deleted file mode 100644
index c92bc73b..00000000
--- a/apps/amm/src/ActiveNetwork.cpp
+++ /dev/null
@@ -1,141 +0,0 @@
-#include "ActiveNetwork.h"
-
-#include
-#include
-#include
-#include
-
-namespace {
- const char NETWORK_ENV[] = "AMM_UI_NETWORK";
- const char DEVNET_FILE_ENV[] = "AMM_UI_DEVNET_FILE";
-
- bool isLowerHex(const QString& value, int size)
- {
- if (value.size() != size)
- return false;
- for (const QChar character : value) {
- const bool isAsciiDigit = character >= QLatin1Char('0')
- && character <= QLatin1Char('9');
- if (!isAsciiDigit
- && (character < QLatin1Char('a') || character > QLatin1Char('f'))) {
- return false;
- }
- }
- return true;
- }
-}
-
-bool ActiveNetwork::load()
-{
- m_network = {};
- m_network.status = QStringLiteral("config_missing");
- m_expectedIdentity.clear();
-
- const QByteArray selected = qgetenv(NETWORK_ENV);
- m_network.id = selected.isEmpty()
- ? QStringLiteral("testnet")
- : QString::fromLocal8Bit(selected).trimmed();
-
- QJsonObject entry;
- if (isDevnet()) {
- const QString path = QString::fromLocal8Bit(qgetenv(DEVNET_FILE_ENV));
- QFile file(path);
- if (path.isEmpty() || !file.open(QIODevice::ReadOnly))
- return false;
- const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
- if (!document.isObject())
- return false;
- entry = document.object();
- m_expectedIdentity = entry.value(QStringLiteral("channelId")).toString();
- } else {
- QFile file(QStringLiteral(":/amm/config/networks.json"));
- if (!file.open(QIODevice::ReadOnly))
- return false;
- const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
- if (!document.isObject())
- return false;
- const QJsonObject networks = document.object();
- entry = networks.value(m_network.id).toObject();
- m_expectedIdentity = entry.value(QStringLiteral("checkpointHash")).toString();
- }
-
- m_network.ammProgramId = entry.value(QStringLiteral("ammProgramId")).toString();
- if (!isValidIdentity(m_expectedIdentity)
- || !isLowerHex(m_network.ammProgramId, 64)) {
- return false;
- }
-
- for (const QJsonValue& value : entry.value(QStringLiteral("tokenDefinitionIds")).toArray()) {
- const QString id = value.toString();
- if (!isLowerHex(id, 64)) {
- m_network.tokenIds.clear();
- return false;
- }
- m_network.tokenIds.append(id);
- }
- m_network.status = QStringLiteral("network_unknown");
- return true;
-}
-
-bool ActiveNetwork::isConfigured() const
-{
- return m_network.status != QStringLiteral("config_missing");
-}
-
-bool ActiveNetwork::isDevnet() const
-{
- return m_network.id == QStringLiteral("devnet");
-}
-
-bool ActiveNetwork::needsIdentityProbe() const
-{
- return m_network.status == QStringLiteral("loading")
- || m_network.status == QStringLiteral("network_unknown");
-}
-
-void ActiveNetwork::sequencerChanged(bool available)
-{
- if (isConfigured())
- clearIdentity(available ? QStringLiteral("loading") : QStringLiteral("network_unknown"));
-}
-
-void ActiveNetwork::reachabilityChanged(bool reachable, bool wasReachable)
-{
- if (!isConfigured())
- return;
- if (!reachable)
- clearIdentity(QStringLiteral("network_unknown"));
- else if (!wasReachable)
- clearIdentity(QStringLiteral("loading"));
-}
-
-void ActiveNetwork::beginIdentityProbe()
-{
- if (isConfigured())
- clearIdentity(QStringLiteral("loading"));
-}
-
-void ActiveNetwork::finishIdentityProbe(const QString& identity)
-{
- if (identity.isEmpty()) {
- clearIdentity(QStringLiteral("network_unknown"));
- } else if (identity != m_expectedIdentity) {
- clearIdentity(QStringLiteral("network_mismatch"));
- } else {
- m_network.status = QStringLiteral("ready");
- m_network.fingerprint = (isDevnet() ? QStringLiteral("channel:")
- : QStringLiteral("block10:"))
- + identity;
- }
-}
-
-bool ActiveNetwork::isValidIdentity(const QString& value)
-{
- return isLowerHex(value, 64);
-}
-
-void ActiveNetwork::clearIdentity(const QString& status)
-{
- m_network.status = status;
- m_network.fingerprint.clear();
-}
diff --git a/apps/amm/src/ActiveNetwork.h b/apps/amm/src/ActiveNetwork.h
index 9b1db9b7..771442b9 100644
--- a/apps/amm/src/ActiveNetwork.h
+++ b/apps/amm/src/ActiveNetwork.h
@@ -3,6 +3,11 @@
#include
#include
+// Network context handed to the new-position flow. The AMM deployment identity
+// (ammProgramId, from $AMM_PROGRAM_BIN) and the configured token set (tokenIds,
+// from $TOKENS_CONFIG) are the same sources the Swap view uses; there is no
+// separate network config file or channel-identity probe. `fingerprint` binds a
+// quote to the deployment so a quote can't be replayed against a different one.
struct ActiveNetworkSnapshot {
QString id;
QString status;
@@ -10,27 +15,3 @@ struct ActiveNetworkSnapshot {
QString ammProgramId;
QStringList tokenIds;
};
-
-class ActiveNetwork final {
-public:
- bool load();
-
- const QString& status() const { return m_network.status; }
- bool isConfigured() const;
- bool isDevnet() const;
- bool needsIdentityProbe() const;
- ActiveNetworkSnapshot snapshot() const { return m_network; }
-
- void sequencerChanged(bool available);
- void reachabilityChanged(bool reachable, bool wasReachable);
- void beginIdentityProbe();
- void finishIdentityProbe(const QString& identity);
-
- static bool isValidIdentity(const QString& value);
-
-private:
- void clearIdentity(const QString& status);
-
- ActiveNetworkSnapshot m_network;
- QString m_expectedIdentity;
-};
diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp
index 353c4a3a..0800f7d2 100644
--- a/apps/amm/src/AmmUiBackend.cpp
+++ b/apps/amm/src/AmmUiBackend.cpp
@@ -14,9 +14,6 @@
#include
#include
#include