diff --git a/apps/amm/README.md b/apps/amm/README.md index e1d8ff1..df5a6c2 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 diff --git a/apps/amm/qml/Main.qml b/apps/amm/qml/Main.qml index 1250bbc..c8619b1 100644 --- a/apps/amm/qml/Main.qml +++ b/apps/amm/qml/Main.qml @@ -89,5 +89,10 @@ Item { anchors.fill: parent visible: navbar.currentIndex === 1 } + + CreatePoolPage { + anchors.fill: parent + visible: navbar.currentIndex === 2 + } } } diff --git a/apps/amm/qml/NavBar.qml b/apps/amm/qml/NavBar.qml index f5b7857..ffea110 100644 --- a/apps/amm/qml/NavBar.qml +++ b/apps/amm/qml/NavBar.qml @@ -11,7 +11,7 @@ Item { id: root property int currentIndex: 0 - readonly property var tabs: ["Trade", "Liquidity"] + readonly property var tabs: ["Trade", "Liquidity", "Create Pool"] // Wallet wiring, passed down from Main.qml. property var backend: null diff --git a/apps/amm/qml/components/pool/PoolStepRail.qml b/apps/amm/qml/components/pool/PoolStepRail.qml new file mode 100644 index 0000000..a2ee12e --- /dev/null +++ b/apps/amm/qml/components/pool/PoolStepRail.qml @@ -0,0 +1,116 @@ +import QtQuick +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +// Vertical progress rail for the pool-creation flow (Uniswap-style): numbered +// steps connected by a line, with the active step highlighted and completed +// steps marked done. Read currentStep to drive which step is active. Clicking an +// already-reached step (index <= currentStep) emits stepClicked so the page can +// navigate back to it. +Item { + id: root + + property int currentStep: 0 + readonly property var steps: [ + { title: qsTr("Select token pair"), subtitle: qsTr("Pick the two tokens for the pool.") }, + { title: qsTr("Deposit amounts"), subtitle: qsTr("Set the initial liquidity.") } + ] + + signal stepClicked(int index) + + implicitWidth: 240 + implicitHeight: column.implicitHeight + + ColumnLayout { + id: column + anchors.fill: parent + spacing: 0 + + Repeater { + model: root.steps + + delegate: Item { + id: stepItem + + readonly property bool active: index === root.currentStep + readonly property bool done: index < root.currentStep + readonly property bool last: index === root.steps.length - 1 + // Only steps already reached can be clicked (no jumping ahead). + readonly property bool reachable: index <= root.currentStep + + Layout.fillWidth: true + implicitHeight: stepRow.implicitHeight + + RowLayout { + id: stepRow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Theme.spacing.medium + + // Indicator: numbered dot + connector line down to the next dot. + Item { + Layout.preferredWidth: 28 + Layout.fillHeight: true + + Rectangle { + id: dot + width: 28 + height: 28 + radius: 14 + color: (stepItem.active || stepItem.done) ? Theme.palette.primary : Theme.palette.backgroundSecondary + border.width: 1 + border.color: (stepItem.active || stepItem.done) ? Theme.palette.primary : Theme.palette.border + + LogosText { + anchors.centerIn: parent + text: stepItem.done ? "✓" : (index + 1) + font.pixelSize: Theme.typography.secondaryText + font.bold: true + color: (stepItem.active || stepItem.done) ? Theme.palette.background : Theme.palette.textSecondary + } + } + Rectangle { + visible: !stepItem.last + width: 2 + anchors.top: dot.bottom + anchors.bottom: parent.bottom + anchors.horizontalCenter: dot.horizontalCenter + color: stepItem.done ? Theme.palette.primary : Theme.palette.border + } + } + + // Step text. + ColumnLayout { + Layout.fillWidth: true + Layout.bottomMargin: stepItem.last ? 0 : Theme.spacing.xlarge + spacing: 2 + + LogosText { + text: modelData.title + font.pixelSize: Theme.typography.primaryText + font.bold: stepItem.active + color: (stepItem.active || stepItem.done) ? Theme.palette.text : Theme.palette.textSecondary + } + LogosText { + Layout.fillWidth: true + text: modelData.subtitle + wrapMode: Text.WordWrap + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + } + } + + MouseArea { + anchors.fill: parent + enabled: stepItem.reachable + cursorShape: stepItem.reachable ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: root.stepClicked(index) + } + } + } + } +} diff --git a/apps/amm/qml/pages/CreatePoolPage.qml b/apps/amm/qml/pages/CreatePoolPage.qml new file mode 100644 index 0000000..e4732b6 --- /dev/null +++ b/apps/amm/qml/pages/CreatePoolPage.qml @@ -0,0 +1,270 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +import "../components/pool" + +// Two-column pool-creation flow (Uniswap-style): a vertical step rail on the +// left and the active step's panel on the right. Step 1 selects the token pair; +// step 2 enters deposit amounts. Both panels stay alive so the entered values +// persist when navigating between steps via the rail. +Item { + id: root + + readonly property int pageMargin: 24 + // Breathing room below the navbar. The Trade page fully centers its card + // (gap = leftover space / 2); this uses a quarter of the leftover so it sits + // roughly half as far down, scaling with the window, with a sensible floor. + readonly property int topMargin: Math.max(48, Math.round((scroll.height - content.implicitHeight) / 4)) + readonly property int contentWidth: 760 + + // 0x123456…cdef style truncation for showing token addresses compactly. + function truncated(addr) { + const a = (addr || "").trim() + return a.length > 13 ? (a.substring(0, 6) + "…" + a.substring(a.length - 4)) : a + } + + // Numbers only (digits + a single decimal point) for the deposit amounts. + RegularExpressionValidator { + id: amountValidator + regularExpression: /^[0-9]*\.?[0-9]*$/ + } + + Rectangle { + anchors.fill: parent + color: Theme.palette.background + } + + Flickable { + id: scroll + anchors.fill: parent + clip: true + contentWidth: width + contentHeight: Math.max(height, content.implicitHeight + root.topMargin + root.pageMargin) + flickableDirection: Flickable.VerticalFlick + + RowLayout { + id: content + x: Math.max(root.pageMargin, (scroll.width - width) / 2) + y: root.topMargin + width: Math.min(scroll.width - root.pageMargin * 2, root.contentWidth) + spacing: Theme.spacing.xxlarge + + PoolStepRail { + id: rail + currentStep: 0 + Layout.preferredWidth: 240 + Layout.alignment: Qt.AlignTop + // Jump back to an already-reached step (e.g. step 1 to re-pick + // tokens). Selections persist because both panels stay alive. + onStepClicked: (index) => { rail.currentStep = index } + } + + // ── Right: active step's panel (both kept alive to preserve state) ── + Item { + id: rightPane + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + Layout.preferredHeight: rail.currentStep === 0 + ? selectCard.implicitHeight + : depositCard.implicitHeight + + // ── Step 1: select pair ────────────────────────────────── + Rectangle { + id: selectCard + width: parent.width + visible: rail.currentStep === 0 + implicitHeight: selectCol.implicitHeight + Theme.spacing.large * 2 + radius: Theme.spacing.radiusLarge + color: Theme.palette.backgroundSecondary + border.width: 1 + border.color: Theme.palette.borderSecondary + + ColumnLayout { + id: selectCol + anchors.fill: parent + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.medium + + LogosText { + text: qsTr("Select pair") + font.pixelSize: Theme.typography.panelTitleText + font.weight: Theme.typography.weightBold + color: Theme.palette.text + } + LogosText { + Layout.fillWidth: true + text: qsTr("Choose the two tokens for your pool by entering each token's address.") + wrapMode: Text.WordWrap + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + + LogosText { + Layout.topMargin: Theme.spacing.small + text: qsTr("Token A address") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosTextField { + id: tokenAField + Layout.fillWidth: true + placeholderText: "0x…" + } + + LogosText { + Layout.topMargin: Theme.spacing.small + text: qsTr("Token B address") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosTextField { + id: tokenBField + Layout.fillWidth: true + placeholderText: "0x…" + } + + LogosButton { + Layout.fillWidth: true + Layout.topMargin: Theme.spacing.medium + height: 44 + text: qsTr("Continue") + enabled: tokenAField.text.trim().length > 0 + && tokenBField.text.trim().length > 0 + && tokenAField.text.trim() !== tokenBField.text.trim() + onClicked: rail.currentStep = 1 + } + } + } + + // ── Step 2: deposit amounts ────────────────────────────── + Rectangle { + id: depositCard + width: parent.width + visible: rail.currentStep === 1 + implicitHeight: depositCol.implicitHeight + Theme.spacing.large * 2 + radius: Theme.spacing.radiusLarge + color: Theme.palette.backgroundSecondary + border.width: 1 + border.color: Theme.palette.borderSecondary + + ColumnLayout { + id: depositCol + anchors.fill: parent + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.medium + + LogosText { + text: qsTr("Deposit amounts") + font.pixelSize: Theme.typography.panelTitleText + font.weight: Theme.typography.weightBold + color: Theme.palette.text + } + LogosText { + Layout.fillWidth: true + text: qsTr("Set the initial liquidity for the pool.") + wrapMode: Text.WordWrap + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + + // Token pair carried over from step 1. + Rectangle { + Layout.fillWidth: true + Layout.topMargin: Theme.spacing.small + implicitHeight: pairCol.implicitHeight + Theme.spacing.medium * 2 + radius: Theme.spacing.radiusLarge + color: Theme.palette.backgroundTertiary + border.width: 1 + border.color: Theme.palette.borderSecondary + + ColumnLayout { + id: pairCol + anchors.fill: parent + anchors.margins: Theme.spacing.medium + spacing: Theme.spacing.small + + LogosText { + text: qsTr("Selected pair") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + RowLayout { + Layout.fillWidth: true + LogosText { + text: qsTr("Token A") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + Item { Layout.fillWidth: true } + LogosText { + text: root.truncated(tokenAField.text) + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.text + } + } + RowLayout { + Layout.fillWidth: true + LogosText { + text: qsTr("Token B") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + Item { Layout.fillWidth: true } + LogosText { + text: root.truncated(tokenBField.text) + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.text + } + } + } + } + + // Amount inputs, one per token. + LogosText { + Layout.topMargin: Theme.spacing.small + text: qsTr("Token A amount") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosTextField { + id: amountAField + Layout.fillWidth: true + placeholderText: "0.0" + Component.onCompleted: textInput.validator = amountValidator + } + + LogosText { + Layout.topMargin: Theme.spacing.small + text: qsTr("Token B amount") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosTextField { + id: amountBField + Layout.fillWidth: true + placeholderText: "0.0" + Component.onCompleted: textInput.validator = amountValidator + } + + LogosButton { + Layout.fillWidth: true + Layout.topMargin: Theme.spacing.medium + height: 44 + text: qsTr("Create pool") + enabled: parseFloat(amountAField.text) > 0 + && parseFloat(amountBField.text) > 0 + // Wiring to the AMM new_definition instruction is a follow-up. + onClicked: console.log("create pool", + tokenAField.text, amountAField.text, + tokenBField.text, amountBField.text) + } + } + } + } + } + } +}