mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
feat(wallet): humanize shared wallet experience
Consolidate wallet account decoding and portfolio handling around the shared Rust IDL decoder. Simplify AMM wallet integration, remove obsolete caches and network plumbing, and cover account selection and live flows.
This commit is contained in:
@@ -20,6 +20,28 @@ set(LOGOS_WALLET_GENERATED_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated_code"
|
||||
CACHE PATH "Path to generated Logos SDK sources"
|
||||
)
|
||||
# LogosModule.cmake stages external libraries after this shared target is
|
||||
# configured. Resolve the decoder from the staged dependency before adding the
|
||||
# shared wallet module so its static target can link successfully.
|
||||
if(NOT LOGOS_WALLET_IDL_DECODER_LIBRARY)
|
||||
set(amm_wallet_decoder_search_path "${CMAKE_CURRENT_SOURCE_DIR}/lib")
|
||||
if(DEFINED ENV{LOGOS_EXT_ROOT_WALLET_IDL_DECODER})
|
||||
set(amm_wallet_decoder_search_path
|
||||
"$ENV{LOGOS_EXT_ROOT_WALLET_IDL_DECODER}/lib")
|
||||
endif()
|
||||
find_library(AMM_WALLET_IDL_DECODER_LIBRARY
|
||||
NAMES wallet_idl_decoder
|
||||
PATHS "${amm_wallet_decoder_search_path}"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
if(AMM_WALLET_IDL_DECODER_LIBRARY)
|
||||
set(LOGOS_WALLET_IDL_DECODER_LIBRARY
|
||||
"${AMM_WALLET_IDL_DECODER_LIBRARY}"
|
||||
CACHE FILEPATH
|
||||
"wallet_idl_decoder library required by logos_wallet_access"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
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 +
|
||||
@@ -42,4 +64,34 @@ logos_module(
|
||||
Qt6::Gui
|
||||
LINK_TARGETS
|
||||
logos_wallet_access
|
||||
EXTERNAL_LIBS
|
||||
wallet_idl_decoder
|
||||
)
|
||||
|
||||
set(LEZ_IDL_ARTIFACTS_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../artifacts"
|
||||
CACHE PATH "Path to committed LEZ program IDL artifacts"
|
||||
)
|
||||
set(AMM_TOKEN_IDL "${LEZ_IDL_ARTIFACTS_DIR}/token-idl.json")
|
||||
set(AMM_IDL "${LEZ_IDL_ARTIFACTS_DIR}/amm-idl.json")
|
||||
|
||||
foreach(idl_file IN ITEMS "${AMM_TOKEN_IDL}" "${AMM_IDL}")
|
||||
if(NOT EXISTS "${idl_file}")
|
||||
message(FATAL_ERROR "Committed IDL artifact not found: ${idl_file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set_source_files_properties(
|
||||
"${AMM_TOKEN_IDL}"
|
||||
PROPERTIES QT_RESOURCE_ALIAS "idl/token-idl.json"
|
||||
)
|
||||
set_source_files_properties(
|
||||
"${AMM_IDL}"
|
||||
PROPERTIES QT_RESOURCE_ALIAS "idl/amm-idl.json"
|
||||
)
|
||||
qt_add_resources(amm_ui_module_plugin amm_ui_wallet_data
|
||||
PREFIX "/amm"
|
||||
FILES
|
||||
"${AMM_TOKEN_IDL}"
|
||||
"${AMM_IDL}"
|
||||
)
|
||||
|
||||
+25
-8
@@ -152,6 +152,23 @@ nix run .#amm-ui
|
||||
Without `AMM_PROGRAM_BIN` the Swap and Liquidity views stay disabled; without
|
||||
`TOKENS_CONFIG` the token picker is empty. Each is detailed below.
|
||||
|
||||
### Network identity
|
||||
|
||||
The shared wallet verifies the sequencer before it enables network-dependent
|
||||
portfolio data or AMM quotes. Testnet uses the bundled checkpoint identity. For
|
||||
devnet, provide the channel identity emitted by the local sequencer:
|
||||
|
||||
```bash
|
||||
LOGOS_WALLET_NETWORK=devnet \
|
||||
LOGOS_WALLET_DEVNET_FILE=/abs/path/to/devnet.json \
|
||||
nix run .#amm-ui
|
||||
```
|
||||
|
||||
`devnet.json` must contain a 64-character lowercase-hex `channelId`. The
|
||||
legacy `AMM_UI_NETWORK` and `AMM_UI_DEVNET_FILE` names remain accepted. AMM
|
||||
deployment and token selection stay app-specific through `AMM_PROGRAM_BIN` and
|
||||
`TOKENS_CONFIG`.
|
||||
|
||||
### AMM program binary (required for swaps and liquidity)
|
||||
|
||||
To execute a swap, the app must submit a transaction against the **exact AMM
|
||||
@@ -291,7 +308,7 @@ New Position validation commands and acceptance criteria live in
|
||||
|
||||
## Running the UI tests
|
||||
|
||||
The UI tests live in `apps/amm/tests/` (e.g. `swap.mjs`). They drive the running
|
||||
The live-chain UI tests live in `apps/amm/tests/e2e/`. They drive the running
|
||||
app through a QML inspector: each test connects to the inspector's TCP server,
|
||||
finds elements, clicks them, and asserts on the resulting state. `swap.mjs`
|
||||
selects two tokens, enters a sell amount, submits a swap end-to-end, and then
|
||||
@@ -309,8 +326,7 @@ import from `test-framework/framework.mjs` — comes from the
|
||||
It isn't vendored here; the `nix build .#test-framework` step below materializes
|
||||
it (Nix resolves it via this app's flake inputs, pinned in `flake.lock`).
|
||||
|
||||
Run everything **from the repository root** (the `apps/amm` flake can't resolve
|
||||
`amm_client_ffi` on its own).
|
||||
Run everything **from the repository root**.
|
||||
|
||||
**Prerequisites** for the swap test to complete:
|
||||
|
||||
@@ -324,23 +340,23 @@ Run everything **from the repository root** (the `apps/amm` flake can't resolve
|
||||
|
||||
```bash
|
||||
# 1. Build the JS test framework once. The -o path is where the tests expect it
|
||||
# (apps/amm/tests/swap.mjs imports ../result-mcp); or set LOGOS_QT_MCP instead.
|
||||
# (apps/amm/tests/e2e/swap.mjs imports ../../result-mcp); or set LOGOS_QT_MCP.
|
||||
nix build .#test-framework -o apps/amm/result-mcp
|
||||
|
||||
# 2. Terminal 1 — launch the AMM UI with a real, visible window. The inspector
|
||||
# listens on localhost:3768. Absolute paths ($(pwd)/…) because nix run may
|
||||
# not preserve the working directory.
|
||||
AMM_DEBUG=1 \
|
||||
AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \
|
||||
AMM_PROGRAM_BIN=$(pwd)/target/guest/amm.bin \
|
||||
TOKENS_CONFIG=$(pwd)/apps/amm/amm-tokens.json \
|
||||
nix run .#amm-ui
|
||||
|
||||
# 3. Terminal 2 — run a test against the running app; watch it drive the UI.
|
||||
node apps/amm/tests/swap.mjs
|
||||
node apps/amm/tests/e2e/swap.mjs
|
||||
```
|
||||
|
||||
On failure the test prints the relevant `SwapCard` state and saves screenshot
|
||||
PNGs next to the test (`apps/amm/tests/swap-*.png`, git-ignored) for inspection.
|
||||
PNGs next to the test (`apps/amm/tests/e2e/swap-*.png`, git-ignored) for inspection.
|
||||
|
||||
**Headless CI variant** (no window, launches the app itself, pass/fail only):
|
||||
|
||||
@@ -348,7 +364,8 @@ PNGs next to the test (`apps/amm/tests/swap-*.png`, git-ignored) for inspection.
|
||||
nix build .#integration-test -L
|
||||
```
|
||||
|
||||
It runs every `*.mjs` under `apps/amm/tests/` with `QT_QPA_PLATFORM=offscreen`.
|
||||
It runs the hermetic UI smoke test with `QT_QPA_PLATFORM=offscreen`. The
|
||||
live-chain tests remain in `apps/amm/tests/e2e/` and use the isolated setup above.
|
||||
|
||||
## Updating Dependencies
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Run from the repository root:
|
||||
cargo +1.94.0 test -p amm_ffi
|
||||
cargo +1.94.0 clippy -p amm_ffi --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)
|
||||
amm_qml=$(nix build .#amm-ui --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"
|
||||
|
||||
Generated
-26869
File diff suppressed because it is too large
Load Diff
@@ -1,63 +0,0 @@
|
||||
{
|
||||
description = "Logos AMM QML UI — trade and provide liquidity on the LEZ AMM";
|
||||
|
||||
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 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 repo-root flake.nix
|
||||
# builds the UI directly (src = ./apps/amm). The UI links no external lib of
|
||||
# its own — the AMM logic lives in the amm_ffi crate, which the amm_module
|
||||
# core module links; the UI reaches it via modules().amm_module (declared in
|
||||
# metadata.json `dependencies`). The repo-root flake exposes the UI as a named
|
||||
# attribute (there is no bare `default`): run it with `nix run .#amm-ui`, and
|
||||
# build the AMM logic crate with `nix build .#amm_ffi`.
|
||||
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}")
|
||||
'';
|
||||
externalLibInputs = { };
|
||||
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"
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
"build": ["pkg-config"],
|
||||
"runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"]
|
||||
},
|
||||
"external_libraries": [],
|
||||
"external_libraries": [
|
||||
{ "name": "wallet_idl_decoder" }
|
||||
],
|
||||
"cmake": {
|
||||
"find_packages": [],
|
||||
"extra_sources": [],
|
||||
|
||||
+198
-14
@@ -1,5 +1,7 @@
|
||||
#include "AmmUiBackend.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
@@ -12,13 +14,34 @@
|
||||
#include <QJsonValue>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "SequencerIdentityProbe.h"
|
||||
#include "SequencerNetworkSettings.h"
|
||||
#include "WalletController.h"
|
||||
#include "WalletPortfolioService.h"
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
namespace {
|
||||
constexpr char WALLET_NETWORK_ENV[] = "LOGOS_WALLET_NETWORK";
|
||||
constexpr char LEGACY_NETWORK_ENV[] = "AMM_UI_NETWORK";
|
||||
constexpr char WALLET_DEVNET_FILE_ENV[] = "LOGOS_WALLET_DEVNET_FILE";
|
||||
constexpr char LEGACY_DEVNET_FILE_ENV[] = "AMM_UI_DEVNET_FILE";
|
||||
|
||||
QByteArray resource(const QString& path)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
QString environmentValue(const char* primary, const char* fallback)
|
||||
{
|
||||
const QByteArray value = qgetenv(primary);
|
||||
return QString::fromLocal8Bit(value.isEmpty() ? qgetenv(fallback) : value).trimmed();
|
||||
}
|
||||
|
||||
// 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).
|
||||
@@ -127,15 +150,32 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
m_logos(std::make_unique<LogosModules>(m_logosAPI)),
|
||||
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
|
||||
m_walletController(std::make_unique<WalletController>(
|
||||
*m_wallet, QStringLiteral("AmmUI")))
|
||||
*m_wallet, QStringLiteral("AmmUI"))),
|
||||
m_portfolio(std::make_unique<WalletPortfolioService>()),
|
||||
m_networkProbe(std::make_unique<SequencerIdentityProbe>(this)),
|
||||
m_tokenIdl(resource(QStringLiteral(":/amm/idl/token-idl.json"))),
|
||||
m_ammIdl(resource(QStringLiteral(":/amm/idl/amm-idl.json")))
|
||||
{
|
||||
setWalletStateReady(false);
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("idle"));
|
||||
setAssetError({});
|
||||
|
||||
connect(m_networkProbe.get(), &SequencerIdentityProbe::snapshotChanged,
|
||||
this, [this]() {
|
||||
publishNetworkState();
|
||||
refreshPortfolio();
|
||||
});
|
||||
configureNetworkIdentity();
|
||||
|
||||
connect(m_walletController.get(), &WalletController::stateChanged,
|
||||
this, &AmmUiBackend::syncWalletState);
|
||||
// Publishes an initial "loading" context (walletStateReady is still false,
|
||||
// so it does not yet reach the module).
|
||||
connect(m_walletController.get(), &WalletController::snapshotChanged,
|
||||
this, [this]() {
|
||||
refreshPortfolio();
|
||||
});
|
||||
syncWalletState();
|
||||
publishNetworkState();
|
||||
m_walletController->start();
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
setWalletStateReady(true);
|
||||
@@ -184,6 +224,16 @@ void AmmUiBackend::disconnectWallet()
|
||||
setWalletStateReady(true);
|
||||
}
|
||||
|
||||
bool AmmUiBackend::setAccountAlias(QString accountId, QString alias)
|
||||
{
|
||||
return m_walletController->setAccountAlias(accountId, alias);
|
||||
}
|
||||
|
||||
bool AmmUiBackend::setPrimaryAccount(QString accountId)
|
||||
{
|
||||
return m_walletController->setPrimaryAccount(accountId);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createAccountPublic()
|
||||
{
|
||||
return m_walletController->createAccount(true);
|
||||
@@ -212,8 +262,19 @@ QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
|
||||
void AmmUiBackend::syncWalletState()
|
||||
{
|
||||
const WalletUiState& state = m_walletController->state();
|
||||
const bool walletWasOpen = isWalletOpen();
|
||||
const bool walletWasReady = walletStateReady();
|
||||
const QString previousSyncStatus = walletSyncStatus();
|
||||
const QString previousAddress = sequencerAddr();
|
||||
const bool wasReachable = sequencerReachable();
|
||||
const bool nextReady = state.syncStatus != QStringLiteral("opening")
|
||||
&& state.syncStatus != QStringLiteral("syncing");
|
||||
|
||||
setIsWalletOpen(state.isWalletOpen);
|
||||
setWalletStateReady(nextReady);
|
||||
setWalletSyncStatus(state.syncStatus);
|
||||
setWalletSyncError(state.syncError);
|
||||
setWalletCanSubmit(state.canSubmit());
|
||||
setWalletExists(state.walletExists);
|
||||
setConfigPath(state.configPath);
|
||||
setStoragePath(state.storagePath);
|
||||
@@ -222,6 +283,23 @@ void AmmUiBackend::syncWalletState()
|
||||
setCurrentBlockHeight(state.currentBlockHeight);
|
||||
setSequencerAddr(state.sequencerAddress);
|
||||
setSequencerReachable(state.sequencerReachable);
|
||||
setPrimaryAccountAddress(state.primaryAccountAddress);
|
||||
setPrimaryAccountName(state.primaryAccountName);
|
||||
|
||||
m_networkProbe->setEndpoint(QUrl(state.sequencerAddress));
|
||||
m_networkProbe->setSequencerAvailable(!state.sequencerAddress.isEmpty());
|
||||
m_networkProbe->setReachable(state.sequencerReachable);
|
||||
m_networkProbe->start();
|
||||
|
||||
const bool lifecycleChanged = walletWasOpen != state.isWalletOpen
|
||||
|| walletWasReady != nextReady
|
||||
|| previousSyncStatus != state.syncStatus
|
||||
|| previousAddress != state.sequencerAddress
|
||||
|| wasReachable != state.sequencerReachable;
|
||||
if (lifecycleChanged) {
|
||||
publishNetworkState();
|
||||
refreshPortfolio();
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::resolvePoolAccount(QString defAHex, QString defBHex)
|
||||
@@ -229,6 +307,107 @@ QVariantMap AmmUiBackend::resolvePoolAccount(QString defAHex, QString defBHex)
|
||||
return m_logos->amm_module.resolvePoolAccount(defAHex, defBHex);
|
||||
}
|
||||
|
||||
void AmmUiBackend::configureNetworkIdentity()
|
||||
{
|
||||
const QString networkId = environmentValue(WALLET_NETWORK_ENV, LEGACY_NETWORK_ENV);
|
||||
const QString devnetConfig = environmentValue(
|
||||
WALLET_DEVNET_FILE_ENV, LEGACY_DEVNET_FILE_ENV);
|
||||
const auto settings = SequencerNetworkSettingsLoader::load(networkId, devnetConfig);
|
||||
if (!settings) {
|
||||
qWarning() << "AmmUiBackend: shared network identity configuration is unavailable";
|
||||
m_networkProbe->clearConfiguration();
|
||||
return;
|
||||
}
|
||||
|
||||
SequencerIdentityProbe::Request request;
|
||||
if (settings->identityMethod == SequencerIdentityMethod::ChannelId) {
|
||||
request.method = QStringLiteral("getChannelId");
|
||||
request.identityFromResult = SequencerIdentityProbe::stringIdentity;
|
||||
} else {
|
||||
request.method = QStringLiteral("getBlock");
|
||||
request.params = QJsonArray { 10 };
|
||||
request.identityFromResult = SequencerIdentityProbe::checkpointBlockHash;
|
||||
}
|
||||
m_networkProbe->configure(settings->context, std::move(request));
|
||||
}
|
||||
|
||||
bool AmmUiBackend::resolveProgramIds()
|
||||
{
|
||||
if (m_networkResolved)
|
||||
return true;
|
||||
const QVariantMap config = m_logos->amm_module.configAccount();
|
||||
if (config.value(QStringLiteral("status")).toString() == QStringLiteral("ok")) {
|
||||
m_ammProgramIdCache = m_logos->logos_execution_zone.account_id_from_base58(
|
||||
config.value(QStringLiteral("ammProgramId")).toString());
|
||||
m_tokenProgramIdCache = m_logos->logos_execution_zone.account_id_from_base58(
|
||||
config.value(QStringLiteral("tokenProgramId")).toString());
|
||||
}
|
||||
m_networkResolved = !m_ammProgramIdCache.isEmpty()
|
||||
&& !m_tokenProgramIdCache.isEmpty();
|
||||
return m_networkResolved;
|
||||
}
|
||||
|
||||
void AmmUiBackend::publishNetworkState()
|
||||
{
|
||||
const SequencerNetworkSnapshot& network = m_networkProbe->snapshot();
|
||||
setActiveNetwork(network.id);
|
||||
setNetworkStatus(network.status);
|
||||
setNetworkFingerprint(network.fingerprint);
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshPortfolio()
|
||||
{
|
||||
if (!m_portfolio)
|
||||
return;
|
||||
if (!m_walletController->state().isWalletOpen) {
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("idle"));
|
||||
setAssetError({});
|
||||
return;
|
||||
}
|
||||
|
||||
QString networkStatus = m_networkProbe->snapshot().status;
|
||||
const QStringList tokenIds = knownTokenIds();
|
||||
if (!walletStateReady())
|
||||
networkStatus = QStringLiteral("loading");
|
||||
else if (!resolveProgramIds() || tokenIds.isEmpty()
|
||||
|| networkStatus == QStringLiteral("config_missing"))
|
||||
networkStatus = QStringLiteral("config_missing");
|
||||
if (networkStatus != QStringLiteral("ready")) {
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("blocked"));
|
||||
setAssetError(networkStatus);
|
||||
return;
|
||||
}
|
||||
if (m_tokenIdl.isEmpty()) {
|
||||
setAssetStatus(QStringLiteral("error"));
|
||||
setAssetError(QStringLiteral("token_idl_missing"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_ammProgramIdCache.isEmpty() && !m_ammIdl.isEmpty()) {
|
||||
m_portfolio->registerProgram(
|
||||
m_ammProgramIdCache, QStringLiteral("AMM"), m_ammIdl);
|
||||
}
|
||||
|
||||
WalletPortfolioRequest request(m_walletController->snapshot());
|
||||
request.tokenDefinitionIds = tokenIds;
|
||||
request.tokens = resolveTokens();
|
||||
request.tokenProgramId = m_tokenProgramIdCache;
|
||||
request.tokenIdl = m_tokenIdl;
|
||||
setAssetStatus(QStringLiteral("loading"));
|
||||
setAssetError({});
|
||||
applyPortfolio(m_portfolio->refresh(request));
|
||||
}
|
||||
|
||||
void AmmUiBackend::applyPortfolio(WalletPortfolioResult result)
|
||||
{
|
||||
m_walletController->applyAccountPresentations(result.presentations);
|
||||
setAssets(std::move(result.assets));
|
||||
setAssetStatus(result.status);
|
||||
setAssetError(result.error);
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::configAccount()
|
||||
{
|
||||
return m_logos->amm_module.configAccount();
|
||||
@@ -387,19 +566,9 @@ QVariantList AmmUiBackend::resolveTokens()
|
||||
// holdingId/balance for whichever of these ids the wallet does hold.
|
||||
const bool wallet_open = isWalletOpen();
|
||||
|
||||
QVariantList ids;
|
||||
const QVariantList configured = readTokensConfig();
|
||||
for (const QVariant& entry : configured) {
|
||||
const QString id = entry.toMap().value(QStringLiteral("definitionId")).toString();
|
||||
if (!id.isEmpty())
|
||||
ids.append(id);
|
||||
}
|
||||
const QStringList custom = loadCustomTokenIds();
|
||||
for (const QString& id : custom)
|
||||
ids.append(id);
|
||||
|
||||
QVariantMap request;
|
||||
request.insert(QStringLiteral("tokenIds"), ids);
|
||||
request.insert(QStringLiteral("tokenIds"), knownTokenIds());
|
||||
QVariantList rows = m_logos->amm_module.resolveTokens(request, wallet_open);
|
||||
|
||||
// The module resolves on-chain fields (definitionId/name/holding/balance) but
|
||||
@@ -501,6 +670,21 @@ QStringList AmmUiBackend::loadCustomTokenIds() const
|
||||
return ids;
|
||||
}
|
||||
|
||||
QStringList AmmUiBackend::knownTokenIds() const
|
||||
{
|
||||
QStringList ids;
|
||||
for (const QVariant& entry : readTokensConfig()) {
|
||||
const QString id = entry.toMap().value(QStringLiteral("definitionId")).toString();
|
||||
if (!id.isEmpty() && !ids.contains(id))
|
||||
ids.append(id);
|
||||
}
|
||||
for (const QString& id : loadCustomTokenIds()) {
|
||||
if (!ids.contains(id))
|
||||
ids.append(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
bool AmmUiBackend::saveCustomTokenIds(const QStringList& ids) const
|
||||
{
|
||||
const QString path = customTokenStorePath();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
@@ -17,7 +18,10 @@
|
||||
class LogosAPI;
|
||||
struct LogosModules;
|
||||
class LogosWalletProvider;
|
||||
class SequencerIdentityProbe;
|
||||
class WalletController;
|
||||
class WalletPortfolioService;
|
||||
struct WalletPortfolioResult;
|
||||
|
||||
// Source-side implementation of the AmmUiBackend .rep interface.
|
||||
// Inheriting from AmmUiBackendSimpleSource gives us the generated PROPs and
|
||||
@@ -52,6 +56,8 @@ public slots:
|
||||
QString createNew(QString configPath, QString storagePath, QString password) override;
|
||||
bool openExisting() override;
|
||||
void disconnectWallet() override;
|
||||
bool setAccountAlias(QString accountId, QString alias) override;
|
||||
bool setPrimaryAccount(QString accountId) override;
|
||||
|
||||
// AMM — all forwarded to the amm_module core module.
|
||||
QVariantMap resolvePoolAccount(QString defAHex, QString defBHex) override;
|
||||
@@ -110,6 +116,12 @@ private:
|
||||
QStringList loadCustomTokenIds() const;
|
||||
bool saveCustomTokenIds(const QStringList& ids) const;
|
||||
QString customTokenStorePath() const;
|
||||
QStringList knownTokenIds() const;
|
||||
bool resolveProgramIds();
|
||||
void configureNetworkIdentity();
|
||||
void publishNetworkState();
|
||||
void refreshPortfolio();
|
||||
void applyPortfolio(WalletPortfolioResult result);
|
||||
|
||||
LogosAPI* m_logosAPI;
|
||||
// Handle for the amm_module core module (resolvePool / swapExactInput /
|
||||
@@ -120,6 +132,14 @@ private:
|
||||
std::unique_ptr<LogosModules> m_logos;
|
||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||
std::unique_ptr<WalletController> m_walletController;
|
||||
std::unique_ptr<WalletPortfolioService> m_portfolio;
|
||||
std::unique_ptr<SequencerIdentityProbe> m_networkProbe;
|
||||
|
||||
bool m_networkResolved = false;
|
||||
QString m_ammProgramIdCache;
|
||||
QString m_tokenProgramIdCache;
|
||||
QByteArray m_tokenIdl;
|
||||
QByteArray m_ammIdl;
|
||||
};
|
||||
|
||||
#endif // AMM_UI_BACKEND_H
|
||||
|
||||
@@ -8,6 +8,9 @@ class AmmUiBackend
|
||||
// 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(QString walletSyncStatus READONLY)
|
||||
PROP(QString walletSyncError READONLY)
|
||||
PROP(bool walletCanSubmit READONLY)
|
||||
PROP(bool walletExists READONLY)
|
||||
PROP(QString configPath READONLY)
|
||||
PROP(QString storagePath READONLY)
|
||||
@@ -18,6 +21,15 @@ class AmmUiBackend
|
||||
// Whether the configured sequencer answered the last reachability probe.
|
||||
// Defaults true so the UI doesn't flash a warning before the first check.
|
||||
PROP(bool sequencerReachable READONLY)
|
||||
PROP(QString primaryAccountAddress READONLY)
|
||||
PROP(QString primaryAccountName READONLY)
|
||||
|
||||
PROP(QString activeNetwork READONLY)
|
||||
PROP(QString networkStatus READONLY)
|
||||
PROP(QString networkFingerprint READONLY)
|
||||
PROP(QVariantList assets READONLY)
|
||||
PROP(QString assetStatus READONLY)
|
||||
PROP(QString assetError READONLY)
|
||||
|
||||
// Account management
|
||||
SLOT(QString createAccountPublic())
|
||||
@@ -25,6 +37,8 @@ class AmmUiBackend
|
||||
SLOT(void refreshAccounts())
|
||||
SLOT(void refreshBalances())
|
||||
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
|
||||
SLOT(bool setAccountAlias(QString accountId, QString alias))
|
||||
SLOT(bool setPrimaryAccount(QString accountId))
|
||||
|
||||
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
|
||||
// fresh wallet at the canonical walletHome with no path picking. createNew()
|
||||
@@ -192,8 +206,9 @@ class AmmUiBackend
|
||||
// the configured tokens (TOKENS_CONFIG) plus the user's persisted custom tokens
|
||||
// (see addCustomToken) — the same "known list" shape the swap side shows. Tokens
|
||||
// the wallet merely holds are NOT auto-listed; add an unlisted one by id. Returns
|
||||
// [{ definitionId (base58), name, totalSupply, holdingId, balance }] — every row
|
||||
// the same shape, held tokens first (holdingId "" / balance "0" when not held).
|
||||
// [{ definitionId (base58), definitionIdHex, name, totalSupply, holdingId,
|
||||
// balance }] — every row the same shape, held tokens first (holdingId "" /
|
||||
// balance "0" when not held).
|
||||
SLOT(QVariantList resolveTokens())
|
||||
|
||||
// Adds a user-pasted custom token id (base58 or hex) to the persisted set, after
|
||||
|
||||
+18
-16
@@ -4,15 +4,15 @@ UI-driven tests for the AMM app, driving the running app through the QML
|
||||
inspector (framework from
|
||||
[`logos-co/logos-qt-mcp`](https://github.com/logos-co/logos-qt-mcp)):
|
||||
|
||||
- `swap.mjs` selects two tokens, enters an amount, submits a swap, and verifies
|
||||
- `e2e/swap.mjs` selects two tokens, enters an amount, submits a swap, and verifies
|
||||
the **A/B** pool reserves changed **on-chain**.
|
||||
- `create-pool.mjs` selects the **A/C** pair (which the setup script leaves
|
||||
- `e2e/create-pool.mjs` selects the **A/C** pair (which the setup script leaves
|
||||
unseeded — only A/B is created), submits a pool creation, and verifies the A/C
|
||||
pool now exists **on-chain**.
|
||||
- `add-liquidity.mjs` selects the seeded **A/B** pair, asserts the CTA stays
|
||||
- `e2e/add-liquidity.mjs` selects the seeded **A/B** pair, asserts the CTA stays
|
||||
disabled until deposit amounts are entered, submits an add, and verifies the
|
||||
A/B pool reserves grew **on-chain**.
|
||||
- `custom-token.mjs` pastes token **D**'s id (created on-chain by the setup but
|
||||
- `e2e/custom-token.mjs` pastes token **D**'s id (created on-chain by the setup but
|
||||
deliberately **absent** from the token config) into a Liquidity token slot and
|
||||
verifies the app resolves it, selects it, and **persists** it to the custom-token
|
||||
store — the "add an unlisted token by id" path. No pool / submit involved.
|
||||
@@ -51,19 +51,19 @@ TEST_SEQUENCER_ADDR=http://127.0.0.1:3040 apps/amm/tests/testnet/setup-amm-testn
|
||||
# (it clears this file before + after running). Required for custom-token.mjs to
|
||||
# avoid touching your real custom-token store.
|
||||
LEE_WALLET_HOME_DIR=$(pwd)/apps/amm/tests/testnet/.wallet \
|
||||
AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \
|
||||
AMM_PROGRAM_BIN=$(pwd)/target/guest/amm.bin \
|
||||
TOKENS_CONFIG=$(pwd)/apps/amm/tests/testnet/amm-tokens.json \
|
||||
CUSTOM_TOKEN_CONFIG=$(pwd)/apps/amm/tests/testnet/custom-tokens.json \
|
||||
nix run .#amm-ui
|
||||
|
||||
# 3. Terminal 2 — drive a test; watch it click through the live UI.
|
||||
node apps/amm/tests/swap.mjs # swap against the seeded A/B pool
|
||||
node apps/amm/tests/create-pool.mjs # create the (unseeded) A/C pool
|
||||
node apps/amm/tests/add-liquidity.mjs # add liquidity to the seeded A/B pool
|
||||
node apps/amm/tests/custom-token.mjs # add token D (unlisted) by id
|
||||
node apps/amm/tests/e2e/swap.mjs # swap against the seeded A/B pool
|
||||
node apps/amm/tests/e2e/create-pool.mjs # create the (unseeded) A/C pool
|
||||
node apps/amm/tests/e2e/add-liquidity.mjs # add liquidity to the seeded A/B pool
|
||||
node apps/amm/tests/e2e/custom-token.mjs # add token D (unlisted) by id
|
||||
```
|
||||
|
||||
Headless CI variant (no window, launches the app itself, pass/fail only):
|
||||
Hermetic headless smoke test (no wallet or sequencer required):
|
||||
|
||||
```bash
|
||||
nix build .#integration-test -L
|
||||
@@ -83,16 +83,18 @@ nix build .#integration-test -L
|
||||
re-restores the isolated `.wallet` (rewrites only that directory).
|
||||
- **Overrides.** `TEST_WALLET_HOME`, `TEST_MNEMONIC`, `TEST_WALLET_PASSWORD`,
|
||||
`TEST_WALLET_DEPTH`, `TEST_SEQUENCER_ADDR` — see the script header.
|
||||
- **Framework location.** `swap.mjs` loads `../result-mcp` by default; override
|
||||
- **Framework location.** `e2e/swap.mjs` loads `../../result-mcp` by default; override
|
||||
with `LOGOS_QT_MCP=/abs/path/to/result-mcp`.
|
||||
- **Artifacts.** On failure `swap.mjs` prints the `SwapCard` state and saves
|
||||
`apps/amm/tests/swap-*.png` (git-ignored) for inspection.
|
||||
- **Artifacts.** On failure `e2e/swap.mjs` prints the `SwapCard` state and saves
|
||||
`apps/amm/tests/e2e/swap-*.png` (git-ignored) for inspection.
|
||||
|
||||
## Files
|
||||
|
||||
- `swap.mjs` — the end-to-end swap UI test (A/B pool).
|
||||
- `create-pool.mjs` — the end-to-end create-pool UI test (creates the A/C pool).
|
||||
- `add-liquidity.mjs` — the end-to-end add-liquidity UI test (adds to the A/B pool).
|
||||
- `ui-smoke.mjs` — the hermetic headless UI integration test.
|
||||
- `e2e/swap.mjs` — the end-to-end swap UI test (A/B pool).
|
||||
- `e2e/create-pool.mjs` — the end-to-end create-pool UI test (creates the A/C pool).
|
||||
- `e2e/add-liquidity.mjs` — the end-to-end add-liquidity UI test (adds to the A/B pool).
|
||||
- `e2e/custom-token.mjs` — the custom-token persistence test.
|
||||
- `testnet/setup-amm-testnet.sh` — isolated testnet + wallet bootstrap (TKA/TKB/TKC,
|
||||
seeds the A/B pool only).
|
||||
- `qml/`, `cpp/` — the module's own QML/C++ unit tests.
|
||||
|
||||
@@ -18,13 +18,13 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const fwRoot =
|
||||
process.env.LOGOS_QT_MCP ||
|
||||
new URL("../result-mcp", import.meta.url).pathname;
|
||||
new URL("../../result-mcp", import.meta.url).pathname;
|
||||
const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs"));
|
||||
|
||||
// The token config the app was launched with (same file the setup script writes).
|
||||
const TOKENS_CONFIG =
|
||||
process.env.TOKENS_CONFIG ||
|
||||
new URL("./testnet/amm-tokens.json", import.meta.url).pathname;
|
||||
new URL("../testnet/amm-tokens.json", import.meta.url).pathname;
|
||||
|
||||
// Deposit added for token A (raw base units — the form treats amounts as raw). The
|
||||
// seeded A/B pool is 10000/10000, so 1000 mints a nonzero LP and is trivially funded.
|
||||
@@ -18,13 +18,13 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const fwRoot =
|
||||
process.env.LOGOS_QT_MCP ||
|
||||
new URL("../result-mcp", import.meta.url).pathname;
|
||||
new URL("../../result-mcp", import.meta.url).pathname;
|
||||
const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs"));
|
||||
|
||||
// The token config the app was launched with (same file the setup script writes).
|
||||
const TOKENS_CONFIG =
|
||||
process.env.TOKENS_CONFIG ||
|
||||
new URL("./testnet/amm-tokens.json", import.meta.url).pathname;
|
||||
new URL("../testnet/amm-tokens.json", import.meta.url).pathname;
|
||||
|
||||
// --- small helpers (mirrors swap.mjs) --------------------------------------
|
||||
|
||||
@@ -26,14 +26,14 @@ import { execFileSync } from "node:child_process";
|
||||
|
||||
const fwRoot =
|
||||
process.env.LOGOS_QT_MCP ||
|
||||
new URL("../result-mcp", import.meta.url).pathname;
|
||||
new URL("../../result-mcp", import.meta.url).pathname;
|
||||
const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs"));
|
||||
|
||||
// The isolated test wallet home (set by the setup script) — used to resolve token
|
||||
// D's deterministic definition id via the wallet CLI, the same way the setup does.
|
||||
const WALLET_HOME =
|
||||
process.env.LEE_WALLET_HOME_DIR ||
|
||||
new URL("./testnet/.wallet", import.meta.url).pathname;
|
||||
new URL("../testnet/.wallet", import.meta.url).pathname;
|
||||
|
||||
// Where the app persists custom tokens. Defaults to the isolated test store; set
|
||||
// CUSTOM_TOKEN_CONFIG to override. IMPORTANT: launch the app with the SAME path
|
||||
@@ -42,7 +42,7 @@ const WALLET_HOME =
|
||||
// from a stale slate. Only used to pre-clear; persistence is verified through the app.
|
||||
const CUSTOM_TOKEN_CONFIG =
|
||||
process.env.CUSTOM_TOKEN_CONFIG ||
|
||||
new URL("./testnet/custom-tokens.json", import.meta.url).pathname;
|
||||
new URL("../testnet/custom-tokens.json", import.meta.url).pathname;
|
||||
|
||||
// --- small helpers (mirror create-pool.mjs) --------------------------------
|
||||
|
||||
@@ -21,7 +21,7 @@ import { writeFile } from "node:fs/promises";
|
||||
// Override with LOGOS_QT_MCP=/abs/path/to/result-mcp if it lives elsewhere.
|
||||
const fwRoot =
|
||||
process.env.LOGOS_QT_MCP ||
|
||||
new URL("../result-mcp", import.meta.url).pathname;
|
||||
new URL("../../result-mcp", import.meta.url).pathname;
|
||||
const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs"));
|
||||
|
||||
const SELL_AMOUNT = "100";
|
||||
@@ -22,8 +22,9 @@ TestCase {
|
||||
var summary = createTemporaryObject(summaryComponent, testCase)
|
||||
verify(summary)
|
||||
|
||||
compare(summary.actionText("NewDefinition"), "Create pool")
|
||||
compare(summary.actionText("AddLiquidity"), "Add liquidity")
|
||||
compare(summary.actionText(""), "-")
|
||||
summary.snapshot = { "poolExists": false }
|
||||
compare(summary.actionText(), "Create pool")
|
||||
summary.snapshot = { "poolExists": true }
|
||||
compare(summary.actionText(), "Add liquidity")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ TestCase {
|
||||
Liquidity.NewPositionForm {
|
||||
visible: false
|
||||
width: 760
|
||||
newPositionContext: testCase.readyContext()
|
||||
tokens: testCase.readyTokens()
|
||||
walletReady: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,72 +38,68 @@ TestCase {
|
||||
signalName: "quoteRequested"
|
||||
}
|
||||
|
||||
function readyContext() {
|
||||
return {
|
||||
"status": "ready",
|
||||
"tokens": [
|
||||
{
|
||||
"definitionId": tokenLow,
|
||||
"name": "Low",
|
||||
"totalSupply": "1000000",
|
||||
"balanceRaw": "1000",
|
||||
"selectable": true
|
||||
},
|
||||
{
|
||||
"definitionId": tokenHigh,
|
||||
"name": "High",
|
||||
"totalSupply": "1000000000000",
|
||||
"balanceRaw": "5000000000",
|
||||
"selectable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
function readyTokens() {
|
||||
return [
|
||||
{
|
||||
"definitionId": tokenLow,
|
||||
"name": "Low",
|
||||
"totalSupply": "1000000",
|
||||
"balance": "1000"
|
||||
},
|
||||
{
|
||||
"definitionId": tokenHigh,
|
||||
"name": "High",
|
||||
"totalSupply": "1000000000000",
|
||||
"balance": "5000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function rawAmountsContext() {
|
||||
return {
|
||||
"status": "ready",
|
||||
"tokens": [
|
||||
{
|
||||
"definitionId": tokenLow,
|
||||
"name": "Sir Mints-a-Lot",
|
||||
"totalSupply": "1000000000000",
|
||||
"balanceRaw": "1000000000",
|
||||
"selectable": true
|
||||
},
|
||||
{
|
||||
"definitionId": tokenHigh,
|
||||
"name": "Aurora",
|
||||
"totalSupply": "1000000000000",
|
||||
"balanceRaw": "1000000000",
|
||||
"selectable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
function rawAmountTokens() {
|
||||
return [
|
||||
{
|
||||
"definitionId": tokenLow,
|
||||
"name": "Sir Mints-a-Lot",
|
||||
"totalSupply": "1000000000000",
|
||||
"balance": "1000000000"
|
||||
},
|
||||
{
|
||||
"definitionId": tokenHigh,
|
||||
"name": "Aurora",
|
||||
"totalSupply": "1000000000000",
|
||||
"balance": "1000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function flowState(quote) {
|
||||
return {
|
||||
function flowState(quote, poolExists) {
|
||||
var state = {
|
||||
"quote": quote || ({}),
|
||||
"contextLoading": false,
|
||||
"quoteLoading": false,
|
||||
"quoteStale": false,
|
||||
"submitting": false
|
||||
}
|
||||
if (poolExists !== undefined)
|
||||
state.poolExists = poolExists
|
||||
else if (quote && quote.poolStatus === "active_pool")
|
||||
state.poolExists = true
|
||||
else if (quote && quote.poolStatus === "missing_pool")
|
||||
state.poolExists = false
|
||||
return state
|
||||
}
|
||||
|
||||
function createEmptyForm(context) {
|
||||
function createEmptyForm(tokens) {
|
||||
var form = createTemporaryObject(formComponent, testCase, {
|
||||
"flowState": flowState(({})),
|
||||
"newPositionContext": context || readyContext()
|
||||
"tokens": tokens || readyTokens()
|
||||
})
|
||||
verify(form)
|
||||
wait(0)
|
||||
return form
|
||||
}
|
||||
|
||||
function createForm(context) {
|
||||
var form = createEmptyForm(context)
|
||||
function createForm(tokens) {
|
||||
var form = createEmptyForm(tokens)
|
||||
form.selectToken("A", tokenLow)
|
||||
form.selectToken("B", tokenHigh)
|
||||
compare(form.selectedTokenAId, tokenLow)
|
||||
@@ -175,7 +172,7 @@ TestCase {
|
||||
}
|
||||
|
||||
function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() {
|
||||
var form = createForm(rawAmountsContext())
|
||||
var form = createForm(rawAmountTokens())
|
||||
form.priceAmountA = "15"
|
||||
form.priceAmountB = "10"
|
||||
form.flowState = flowState({
|
||||
@@ -210,7 +207,7 @@ TestCase {
|
||||
}
|
||||
|
||||
function test_missingPoolRoundsPairedRawAmounts() {
|
||||
var form = createForm(rawAmountsContext())
|
||||
var form = createForm(rawAmountTokens())
|
||||
form.priceAmountA = "15"
|
||||
form.priceAmountB = "10"
|
||||
form.flowState = flowState({
|
||||
@@ -378,7 +375,6 @@ TestCase {
|
||||
|
||||
compare(form.fieldError("amountB"), "")
|
||||
compare(form.formErrorText(), "")
|
||||
compare(form.accountPreview().length, 0)
|
||||
}
|
||||
|
||||
function test_activePoolEditUsesDisplayReserveRatio() {
|
||||
@@ -421,6 +417,8 @@ TestCase {
|
||||
|
||||
function test_activePoolEditRecoversAfterInvalidQuote() {
|
||||
var form = createForm()
|
||||
form.flowState = flowState(({}), true)
|
||||
wait(0)
|
||||
form.flowState = flowState({
|
||||
"status": "ok",
|
||||
"tokenAId": tokenHigh,
|
||||
@@ -441,7 +439,7 @@ TestCase {
|
||||
"code": "value_must_be_positive",
|
||||
"tokenAId": tokenHigh,
|
||||
"tokenBId": tokenLow
|
||||
})
|
||||
}, true)
|
||||
wait(0)
|
||||
|
||||
compare(form.poolFeeBps, 30)
|
||||
@@ -485,7 +483,7 @@ TestCase {
|
||||
"code": "fee_tier_mismatch",
|
||||
"details": { "poolFeeBps": "5" }
|
||||
}]
|
||||
})
|
||||
}, true)
|
||||
wait(0)
|
||||
|
||||
compare(form.selectedFeeBps, 5)
|
||||
@@ -497,17 +495,12 @@ TestCase {
|
||||
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountB, "1000")
|
||||
}
|
||||
|
||||
function test_contextFailureFinishesTokenResolution() {
|
||||
function test_tokenResolutionFailureClearsPendingState() {
|
||||
var form = createForm()
|
||||
form.resolvingTokenId = tokenThird
|
||||
form.resolvingTokenSide = "A"
|
||||
|
||||
form.newPositionContext = {
|
||||
"status": "error",
|
||||
"code": "config_unavailable",
|
||||
"tokens": []
|
||||
}
|
||||
form.finishTokenResolution(true)
|
||||
form.failTokenResolution("config_unavailable")
|
||||
wait(0)
|
||||
|
||||
compare(form.resolvingTokenId, "")
|
||||
@@ -515,53 +508,27 @@ TestCase {
|
||||
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.minimumAmountA = "12"
|
||||
form.minimumAmountB = "34"
|
||||
form.confirmedPoolStatus = "active_pool"
|
||||
|
||||
form.newPositionContext = {
|
||||
"status": "ready",
|
||||
"tokens": [
|
||||
{
|
||||
"definitionId": tokenHigh,
|
||||
"name": "High",
|
||||
"totalSupply": "1000000000000",
|
||||
"balanceRaw": "5000000000",
|
||||
"selectable": true
|
||||
},
|
||||
{
|
||||
"definitionId": tokenThird,
|
||||
"name": "Third",
|
||||
"totalSupply": "1000000",
|
||||
"balanceRaw": "100",
|
||||
"selectable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
form.tokens = [
|
||||
{
|
||||
"definitionId": tokenHigh,
|
||||
"name": "High",
|
||||
"totalSupply": "1000000000000",
|
||||
"balance": "5000000000"
|
||||
},
|
||||
{
|
||||
"definitionId": tokenThird,
|
||||
"name": "Third",
|
||||
"totalSupply": "1000000",
|
||||
"balance": "100"
|
||||
}
|
||||
]
|
||||
wait(0)
|
||||
|
||||
compare(form.selectedTokenAId, "")
|
||||
@@ -570,27 +537,6 @@ TestCase {
|
||||
compare(form.amountB, "")
|
||||
compare(form.minimumAmountA, "")
|
||||
compare(form.minimumAmountB, "")
|
||||
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() {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtTest
|
||||
|
||||
import "../../qml/components/liquidity" as Liquidity
|
||||
import "../../qml/components/swap" as Swap
|
||||
|
||||
TestCase {
|
||||
id: testCase
|
||||
|
||||
name: "TokenInput"
|
||||
readonly property string base58Id: "Eiw5zDP1BKukkxMY8dj7Hkw9NfCTh6iU5av5F7FT8ExC"
|
||||
readonly property string hexId: "cbe5e5fed00f9af47a0cbc6f96de828dd8e72090971fa1b904bec2014e3f634d"
|
||||
|
||||
Component {
|
||||
id: inputComponent
|
||||
|
||||
Swap.TokenInput {
|
||||
visible: false
|
||||
width: 400
|
||||
theme: inputTheme
|
||||
selectorObjectName: "accountSelector"
|
||||
|
||||
Liquidity.AmmTheme {
|
||||
id: inputTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createInput(definitionId) {
|
||||
return createTemporaryObject(inputComponent, testCase, {
|
||||
"token": { "definitionId": definitionId, "symbol": "TKA" },
|
||||
"holdings": [{
|
||||
"accountId": "holding-a",
|
||||
"accountType": "TokenHolding",
|
||||
"balanceRaw": "10",
|
||||
"definitionId": base58Id,
|
||||
"definitionIdHex": hexId
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
function test_base58DefinitionSelectsHolding() {
|
||||
var input = createInput(base58Id)
|
||||
verify(input)
|
||||
var selector = findChild(input, "accountSelector")
|
||||
verify(selector)
|
||||
|
||||
compare(selector.stateField, "definitionId")
|
||||
tryCompare(selector, "hasFunds", true)
|
||||
tryCompare(selector, "selectedAccountId", "holding-a")
|
||||
}
|
||||
|
||||
function test_hexDefinitionSelectsHolding() {
|
||||
var input = createInput(hexId)
|
||||
verify(input)
|
||||
var selector = findChild(input, "accountSelector")
|
||||
verify(selector)
|
||||
|
||||
compare(selector.stateField, "definitionIdHex")
|
||||
tryCompare(selector, "hasFunds", true)
|
||||
tryCompare(selector, "selectedAccountId", "holding-a")
|
||||
}
|
||||
}
|
||||
@@ -70,10 +70,10 @@ TEST_SEQUENCER_ADDR="${TEST_SEQUENCER_ADDR:-}"
|
||||
# Deterministic accounts, created in THIS fixed order after a fresh restore so
|
||||
# their ids are reproducible. Resolved to ids at runtime via `wallet account id`.
|
||||
# token-c-*/token-d-* are APPENDED (not inserted) so the pre-existing a/b/lp ids don't shift.
|
||||
# Token C has no seeded pool — the create-pool UI test (apps/amm/tests/create-pool.mjs)
|
||||
# Token C has no seeded pool — the create-pool UI test (apps/amm/tests/e2e/create-pool.mjs)
|
||||
# creates the A/C pool itself, minting its own LP holding via the app.
|
||||
# Token D is created but LEFT OUT of the token config — the custom-token UI test
|
||||
# (apps/amm/tests/custom-token.mjs) adds it by id.
|
||||
# (apps/amm/tests/e2e/custom-token.mjs) adds it by id.
|
||||
ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holding token-c-def token-c-holding token-d-def token-d-holding)
|
||||
|
||||
###############################################################################
|
||||
@@ -81,9 +81,9 @@ ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holdi
|
||||
###############################################################################
|
||||
|
||||
# --- Program binaries (docker release builds; image ids must match deployment) ---
|
||||
TOKEN_BIN="programs/token/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token.bin"
|
||||
AMM_BIN="programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin"
|
||||
TWAP_BIN="programs/twap_oracle/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/twap_oracle.bin"
|
||||
TOKEN_BIN="${TOKEN_BIN:-target/guest/token.bin}"
|
||||
AMM_BIN="${AMM_BIN:-target/guest/amm.bin}"
|
||||
TWAP_BIN="${TWAP_BIN:-target/guest/twap_oracle.bin}"
|
||||
|
||||
# --- IDLs ---
|
||||
TOKEN_IDL="artifacts/token-idl.json"
|
||||
@@ -522,6 +522,7 @@ log ""
|
||||
log "Token D was created ON-CHAIN but left out of the token config (the ${DIM}custom${RST}"
|
||||
log "token). Its id: ${DIM}$TOKEN_D_DEF${RST}"
|
||||
log ""
|
||||
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)"
|
||||
log " or: ${DIM}node apps/amm/tests/custom-token.mjs${RST} (add token D by id)"
|
||||
log "Then in another terminal: ${DIM}node apps/amm/tests/e2e/swap.mjs${RST} (swap A/B)"
|
||||
log " or: ${DIM}node apps/amm/tests/e2e/create-pool.mjs${RST} (create A/C pool)"
|
||||
log " or: ${DIM}node apps/amm/tests/e2e/add-liquidity.mjs${RST} (add A/B liquidity)"
|
||||
log " or: ${DIM}node apps/amm/tests/e2e/custom-token.mjs${RST} (add token D by id)"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const frameworkRoot = process.env.LOGOS_QT_MCP;
|
||||
if (!frameworkRoot)
|
||||
throw new Error("LOGOS_QT_MCP is required");
|
||||
|
||||
const { test, run } = await import(resolve(frameworkRoot, "test-framework/framework.mjs"));
|
||||
|
||||
test("amm ui: renders primary navigation", async (app) => {
|
||||
await app.waitFor(
|
||||
async () => { await app.expectTexts(["Trade", "Liquidity", "Pools"]); },
|
||||
{ timeout: 20000, interval: 500, description: "primary navigation" },
|
||||
);
|
||||
});
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user