mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(apps/amm): drive the Pools list from AMM_POOLS_CONFIG
The Pools page shipped with a hardcoded four-pair sample. Replace it with a config-driven "known pools" list, mirroring how the Swap token picker reads TOKENS_CONFIG: the app loads a flat JSON array from the AMM_POOLS_CONFIG environment variable and renders one row per entry. Adding pairs is a config edit — no app change. Pool discovery is an app concern, so the config is read in the backend (AmmUiBackend::poolList, Qt JSON) rather than the amm_module — the module is shedding app-specific view surface (tokenList/newPositionContext), so pools go where tokens are heading, not where they are today. poolList() fails soft to an empty list when AMM_POOLS_CONFIG is unset/unreadable/not an array, and skips individual entries missing tokenA/tokenB/a numeric feeBps. Each entry carries the display symbols (tokenA/tokenB), feeBps, and the on-chain identifiers (poolId, tokenADefinitionId, tokenBDefinitionId) so a row can later be resolved against chain state. PoolsPage takes injected backend/runtime and loads via runtime.watch(backend.poolList()); the Repeater renders entries generically. The AMM testnet setup script now emits amm-pools.json from a POOL_SPECS array (one line per seeded pool, currently the seeded TKA/TKB pool) and prints AMM_POOLS_CONFIG in the launch instructions. Adds amm-pools.json.example, a README section, and gitignores the runtime config files.
This commit is contained in:
committed by
r4bbit
parent
c47f387ad4
commit
4cfc03a815
@@ -6,6 +6,9 @@ modules/
|
||||
# your own definitionId/holding accounts. Never commit your real accounts.
|
||||
amm-tokens.json
|
||||
|
||||
# Personal known-pools config (AMM_POOLS_CONFIG). Same rationale as above.
|
||||
amm-pools.json
|
||||
|
||||
# Screenshot artifacts written by tests/swap.mjs
|
||||
tests/*.png
|
||||
|
||||
@@ -14,3 +17,6 @@ tests/testnet/.wallet/
|
||||
|
||||
# Isolated token config written by tests/testnet/setup-amm-testnet.sh
|
||||
tests/testnet/amm-tokens.json
|
||||
|
||||
# Isolated known-pools config written by tests/testnet/setup-amm-testnet.sh
|
||||
tests/testnet/amm-pools.json
|
||||
|
||||
+34
-1
@@ -211,11 +211,44 @@ picker stays empty (a `qWarning` naming the exact cause is logged to stderr; no
|
||||
swap can be started). `definitionId`/`holding` may be given as base58 (as the
|
||||
wallet/runbook display them) or hex — the app normalizes both to hex.
|
||||
|
||||
Full command with both variables set (absolute paths, from the repo root):
|
||||
### Known-pools config (optional, for the Pools list)
|
||||
|
||||
The Pools view is config-driven the same way: it reads a flat JSON list from the
|
||||
`AMM_POOLS_CONFIG` environment variable (absolute path) and renders one row per
|
||||
entry. `tokenA`/`tokenB` are the display symbols and `feeBps` the fee tier;
|
||||
`poolId`/`tokenADefinitionId`/`tokenBDefinitionId` identify the pool on-chain.
|
||||
Adding more pairs is purely a config edit — no app change:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"tokenA": "TKA",
|
||||
"tokenB": "TKB",
|
||||
"feeBps": 1,
|
||||
"poolId": "9qbX…",
|
||||
"tokenADefinitionId": "4T69…",
|
||||
"tokenBDefinitionId": "7Zc2…"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Copy the checked-in template to start (`amm-pools.json` is git-ignored):
|
||||
|
||||
```bash
|
||||
cp apps/amm/amm-pools.json.example apps/amm/amm-pools.json # then replace the REPLACE_… placeholders
|
||||
```
|
||||
|
||||
If `AMM_POOLS_CONFIG` is unset, unreadable, or not a valid JSON array, the Pools
|
||||
list shows its empty state. Entries missing `tokenA`, `tokenB`, or a numeric
|
||||
`feeBps` are skipped individually. The AMM testnet setup script writes this file
|
||||
for the pool(s) it seeds (see below).
|
||||
|
||||
Full command with the variables set (absolute paths, from the repo root):
|
||||
|
||||
```bash
|
||||
AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \
|
||||
TOKENS_CONFIG=$(pwd)/apps/amm/amm-tokens.json \
|
||||
AMM_POOLS_CONFIG=$(pwd)/apps/amm/amm-pools.json \
|
||||
nix run .#amm-ui
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ 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
|
||||
"$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/pages/PoolsPage.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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"tokenA": "TKA",
|
||||
"tokenB": "TKB",
|
||||
"feeBps": 1,
|
||||
"poolId": "REPLACE_WITH_POOL_PDA",
|
||||
"tokenADefinitionId": "REPLACE_WITH_TOKEN_A_DEFINITION_ID",
|
||||
"tokenBDefinitionId": "REPLACE_WITH_TOKEN_B_DEFINITION_ID"
|
||||
}
|
||||
]
|
||||
@@ -92,5 +92,12 @@ Item {
|
||||
runtime: logos
|
||||
visible: navbar.currentIndex === 1
|
||||
}
|
||||
|
||||
PoolsPage {
|
||||
anchors.fill: parent
|
||||
backend: root.ready ? root.backend : null
|
||||
runtime: logos
|
||||
visible: navbar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-7
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
@@ -10,7 +12,7 @@ Item {
|
||||
id: root
|
||||
|
||||
property int currentIndex: 0
|
||||
readonly property var tabs: ["Trade", "Liquidity"]
|
||||
readonly property var tabs: [qsTr("Trade"), qsTr("Liquidity"), qsTr("Pools")]
|
||||
|
||||
// Wallet wiring, passed down from Main.qml.
|
||||
property var backend: null
|
||||
@@ -44,7 +46,7 @@ Item {
|
||||
|
||||
// App identity
|
||||
Text {
|
||||
text: "Logos AMM"
|
||||
text: qsTr("Logos AMM")
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: 17
|
||||
font.weight: Font.Bold
|
||||
@@ -54,28 +56,61 @@ Item {
|
||||
|
||||
// Tab pills
|
||||
Row {
|
||||
Accessible.role: Accessible.PageTabList
|
||||
Accessible.name: qsTr("Primary navigation")
|
||||
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: root.tabs
|
||||
|
||||
delegate: Rectangle {
|
||||
id: tabButton
|
||||
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
readonly property bool active: root.currentIndex === index
|
||||
|
||||
height: 36
|
||||
width: tabLabel.implicitWidth + 28
|
||||
radius: 18
|
||||
color: active ? Theme.palette.backgroundSecondary : "transparent"
|
||||
border.width: activeFocus ? 1 : 0
|
||||
border.color: Theme.palette.text
|
||||
activeFocusOnTab: true
|
||||
Accessible.role: Accessible.PageTab
|
||||
Accessible.name: tabLabel.text
|
||||
|
||||
function activate() {
|
||||
root.currentIndex = index
|
||||
root.tabChanged(index)
|
||||
}
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Keys.onReturnPressed: function(event) {
|
||||
tabButton.activate()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: function(event) {
|
||||
tabButton.activate()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Keys.onSpacePressed: function(event) {
|
||||
tabButton.activate()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Text {
|
||||
id: tabLabel
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: active ? Theme.palette.text : Theme.palette.textSecondary
|
||||
text: tabButton.modelData
|
||||
color: tabButton.active ? Theme.palette.text : Theme.palette.textSecondary
|
||||
font.pixelSize: 14
|
||||
font.weight: active ? Font.Medium : Font.Normal
|
||||
font.weight: tabButton.active ? Font.Medium : Font.Normal
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
@@ -84,8 +119,8 @@ Item {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentIndex = index
|
||||
root.tabChanged(index)
|
||||
tabButton.forceActiveFocus()
|
||||
tabButton.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "../components/liquidity"
|
||||
import "../components/swap/TokenVisuals.js" as TokenVisuals
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Real backend replica (logos.module("amm_ui")) and the watch runtime,
|
||||
// wired from Main.qml. Null until the app is ready.
|
||||
property var backend: null
|
||||
property var runtime: null
|
||||
|
||||
readonly property int pageMargin: width < 640 ? 16 : 24
|
||||
readonly property int contentMaxWidth: 760
|
||||
readonly property int poolCount: root.pools ? root.pools.length : 0
|
||||
readonly property bool showEmptyState: root.poolCount === 0
|
||||
|
||||
// Config-driven known pools, loaded from AmmUiBackend::poolList() (which
|
||||
// reads the AMM_POOLS_CONFIG JSON file). Each entry is rendered generically,
|
||||
// so adding pairs to the config needs no change here. Empty until the
|
||||
// backend is ready and the call resolves.
|
||||
property var pools: []
|
||||
|
||||
function loadPools() {
|
||||
if (!root.backend || !root.runtime)
|
||||
return
|
||||
root.runtime.watch(root.backend.poolList(),
|
||||
function(list) { root.pools = list },
|
||||
function(err) { console.warn("poolList error:", err) })
|
||||
}
|
||||
|
||||
onBackendChanged: root.loadPools()
|
||||
onRuntimeChanged: root.loadPools()
|
||||
|
||||
AmmTheme {
|
||||
id: theme
|
||||
}
|
||||
|
||||
function feeLabel(feeBps) {
|
||||
var percentage = Number(feeBps) / 100
|
||||
return qsTr("%1%").arg(percentage.toLocaleString(Qt.locale(), "f", 2))
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: theme.colors.background
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: scroll
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: Math.max(height, pageContent.y + pageContent.implicitHeight
|
||||
+ root.pageMargin)
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
Item {
|
||||
id: pageContent
|
||||
|
||||
x: Math.max(root.pageMargin, (scroll.width - width) / 2)
|
||||
y: root.width < 640 ? 24 : 40
|
||||
width: Math.max(0, Math.min(root.contentMaxWidth,
|
||||
scroll.width - root.pageMargin * 2))
|
||||
implicitHeight: contentColumn.implicitHeight
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
width: parent.width
|
||||
spacing: 24
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: qsTr("Pools")
|
||||
color: theme.colors.textPrimary
|
||||
font.pixelSize: 30
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Pools")
|
||||
color: theme.colors.textSecondary
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: poolsList
|
||||
|
||||
objectName: "poolsList"
|
||||
width: parent.width
|
||||
implicitHeight: !root.showEmptyState
|
||||
? listContent.implicitHeight : 144
|
||||
color: theme.colors.cardBg
|
||||
radius: 16
|
||||
border.color: theme.colors.border
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: listContent
|
||||
|
||||
width: parent.width
|
||||
visible: !root.showEmptyState
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Pair")
|
||||
color: theme.colors.textSecondary
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Fee")
|
||||
color: theme.colors.textSecondary
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: theme.colors.divider
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.pools || []
|
||||
|
||||
delegate: PoolRow {
|
||||
width: listContent.width
|
||||
showDivider: index < root.poolCount - 1
|
||||
objectName: "poolRow%1".arg(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: emptyState
|
||||
|
||||
objectName: "poolsListEmptyState"
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 40
|
||||
visible: root.showEmptyState
|
||||
text: qsTr("No pools configured.")
|
||||
color: theme.colors.textSecondary
|
||||
font.pixelSize: 14
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component PoolRow: Item {
|
||||
id: row
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
property bool showDivider: false
|
||||
readonly property var pool: modelData
|
||||
readonly property string pairText: qsTr("%1 / %2")
|
||||
.arg(String(pool.tokenA || ""))
|
||||
.arg(String(pool.tokenB || ""))
|
||||
readonly property string feeText: root.feeLabel(pool.feeBps)
|
||||
|
||||
height: 68
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 20
|
||||
anchors.rightMargin: 20
|
||||
spacing: 12
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 46
|
||||
Layout.preferredHeight: 30
|
||||
Accessible.ignored: true
|
||||
|
||||
TokenAvatar {
|
||||
x: 0
|
||||
y: 1
|
||||
symbol: String(row.pool.tokenA || "")
|
||||
z: 1
|
||||
}
|
||||
|
||||
TokenAvatar {
|
||||
x: 18
|
||||
y: 1
|
||||
symbol: String(row.pool.tokenB || "")
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: row.pairText
|
||||
color: theme.colors.textPrimary
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: feeText.implicitWidth + 20
|
||||
Layout.preferredHeight: 30
|
||||
radius: 6
|
||||
color: theme.colors.inputBg
|
||||
border.color: theme.colors.borderStrong
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: feeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: row.feeText
|
||||
color: theme.colors.textPrimary
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: row.showDivider
|
||||
height: 1
|
||||
color: theme.colors.divider
|
||||
}
|
||||
}
|
||||
|
||||
component TokenAvatar: Rectangle {
|
||||
id: avatar
|
||||
|
||||
required property string symbol
|
||||
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: TokenVisuals.colorFor(symbol)
|
||||
border.color: theme.colors.cardBg
|
||||
border.width: 2
|
||||
Accessible.ignored: true
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: TokenVisuals.letterFor(avatar.symbol)
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Bold
|
||||
Accessible.ignored: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "AmmUiBackend.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "LogosWalletProvider.h"
|
||||
@@ -9,6 +13,59 @@
|
||||
#include "logos_sdk.h"
|
||||
|
||||
namespace {
|
||||
// Absolute path to the JSON known-pools config consumed by poolList().
|
||||
// Mirrors TOKENS_CONFIG for the token list; produced by the AMM testnet
|
||||
// setup script (apps/amm/tests/testnet/setup-amm-testnet.sh).
|
||||
constexpr char POOLS_CONFIG_ENV[] = "AMM_POOLS_CONFIG";
|
||||
|
||||
// Parses the AMM_POOLS_CONFIG JSON file into the QVariantList the Pools UI
|
||||
// renders. Fails soft (empty list) when the env var is unset, the file is
|
||||
// unreadable, or the payload is not a JSON array — one malformed entry is
|
||||
// skipped rather than dropping the whole list. tokenA/tokenB (display
|
||||
// symbols) and a numeric feeBps are required; the id fields pass through
|
||||
// when present so the entry can later be resolved on-chain.
|
||||
QVariantList readPoolsConfig()
|
||||
{
|
||||
QVariantList out;
|
||||
|
||||
const QString path = qEnvironmentVariable(POOLS_CONFIG_ENV);
|
||||
if (path.isEmpty())
|
||||
return out;
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return out;
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (!doc.isArray())
|
||||
return out;
|
||||
|
||||
for (const QJsonValue& entry : doc.array()) {
|
||||
if (!entry.isObject())
|
||||
continue;
|
||||
const QJsonObject obj = entry.toObject();
|
||||
|
||||
const QString tokenA = obj.value(QStringLiteral("tokenA")).toString();
|
||||
const QString tokenB = obj.value(QStringLiteral("tokenB")).toString();
|
||||
const QJsonValue feeBps = obj.value(QStringLiteral("feeBps"));
|
||||
if (tokenA.isEmpty() || tokenB.isEmpty() || !feeBps.isDouble())
|
||||
continue;
|
||||
|
||||
QVariantMap pool;
|
||||
pool.insert(QStringLiteral("tokenA"), tokenA);
|
||||
pool.insert(QStringLiteral("tokenB"), tokenB);
|
||||
pool.insert(QStringLiteral("feeBps"), feeBps.toInt());
|
||||
pool.insert(QStringLiteral("poolId"),
|
||||
obj.value(QStringLiteral("poolId")).toString());
|
||||
pool.insert(QStringLiteral("tokenADefinitionId"),
|
||||
obj.value(QStringLiteral("tokenADefinitionId")).toString());
|
||||
pool.insert(QStringLiteral("tokenBDefinitionId"),
|
||||
obj.value(QStringLiteral("tokenBDefinitionId")).toString());
|
||||
out.append(pool);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The new-position context placeholder published before the module
|
||||
// connection is up (matches the module's "loading" contextState).
|
||||
QVariantMap loadingContext()
|
||||
@@ -249,6 +306,15 @@ QVariantList AmmUiBackend::tokenHoldings()
|
||||
return m_logos->amm_module.tokenHoldings(isWalletOpen());
|
||||
}
|
||||
|
||||
QVariantList AmmUiBackend::poolList()
|
||||
{
|
||||
// Config-driven known pools. Read straight from AMM_POOLS_CONFIG on every
|
||||
// call (the UI fetches this once on load); adding more pairs is a config
|
||||
// edit, no app change. Pool discovery is an app concern, so this stays in
|
||||
// the backend rather than the amm_module.
|
||||
return readPoolsConfig();
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::createPool(QVariantMap request)
|
||||
{
|
||||
// Same connected-state submit guard as the swaps — this app's lock is
|
||||
|
||||
@@ -82,6 +82,9 @@ public slots:
|
||||
QVariantMap addLiquidity(QVariantMap request) override;
|
||||
// Lists the wallet's fungible token holdings for the account selector.
|
||||
QVariantList tokenHoldings() override;
|
||||
// Reads the known-pools list from AMM_POOLS_CONFIG (app config JSON, read
|
||||
// here rather than in the amm_module — pool discovery is an app detail).
|
||||
QVariantList poolList() override;
|
||||
|
||||
private:
|
||||
void syncWalletState();
|
||||
|
||||
@@ -137,4 +137,11 @@ class AmmUiBackend
|
||||
// definitionIdHex, balanceRaw }] — one row per holding account, every token,
|
||||
// including zero-balance holdings. The selector narrows to a token by id.
|
||||
SLOT(QVariantList tokenHoldings())
|
||||
// Reads the known-pools config at AMM_POOLS_CONFIG (absolute path, JSON array
|
||||
// of { tokenA, tokenB, feeBps, poolId, tokenADefinitionId, tokenBDefinitionId })
|
||||
// and returns it as a QVariantList of QVariantMap entries. tokenA/tokenB are
|
||||
// display symbols; the id fields identify the pool on-chain. Returns an empty
|
||||
// list if AMM_POOLS_CONFIG is unset/unreadable/invalid. This is app config, not
|
||||
// an on-chain read, so it lives in the backend rather than the amm_module.
|
||||
SLOT(QVariantList poolList())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtTest
|
||||
|
||||
import "../../qml/pages" as Pages
|
||||
|
||||
TestCase {
|
||||
id: testCase
|
||||
|
||||
name: "PoolsPage"
|
||||
|
||||
Component {
|
||||
id: backendComponent
|
||||
|
||||
QtObject {
|
||||
property var poolListResult: [
|
||||
{ "tokenA": "TKA", "tokenB": "TKB", "feeBps": 5 },
|
||||
{ "tokenA": "TKC", "tokenB": "TKA", "feeBps": 30 }
|
||||
]
|
||||
|
||||
function poolList() {
|
||||
return poolListResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: runtimeComponent
|
||||
|
||||
QtObject {
|
||||
function watch(value, succeeded, failed) {
|
||||
if (value === undefined)
|
||||
return
|
||||
succeeded(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pageComponent
|
||||
|
||||
Pages.PoolsPage {
|
||||
visible: false
|
||||
width: 800
|
||||
height: 600
|
||||
}
|
||||
}
|
||||
|
||||
function test_rowsAreDrivenByBackendPoolList() {
|
||||
var backend = createTemporaryObject(backendComponent, testCase)
|
||||
var runtime = createTemporaryObject(runtimeComponent, testCase)
|
||||
var page = createTemporaryObject(pageComponent, testCase, {
|
||||
"backend": backend,
|
||||
"runtime": runtime
|
||||
})
|
||||
verify(backend)
|
||||
verify(runtime)
|
||||
verify(page)
|
||||
|
||||
// The config drives the row count — no pair is hardcoded in the page,
|
||||
// so adding entries to poolList() is all it takes to render more rows.
|
||||
compare(page.poolCount, 2)
|
||||
verify(page.feeLabel(5).endsWith("%"))
|
||||
|
||||
var list = findChild(page, "poolsList")
|
||||
var firstRow = findChild(page, "poolRow0")
|
||||
verify(list)
|
||||
verify(firstRow)
|
||||
compare(firstRow.pairText, "TKA / TKB")
|
||||
compare(firstRow.feeText, page.feeLabel(5))
|
||||
}
|
||||
|
||||
function test_emptyPoolModelUsesTheEmptyState() {
|
||||
var page = createTemporaryObject(pageComponent, testCase)
|
||||
verify(page)
|
||||
|
||||
// No backend wired: pools defaults to [] and the empty state shows.
|
||||
compare(page.poolCount, 0)
|
||||
compare(page.showEmptyState, true)
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,11 @@ POOL_DEADLINE="18446744073709551615"
|
||||
# TOKENS_CONFIG when launching the UI for a test run.
|
||||
TOKENS_CONFIG_OUT="apps/amm/tests/testnet/amm-tokens.json"
|
||||
|
||||
# Where the UI known-pools config is written (git-ignored, tests only). Pass this
|
||||
# path as AMM_POOLS_CONFIG when launching the UI; the Pools page renders one row
|
||||
# per entry. More seeded pools = more entries here, no app change.
|
||||
POOLS_CONFIG_OUT="apps/amm/tests/testnet/amm-pools.json"
|
||||
|
||||
###############################################################################
|
||||
# Helpers
|
||||
###############################################################################
|
||||
@@ -423,6 +428,42 @@ cat > "$TOKENS_CONFIG_OUT" <<JSON
|
||||
JSON
|
||||
kv "wrote" "$TOKENS_CONFIG_OUT"
|
||||
|
||||
###############################################################################
|
||||
# 11. Write the UI known-pools config from the seeded pool(s)
|
||||
###############################################################################
|
||||
sec "Write UI pools config -> $POOLS_CONFIG_OUT"
|
||||
# One row per seeded pool: "SYMBOL_A SYMBOL_B FEE_BPS POOL_ID DEF_A DEF_B".
|
||||
# Add a line here for each new seeded pool — nothing else (script or app) needs
|
||||
# to change; the Pools page renders one row per entry generically.
|
||||
POOL_SPECS=(
|
||||
"$TOKEN_A_SYMBOL $TOKEN_B_SYMBOL $POOL_FEES $POOL $TOKEN_A_DEF $TOKEN_B_DEF"
|
||||
)
|
||||
|
||||
pool_entry() {
|
||||
cat <<JSON
|
||||
{
|
||||
"tokenA": "$1",
|
||||
"tokenB": "$2",
|
||||
"feeBps": $3,
|
||||
"poolId": "$4",
|
||||
"tokenADefinitionId": "$5",
|
||||
"tokenBDefinitionId": "$6"
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
{
|
||||
echo "["
|
||||
for i in "${!POOL_SPECS[@]}"; do
|
||||
[ "$i" -gt 0 ] && echo " ,"
|
||||
# shellcheck disable=SC2086 # deliberate word-split of the spec into fields
|
||||
set -- ${POOL_SPECS[$i]}
|
||||
pool_entry "$1" "$2" "$3" "$4" "$5" "$6"
|
||||
done
|
||||
echo "]"
|
||||
} > "$POOLS_CONFIG_OUT"
|
||||
kv "wrote" "$POOLS_CONFIG_OUT"
|
||||
|
||||
sec "Done"
|
||||
log "${GRN}✅ Setup complete.${RST}"
|
||||
kv "AMM program id" "$AMM_PID"
|
||||
@@ -433,6 +474,7 @@ log "Launch the UI against the ISOLATED test wallet + test token config:"
|
||||
log " ${DIM}LEE_WALLET_HOME_DIR=$TEST_WALLET_HOME \\${RST}"
|
||||
log " ${DIM} AMM_PROGRAM_BIN=$REPO_ROOT/$AMM_BIN \\${RST}"
|
||||
log " ${DIM} TOKENS_CONFIG=$REPO_ROOT/$TOKENS_CONFIG_OUT \\${RST}"
|
||||
log " ${DIM} AMM_POOLS_CONFIG=$REPO_ROOT/$POOLS_CONFIG_OUT \\${RST}"
|
||||
log " ${DIM} nix run .#amm-ui${RST}"
|
||||
log "Then in another terminal: ${DIM}node apps/amm/tests/swap.mjs${RST} (swap A/B)"
|
||||
log " or: ${DIM}node apps/amm/tests/create-pool.mjs${RST} (create A/C pool)"
|
||||
|
||||
Reference in New Issue
Block a user