mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(amm): move all AMM logic into the amm_module core module; flip UI to consume it
Introduce modules/amm — the AMM business logic as a universal core Logos module, consumed identically by the QML UI (via modules().amm_module) and headlessly (logoscore call amm_module ...). The module is a thin transport adapter: the domain math lives in the Rust amm_client crate (the transport-independent JSON FFI), and the module sequences those pure ops with chain I/O delegated to the logos_execution_zone wallet module. It reaches the same shared wallet instance the UI opened (Basecamp loads core modules as singletons; standalone the LogosAPI client cache dedups the connection), so it never opens a second wallet. The module owns the full AMM surface — not just swaps: - resolvePool / swapExactInput / tokenList (the swap path) - newPositionContext / quoteNewPosition / submitNewPosition (add-liquidity) apps/amm: delete the app-side orchestration (SwapRuntime, NewPositionRuntime, AmmClient/BundledAmmClient) and the amm_client link. AmmUiBackend now owns only wallet-session lifecycle and forwards every AMM slot to modules().amm_module. The one wallet-keyset mutation add-liquidity needs — creating a fresh LP holding — stays in the backend (via its wallet provider, keeping the account model and on-disk storage coherent): the module returns "requires_fresh_lp" without submitting, the backend creates the account and resubmits with its id. flake.nix / CMakeLists / metadata: the module links the amm_client crate; the UI links no external lib and depends on amm_module (injected into the UI builder's flakeInputs so the dependency resolves). - amounts/deadline declared nlohmann::json so the generated dispatch accepts a JSON number (bare small ints on the CLI) or a string (exact u128 from the UI, or a quote-wrapped big value on the CLI); JSON floats are rejected rather than submit a silently-rounded amount. - AMM_DEBUG-gated tracing for the swap path. - Drop tests/cpp/NewPositionRuntimeTest.cpp with the class it covered (module-level tests to follow). - modules/amm/README.md: architecture, headless prerequisites, logoscore recipe.
This commit is contained in:
+3
-27
@@ -4,9 +4,6 @@ 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})
|
||||
@@ -27,6 +24,9 @@ add_subdirectory("${LOGOS_WALLET_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/share
|
||||
|
||||
# ui_qml module with a hand-written C++ backend (QtRO .rep view contract +
|
||||
# generated *SimpleSource/*ViewPluginBase). Mirrors the LEZ wallet UI module.
|
||||
# The AMM business logic lives in the amm_module core module (declared as a
|
||||
# dependency in metadata.json, reached via modules().amm_module in the backend),
|
||||
# so the UI links no amm_client library of its own.
|
||||
logos_module(
|
||||
NAME amm_ui
|
||||
REP_FILE src/AmmUiBackend.rep
|
||||
@@ -36,34 +36,10 @@ logos_module(
|
||||
src/AmmUiPlugin.cpp
|
||||
src/AmmUiBackend.h
|
||||
src/AmmUiBackend.cpp
|
||||
src/ActiveNetwork.h
|
||||
src/AmmClient.h
|
||||
src/AmmClient.cpp
|
||||
src/NewPositionRuntime.h
|
||||
src/NewPositionRuntime.cpp
|
||||
src/SwapRuntime.h
|
||||
src/SwapRuntime.cpp
|
||||
FIND_PACKAGES
|
||||
Qt6Gui
|
||||
LINK_LIBRARIES
|
||||
Qt6::Gui
|
||||
PkgConfig::BASE58
|
||||
LINK_TARGETS
|
||||
logos_wallet_access
|
||||
EXTERNAL_LIBS
|
||||
amm_client
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
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()
|
||||
|
||||
+7
-6
@@ -52,9 +52,9 @@ This makes `lgpm` available as a global command.
|
||||
|
||||
## Running the UI standalone
|
||||
|
||||
The app is built from the **repository-root** flake (which also provides the
|
||||
`amm_client_ffi` library it links). From the repo root, launch it with its named
|
||||
attribute:
|
||||
The app is built from the **repository-root** flake (which also builds the
|
||||
`amm_module` core module the UI delegates its AMM logic to). From the repo root,
|
||||
launch it with its named attribute:
|
||||
|
||||
```bash
|
||||
nix run .#amm-ui
|
||||
@@ -62,9 +62,10 @@ nix run .#amm-ui
|
||||
|
||||
This builds and runs the application in development mode. The Logos bridge is unavailable in standalone mode, but the UI layout and mock data are fully functional.
|
||||
|
||||
Build just the FFI crate with `nix build .#amm_client_ffi`. (Each UI is exposed
|
||||
under its own name, so future apps are `nix run .#<name>` — there is no bare
|
||||
`nix run .` default.)
|
||||
Build just the AMM core module with `nix build .#amm-module`, or its underlying
|
||||
client crate with `nix build .#amm_client`. (Each UI is exposed under its own
|
||||
name, so future apps are `nix run .#<name>` — there is no bare `nix run .`
|
||||
default.)
|
||||
|
||||
## Running inside Logos Basecamp
|
||||
|
||||
|
||||
@@ -7,20 +7,18 @@
|
||||
"main": "amm_ui_plugin",
|
||||
"view": "qml/Main.qml",
|
||||
"icon": "icons/amm.png",
|
||||
"dependencies": ["logos_execution_zone"],
|
||||
"dependencies": ["logos_execution_zone", "amm_module"],
|
||||
|
||||
"nix": {
|
||||
"packages": {
|
||||
"build": ["pkg-config"],
|
||||
"runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"]
|
||||
},
|
||||
"external_libraries": [
|
||||
{ "name": "amm_client" }
|
||||
],
|
||||
"external_libraries": [],
|
||||
"cmake": {
|
||||
"find_packages": [],
|
||||
"extra_sources": [],
|
||||
"extra_include_dirs": ["lib"],
|
||||
"extra_include_dirs": [],
|
||||
"extra_link_libraries": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,11 @@ Rectangle {
|
||||
readonly property bool hasAmount: editingSide === "sell" ? parsedSellInput > 0 : parsedBuyInput > 0
|
||||
readonly property bool tokensSelected: sellToken !== null && buyToken !== null
|
||||
readonly property bool insufficientLiquidity: hasAmount && root.poolExists && parsedBuyAmount > buyReserveNum
|
||||
// True only when THIS app's wallet is connected. The backend also enforces
|
||||
// this before submitting (AmmUiBackend::swapExactInput), but gate the UI too
|
||||
// so a disconnected app never even initiates a swap against the shared wallet.
|
||||
readonly property bool walletOpen: root.backend !== null && root.backend.isWalletOpen
|
||||
|
||||
// The backend only exposes swapExactInput, so only the "I know exactly
|
||||
// how much I'm selling" direction can actually be submitted. Editing the
|
||||
// buy field still previews an estimate (via amountInFor above) but can't
|
||||
@@ -175,6 +180,7 @@ Rectangle {
|
||||
&& parsedSellAmount > 0 && parsedBuyAmount > 0
|
||||
&& root.poolResolved && root.poolExists
|
||||
&& !insufficientLiquidity && !root.swapInProgress
|
||||
&& root.walletOpen
|
||||
|
||||
readonly property string submitButtonText: {
|
||||
if (!tokensSelected) return qsTr("Select tokens")
|
||||
@@ -185,6 +191,7 @@ Rectangle {
|
||||
if (!root.poolExists) return qsTr("No pool / no liquidity")
|
||||
if (insufficientLiquidity) return qsTr("Insufficient liquidity")
|
||||
if (parsedBuyAmount <= 0) return qsTr("Amount too small")
|
||||
if (!root.walletOpen) return qsTr("Connect wallet to swap")
|
||||
return qsTr("Swap")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
// 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;
|
||||
QString fingerprint;
|
||||
QString ammProgramId;
|
||||
QStringList tokenIds;
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
#include "AmmClient.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
AmmClientResult BundledAmmClient::swapPair(const QJsonObject& request) const
|
||||
{
|
||||
return call(amm_swap_pair, request);
|
||||
}
|
||||
|
||||
AmmClientResult BundledAmmClient::resolvePool(const QJsonObject& request) const
|
||||
{
|
||||
return call(amm_resolve_pool, request);
|
||||
}
|
||||
|
||||
AmmClientResult BundledAmmClient::swapPlan(const QJsonObject& request) const
|
||||
{
|
||||
return call(amm_swap_plan, request);
|
||||
}
|
||||
|
||||
AmmClientResult BundledAmmClient::programId(const QJsonObject& request) const
|
||||
{
|
||||
return call(amm_program_id, request);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
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;
|
||||
virtual AmmClientResult swapPair(const QJsonObject& request) const = 0;
|
||||
virtual AmmClientResult resolvePool(const QJsonObject& request) const = 0;
|
||||
virtual AmmClientResult swapPlan(const QJsonObject& request) const = 0;
|
||||
virtual AmmClientResult programId(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;
|
||||
AmmClientResult swapPair(const QJsonObject& request) const override;
|
||||
AmmClientResult resolvePool(const QJsonObject& request) const override;
|
||||
AmmClientResult swapPlan(const QJsonObject& request) const override;
|
||||
AmmClientResult programId(const QJsonObject& request) const override;
|
||||
};
|
||||
+81
-205
@@ -1,46 +1,50 @@
|
||||
#include "AmmUiBackend.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "AmmClient.h"
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "NewPositionRuntime.h"
|
||||
#include "SwapRuntime.h"
|
||||
#include "WalletController.h"
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
namespace {
|
||||
// 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
|
||||
// the raw guest ELF yields a different/failed program id). The app can't
|
||||
// safely embed/derive this itself: the wallet module's bundled AMM program
|
||||
// may differ from whatever is actually deployed on the target sequencer, and
|
||||
// the binary's bytes are what determine its program id (and therefore every
|
||||
// PDA derived from it). See apps/amm/README.md.
|
||||
const char AMM_PROGRAM_BIN_ENV[] = "AMM_PROGRAM_BIN";
|
||||
const char NEW_POSITION_SCHEMA[] = "new-position.v1";
|
||||
|
||||
// Absolute path to the JSON token-list config consumed by tokenList()
|
||||
// (see apps/amm/README.md). Config-driven so the Swap view's token picker
|
||||
// doesn't need a hardcoded/dummy token list.
|
||||
const char TOKENS_CONFIG_ENV[] = "TOKENS_CONFIG";
|
||||
// The new-position context placeholder published before the module
|
||||
// connection is up (matches the module's "loading" contextState).
|
||||
QVariantMap loadingContext()
|
||||
{
|
||||
return QVariantMap {
|
||||
{ QStringLiteral("schema"), QString::fromLatin1(NEW_POSITION_SCHEMA) },
|
||||
{ QStringLiteral("status"), QStringLiteral("loading") },
|
||||
{ QStringLiteral("networkId"), QStringLiteral("lez") },
|
||||
{ QStringLiteral("networkFingerprint"), QString() },
|
||||
{ QStringLiteral("tokens"), QVariantList() },
|
||||
{ QStringLiteral("feeTiers"), QVariantList() },
|
||||
{ QStringLiteral("warnings"), QVariantList() },
|
||||
};
|
||||
}
|
||||
|
||||
// A new-position.v1 error envelope (matches the module's publicError), for
|
||||
// the backend-side failure paths (e.g. LP-account creation failing).
|
||||
QVariantMap newPositionError(const QString& code)
|
||||
{
|
||||
return QVariantMap {
|
||||
{ QStringLiteral("schema"), QString::fromLatin1(NEW_POSITION_SCHEMA) },
|
||||
{ QStringLiteral("status"), QStringLiteral("error") },
|
||||
{ QStringLiteral("canSubmit"), false },
|
||||
{ QStringLiteral("code"), code },
|
||||
{ QStringLiteral("errors"), QVariantList { QVariantMap {
|
||||
{ QStringLiteral("code"), code },
|
||||
{ QStringLiteral("recoverable"), true },
|
||||
{ QStringLiteral("blockingFields"), QVariantList() },
|
||||
{ QStringLiteral("details"), QVariantMap() },
|
||||
} } },
|
||||
{ QStringLiteral("warnings"), QVariantList() },
|
||||
{ QStringLiteral("accountPreview"), QVariantList() },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
@@ -49,17 +53,14 @@ 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_ammClient(std::make_unique<BundledAmmClient>()),
|
||||
m_newPosition(std::make_unique<NewPositionRuntime>(m_wallet.get(), m_ammClient.get())),
|
||||
m_swap(std::make_unique<SwapRuntime>(m_wallet.get(), m_ammClient.get()))
|
||||
*m_wallet, QStringLiteral("AmmUI")))
|
||||
{
|
||||
setWalletStateReady(false);
|
||||
setNewPositionContext(m_newPosition->context(
|
||||
QVariantMap(), networkSnapshot(), false, false));
|
||||
|
||||
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).
|
||||
syncWalletState();
|
||||
m_walletController->start();
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
@@ -107,7 +108,6 @@ void AmmUiBackend::disconnectWallet()
|
||||
{
|
||||
m_walletController->disconnect();
|
||||
setWalletStateReady(true);
|
||||
m_newPosition->clearWalletAccounts();
|
||||
refreshNewPositionContext(QVariantMap());
|
||||
}
|
||||
|
||||
@@ -147,25 +147,45 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request)
|
||||
else {
|
||||
request = m_newPositionHints;
|
||||
}
|
||||
setNewPositionContext(m_newPosition->context(
|
||||
request, networkSnapshot(), isWalletOpen(), refreshWalletAccounts));
|
||||
if (!walletStateReady()) {
|
||||
setNewPositionContext(loadingContext());
|
||||
return;
|
||||
}
|
||||
setNewPositionContext(m_logos->amm_module.newPositionContext(
|
||||
request, isWalletOpen(), refreshWalletAccounts));
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request)
|
||||
{
|
||||
return m_newPosition->quote(request, networkSnapshot(), isWalletOpen());
|
||||
return m_logos->amm_module.quoteNewPosition(request, isWalletOpen());
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::submitNewPosition(QVariantMap request, QString quoteHash)
|
||||
{
|
||||
return m_newPosition->submit(
|
||||
request, quoteHash, networkSnapshot(), isWalletOpen());
|
||||
// First attempt with no LP account. If the module needs a fresh LP holding
|
||||
// it returns "requires_fresh_lp" without submitting; we own wallet-keyset
|
||||
// mutation, so create the account here (keeping the account model + on-disk
|
||||
// storage coherent) and resubmit with its id.
|
||||
QVariantMap result = m_logos->amm_module.submitNewPosition(
|
||||
request, quoteHash, isWalletOpen(), QString());
|
||||
|
||||
if (result.value(QStringLiteral("status")).toString()
|
||||
== QStringLiteral("requires_fresh_lp")) {
|
||||
const QString lpId = m_walletController->createAccount(true);
|
||||
if (lpId.isEmpty())
|
||||
return newPositionError(QStringLiteral("wallet_submission_failed"));
|
||||
result = m_logos->amm_module.submitNewPosition(
|
||||
request, quoteHash, isWalletOpen(), lpId);
|
||||
}
|
||||
|
||||
if (result.value(QStringLiteral("status")).toString() == QStringLiteral("submitted"))
|
||||
refreshBalances();
|
||||
return result;
|
||||
}
|
||||
|
||||
void AmmUiBackend::syncWalletState()
|
||||
{
|
||||
const WalletUiState& state = m_walletController->state();
|
||||
const bool walletWasOpen = isWalletOpen();
|
||||
|
||||
setIsWalletOpen(state.isWalletOpen);
|
||||
setWalletExists(state.walletExists);
|
||||
@@ -177,129 +197,39 @@ void AmmUiBackend::syncWalletState()
|
||||
setSequencerAddr(state.sequencerAddress);
|
||||
setSequencerReachable(state.sequencerReachable);
|
||||
|
||||
if (walletWasOpen && !state.isWalletOpen)
|
||||
m_newPosition->clearWalletAccounts();
|
||||
|
||||
publishNetworkContext();
|
||||
}
|
||||
|
||||
void AmmUiBackend::publishNetworkContext()
|
||||
{
|
||||
setNewPositionContext(m_newPosition->context(
|
||||
m_newPositionHints, networkSnapshot(), isWalletOpen(), false));
|
||||
}
|
||||
|
||||
QString AmmUiBackend::ammProgramIdHex()
|
||||
{
|
||||
const QByteArray elf = loadAmmElf();
|
||||
if (elf.isEmpty())
|
||||
return QString();
|
||||
// Hand the deployed program binary to the amm_client program_id op, which
|
||||
// decodes it and computes the Image ID — 64-char lowercase hex, little-endian
|
||||
// per u32 word (matches `spel program-id` and the on-chain *_program_id fields).
|
||||
const AmmClientResult result = m_ammClient->programId(
|
||||
QJsonObject { { QStringLiteral("elf"), QString::fromLatin1(elf.toHex()) } });
|
||||
if (!result.ok) {
|
||||
qWarning() << "AmmUiBackend::ammProgramIdHex: amm_program_id failed";
|
||||
return QString();
|
||||
}
|
||||
return result.value.value(QStringLiteral("programId")).toString();
|
||||
}
|
||||
|
||||
ActiveNetworkSnapshot AmmUiBackend::networkSnapshot()
|
||||
{
|
||||
ActiveNetworkSnapshot snapshot;
|
||||
snapshot.id = QStringLiteral("lez");
|
||||
// Defer program/token resolution (which reaches the module) until wallet
|
||||
// state is resolved; the constructor publishes an initial context before
|
||||
// the module is up, and syncWalletState() republishes once it is.
|
||||
if (!walletStateReady()) {
|
||||
snapshot.status = QStringLiteral("loading");
|
||||
return snapshot;
|
||||
setNewPositionContext(loadingContext());
|
||||
return;
|
||||
}
|
||||
// Resolve the AMM deployment id ($AMM_PROGRAM_BIN) and configured token set
|
||||
// ($TOKENS_CONFIG) ONCE and cache — they're fixed for the process lifetime.
|
||||
// networkSnapshot() runs on the quote hot path and from inside runtime reply
|
||||
// callbacks, and tokenList() makes remote base58 conversions; recomputing each
|
||||
// call reenters the module connection and hangs the reply.
|
||||
if (!m_networkResolved) {
|
||||
m_ammProgramIdCache = ammProgramIdHex();
|
||||
m_tokenIdsCache.clear();
|
||||
// Configured token set = the TOKENS_CONFIG definition ids, the same source
|
||||
// the Swap view's token picker uses (tokenList normalizes them to hex).
|
||||
const QVariantList tokens = tokenList();
|
||||
for (const QVariant& entry : tokens) {
|
||||
const QString id = entry.toMap().value(QStringLiteral("definitionId")).toString();
|
||||
if (!id.isEmpty())
|
||||
m_tokenIdsCache.append(id);
|
||||
}
|
||||
m_networkResolved = true;
|
||||
}
|
||||
snapshot.ammProgramId = m_ammProgramIdCache;
|
||||
// Bind a quote to this AMM deployment: the program id changes per deployment,
|
||||
// so it doubles as the network fingerprint (a quote can't be replayed against
|
||||
// a different program). Empty when AMM_PROGRAM_BIN is unset — status gates it.
|
||||
snapshot.fingerprint = m_ammProgramIdCache;
|
||||
snapshot.tokenIds = m_tokenIdsCache;
|
||||
snapshot.status = m_ammProgramIdCache.isEmpty()
|
||||
? QStringLiteral("config_missing")
|
||||
: QStringLiteral("ready");
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
QString AmmUiBackend::normalizeAccountId(const QString& id)
|
||||
{
|
||||
const QString t = id.trimmed();
|
||||
// Already 64 hex chars?
|
||||
if (t.size() == 64) {
|
||||
bool allHex = true;
|
||||
for (const QChar c : t) {
|
||||
if (!std::isxdigit(static_cast<unsigned char>(c.toLatin1()))) {
|
||||
allHex = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allHex)
|
||||
return t.toLower();
|
||||
}
|
||||
// Try base58 -> hex via the wallet module.
|
||||
const QString hex = m_logos->logos_execution_zone.account_id_from_base58(t);
|
||||
return hex.toLower(); // account_id_from_base58 returns "" on failure
|
||||
}
|
||||
|
||||
QByteArray AmmUiBackend::loadAmmElf()
|
||||
{
|
||||
const QByteArray binPath = qgetenv(AMM_PROGRAM_BIN_ENV);
|
||||
if (binPath.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend::loadAmmElf: AMM_PROGRAM_BIN not set";
|
||||
return QByteArray();
|
||||
}
|
||||
QFile elfFile(QString::fromLocal8Bit(binPath));
|
||||
if (!elfFile.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "AmmUiBackend::loadAmmElf: cannot read AMM_PROGRAM_BIN at" << elfFile.fileName();
|
||||
return QByteArray();
|
||||
}
|
||||
const QByteArray elf = elfFile.readAll();
|
||||
elfFile.close();
|
||||
if (elf.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend::loadAmmElf: AMM_PROGRAM_BIN is empty";
|
||||
return QByteArray();
|
||||
}
|
||||
return elf;
|
||||
setNewPositionContext(m_logos->amm_module.newPositionContext(
|
||||
m_newPositionHints, isWalletOpen(), false));
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
||||
{
|
||||
return m_swap->resolvePool(defAHex, defBHex, networkSnapshot());
|
||||
return m_logos->amm_module.resolvePool(defAHex, defBHex);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal)
|
||||
{
|
||||
const QString txHash = m_swap->swap(defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex,
|
||||
amountInDecimal, minOutDecimal, deadlineDecimal,
|
||||
networkSnapshot(), isWalletOpen());
|
||||
// This app's connected state is the authoritative submit guard. disconnectWallet()
|
||||
// only locks this UI and leaves the shared logos_execution_zone wallet open (another
|
||||
// app may keep it open, or this app opened-then-disconnected), and the QML submit path
|
||||
// doesn't check isWalletOpen — so without this a swap could sign/submit while the UI
|
||||
// shows "Connect". Mirrors the guard the old SwapRuntime::swap() enforced.
|
||||
if (!isWalletOpen())
|
||||
return {};
|
||||
|
||||
const QString txHash = m_logos->amm_module.swapExactInput(
|
||||
defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex,
|
||||
amountInDecimal, minOutDecimal, deadlineDecimal);
|
||||
if (!txHash.isEmpty())
|
||||
refreshBalances();
|
||||
return txHash;
|
||||
@@ -307,59 +237,5 @@ QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString u
|
||||
|
||||
QVariantList AmmUiBackend::tokenList()
|
||||
{
|
||||
QVariantList out;
|
||||
|
||||
const QByteArray path = qgetenv(TOKENS_CONFIG_ENV);
|
||||
if (path.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend::tokenList: TOKENS_CONFIG not set";
|
||||
return out;
|
||||
}
|
||||
|
||||
QFile file(QString::fromLocal8Bit(path));
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "AmmUiBackend::tokenList: cannot read TOKENS_CONFIG at" << file.fileName();
|
||||
return out;
|
||||
}
|
||||
const QByteArray json = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonParseError parseError{};
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isArray()) {
|
||||
qWarning() << "AmmUiBackend::tokenList: TOKENS_CONFIG at" << file.fileName()
|
||||
<< "is not a valid JSON array:" << parseError.errorString();
|
||||
return out;
|
||||
}
|
||||
|
||||
const QJsonArray arr = doc.array();
|
||||
for (const QJsonValue& entry : arr) {
|
||||
if (!entry.isObject()) {
|
||||
qWarning() << "AmmUiBackend::tokenList: skipping non-object entry in TOKENS_CONFIG";
|
||||
continue;
|
||||
}
|
||||
const QJsonObject obj = entry.toObject();
|
||||
const QString symbol = obj.value(QStringLiteral("symbol")).toString();
|
||||
|
||||
// TOKENS_CONFIG entries may give definitionId/holding as hex or
|
||||
// base58 (the wallet/runbook display base58) — normalize both to
|
||||
// lowercase hex here so every downstream consumer (resolvePool,
|
||||
// swapExactInput, and the QML's hex comparisons) can assume hex.
|
||||
const QString definitionId =
|
||||
normalizeAccountId(obj.value(QStringLiteral("definitionId")).toString());
|
||||
const QString holding = normalizeAccountId(obj.value(QStringLiteral("holding")).toString());
|
||||
if (definitionId.isEmpty() || holding.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend::tokenList: skipping token" << symbol
|
||||
<< "— cannot normalize definitionId/holding to hex (not valid hex or base58)";
|
||||
continue;
|
||||
}
|
||||
|
||||
QVariantMap token;
|
||||
token[QStringLiteral("symbol")] = symbol;
|
||||
token[QStringLiteral("name")] = obj.value(QStringLiteral("name")).toString();
|
||||
token[QStringLiteral("definitionId")] = definitionId;
|
||||
token[QStringLiteral("holding")] = holding;
|
||||
token[QStringLiteral("decimals")] = obj.value(QStringLiteral("decimals")).toInt();
|
||||
out.append(token);
|
||||
}
|
||||
return out;
|
||||
return m_logos->amm_module.tokenList();
|
||||
}
|
||||
|
||||
+20
-43
@@ -12,20 +12,23 @@
|
||||
|
||||
#include "rep_AmmUiBackend_source.h"
|
||||
|
||||
#include "ActiveNetwork.h"
|
||||
#include "WalletAccountModel.h"
|
||||
|
||||
class LogosAPI;
|
||||
struct LogosModules;
|
||||
class AmmClient;
|
||||
class LogosWalletProvider;
|
||||
class NewPositionRuntime;
|
||||
class SwapRuntime;
|
||||
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.
|
||||
//
|
||||
// The AMM business logic (pool resolution, swaps, add-liquidity) lives in the
|
||||
// amm_module core Logos module; this backend owns only wallet-session lifecycle
|
||||
// and forwards every AMM slot to modules().amm_module. The one exception is
|
||||
// creating a fresh LP holding for add-liquidity: that mutates the wallet keyset,
|
||||
// so it stays here (via the wallet provider, which keeps the account model and
|
||||
// on-disk storage coherent) and its id is handed to the module's submit.
|
||||
class AmmUiBackend : public AmmUiBackendSimpleSource {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(WalletAccountModel* accountModel READ accountModel CONSTANT)
|
||||
@@ -53,61 +56,35 @@ public slots:
|
||||
bool openExisting() override;
|
||||
void disconnectWallet() override;
|
||||
|
||||
// AMM
|
||||
// AMM — all forwarded to the amm_module core module.
|
||||
QVariantMap resolvePool(QString defAHex, QString defBHex) override;
|
||||
QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal) override;
|
||||
// Reads the token list from TOKENS_CONFIG (see AmmUiBackend.cpp) so the
|
||||
// Swap UI's token picker is config-driven instead of hardcoded.
|
||||
// Reads the token list from TOKENS_CONFIG (via the module) so the Swap UI's
|
||||
// token picker is config-driven instead of hardcoded.
|
||||
QVariantList tokenList() override;
|
||||
|
||||
private:
|
||||
void syncWalletState();
|
||||
// Publishes the new-position context PROP: a local "loading" placeholder
|
||||
// until wallet state (and thus the module connection) is ready, then the
|
||||
// module's newPositionContext for the current hints.
|
||||
void publishNetworkContext();
|
||||
|
||||
// Builds the new-position network context from the same sources the Swap
|
||||
// view uses: ammProgramId from $AMM_PROGRAM_BIN, tokenIds from
|
||||
// $TOKENS_CONFIG. status is "ready" once AMM_PROGRAM_BIN resolves, else
|
||||
// "config_missing". There is no separate network config or channel probe.
|
||||
ActiveNetworkSnapshot networkSnapshot();
|
||||
|
||||
// 64-char lowercase-hex AMM program id derived from $AMM_PROGRAM_BIN (empty
|
||||
// if unset/unreadable); matches swapExactInput's program-id encoding.
|
||||
QString ammProgramIdHex();
|
||||
|
||||
// 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);
|
||||
|
||||
// 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();
|
||||
|
||||
LogosAPI* m_logosAPI;
|
||||
// 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.
|
||||
// Handle for the amm_module core module (resolvePool / swapExactInput /
|
||||
// tokenList / new-position). The module wraps the amm_client brain and
|
||||
// reaches the shared wallet through its own logos_execution_zone dependency;
|
||||
// this backend keeps a thin LogosModules over the same LogosAPI as the
|
||||
// wallet provider so both resolve that one shared wallet instance.
|
||||
std::unique_ptr<LogosModules> m_logos;
|
||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||
std::unique_ptr<WalletController> m_walletController;
|
||||
std::unique_ptr<AmmClient> m_ammClient;
|
||||
std::unique_ptr<NewPositionRuntime> m_newPosition;
|
||||
std::unique_ptr<SwapRuntime> m_swap;
|
||||
|
||||
// Sticky new-position hints (recent/resolved token ids) so a bare
|
||||
// republish (wallet-state change) keeps the user's last selection.
|
||||
QVariantMap m_newPositionHints;
|
||||
|
||||
// Network context is derived from $AMM_PROGRAM_BIN + $TOKENS_CONFIG, which are
|
||||
// fixed for the process lifetime — resolve them once and cache. networkSnapshot()
|
||||
// runs on the hot path (every quote) and from inside runtime callbacks, and
|
||||
// tokenList() makes remote base58 conversions, so it must not recompute each call.
|
||||
bool m_networkResolved = false;
|
||||
QString m_ammProgramIdCache;
|
||||
QStringList m_tokenIdsCache;
|
||||
};
|
||||
|
||||
#endif // AMM_UI_BACKEND_H
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
#include "NewPositionRuntime.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QScopedValueRollback>
|
||||
#include <libbase58.h>
|
||||
|
||||
#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<char, BASE58_BUFFER_SIZE> encoded {};
|
||||
std::size_t size = encoded.size();
|
||||
if (!b58enc(encoded.data(), &size, bytes.constData(),
|
||||
static_cast<std::size_t>(bytes.size()))) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromLatin1(
|
||||
encoded.data(), static_cast<qsizetype>(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<bool> jsonBoolList(const QJsonArray& values)
|
||||
{
|
||||
QVector<bool> result;
|
||||
result.reserve(values.size());
|
||||
for (const QJsonValue& value : values)
|
||||
result.append(value.toBool());
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<quint32> jsonUIntList(const QJsonArray& values)
|
||||
{
|
||||
QVector<quint32> result;
|
||||
result.reserve(values.size());
|
||||
for (const QJsonValue& value : values)
|
||||
result.append(static_cast<quint32>(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<WalletAccountRead>& 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<bool> 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<bool> signingRequirements = jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray());
|
||||
const QVector<quint32> 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<qulonglong>(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();
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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;
|
||||
};
|
||||
@@ -1,169 +0,0 @@
|
||||
#include "SwapRuntime.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
#include "AmmClient.h"
|
||||
#include "WalletProvider.h"
|
||||
|
||||
namespace {
|
||||
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;
|
||||
}
|
||||
|
||||
QStringList jsonStringList(const QJsonArray& values)
|
||||
{
|
||||
QStringList result;
|
||||
result.reserve(values.size());
|
||||
for (const QJsonValue& value : values)
|
||||
result.append(value.toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<bool> jsonBoolList(const QJsonArray& values)
|
||||
{
|
||||
QVector<bool> result;
|
||||
result.reserve(values.size());
|
||||
for (const QJsonValue& value : values)
|
||||
result.append(value.toBool());
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<quint32> jsonUIntList(const QJsonArray& values)
|
||||
{
|
||||
QVector<quint32> result;
|
||||
result.reserve(values.size());
|
||||
for (const QJsonValue& value : values)
|
||||
result.append(static_cast<quint32>(value.toInteger()));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
SwapRuntime::SwapRuntime(WalletProvider* wallet, AmmClient* client)
|
||||
: m_wallet(wallet),
|
||||
m_client(client)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject SwapRuntime::readConfig(const ActiveNetworkSnapshot& network) const
|
||||
{
|
||||
const AmmClientResult configResult = m_client->configId(
|
||||
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
|
||||
if (!configResult.ok)
|
||||
return {};
|
||||
return accountReadJson(m_wallet->readPublicAccount(
|
||||
configResult.value.value(QStringLiteral("configId")).toString()));
|
||||
}
|
||||
|
||||
QVariantMap SwapRuntime::resolvePool(const QString& tokenInId,
|
||||
const QString& tokenOutId,
|
||||
const ActiveNetworkSnapshot& network)
|
||||
{
|
||||
const QVariantMap absent { { QStringLiteral("exists"), false } };
|
||||
// Attaches a diagnostic code the Swap UI surfaces verbatim (SwapCard.qml:
|
||||
// pool.error). The normal "no pool / no liquidity yet" state stays
|
||||
// code-less — that's the bare `absent` / resolve_pool `{exists:false}`
|
||||
// below, which the UI renders as its neutral "no pool" message, not an error.
|
||||
const auto failure = [](const QString& code) {
|
||||
return QVariantMap {
|
||||
{ QStringLiteral("exists"), false },
|
||||
{ QStringLiteral("error"), code },
|
||||
};
|
||||
};
|
||||
|
||||
// Network still resolving AMM_PROGRAM_BIN: transient startup state, not a
|
||||
// diagnostic — surface nothing so the UI keeps its "loading" affordance.
|
||||
if (network.status != QStringLiteral("ready"))
|
||||
return absent;
|
||||
|
||||
// readConfig returns {} only when the config_id op itself fails (a client
|
||||
// bug, not a chain state) — as opposed to the config account merely being
|
||||
// unreadable, which surfaces as swap_pair's `config_unavailable` below.
|
||||
const QJsonObject config = readConfig(network);
|
||||
if (config.isEmpty())
|
||||
return failure(QStringLiteral("backend_error"));
|
||||
|
||||
const AmmClientResult pairResult = m_client->swapPair(QJsonObject {
|
||||
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||
{ QStringLiteral("tokenInId"), tokenInId },
|
||||
{ QStringLiteral("tokenOutId"), tokenOutId },
|
||||
{ QStringLiteral("config"), config },
|
||||
});
|
||||
if (!pairResult.ok)
|
||||
return failure(QStringLiteral("backend_error"));
|
||||
if (pairResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
|
||||
// swap_pair reports `config_unavailable` when the AMM config account is
|
||||
// missing/uninitialized on this network, `same_token_pair` for an
|
||||
// invalid pair, etc. Propagate its code rather than flattening to
|
||||
// "no pool", so a misconfigured network is distinguishable from an
|
||||
// empty one.
|
||||
const QString code = pairResult.value.value(QStringLiteral("code")).toString();
|
||||
return failure(code.isEmpty() ? QStringLiteral("backend_error") : code);
|
||||
}
|
||||
|
||||
const QJsonObject pool = accountReadJson(m_wallet->readPublicAccount(
|
||||
pairResult.value.value(QStringLiteral("poolId")).toString()));
|
||||
const AmmClientResult resolveResult =
|
||||
m_client->resolvePool(QJsonObject { { QStringLiteral("pool"), pool } });
|
||||
if (!resolveResult.ok)
|
||||
return failure(QStringLiteral("backend_error"));
|
||||
return resolveResult.value.toVariantMap();
|
||||
}
|
||||
|
||||
QString SwapRuntime::swap(const QString& tokenInId,
|
||||
const QString& tokenOutId,
|
||||
const QString& userInputHoldingId,
|
||||
const QString& userOutputHoldingId,
|
||||
const QString& amountInDecimal,
|
||||
const QString& minOutDecimal,
|
||||
const QString& deadlineMs,
|
||||
const ActiveNetworkSnapshot& network,
|
||||
bool walletOpen)
|
||||
{
|
||||
if (network.status != QStringLiteral("ready") || !walletOpen)
|
||||
return {};
|
||||
|
||||
const QJsonObject config = readConfig(network);
|
||||
if (config.isEmpty())
|
||||
return {};
|
||||
|
||||
const AmmClientResult planResult = m_client->swapPlan(QJsonObject {
|
||||
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||
{ QStringLiteral("tokenInId"), tokenInId },
|
||||
{ QStringLiteral("tokenOutId"), tokenOutId },
|
||||
{ QStringLiteral("config"), config },
|
||||
{ QStringLiteral("userInputHoldingId"), userInputHoldingId },
|
||||
{ QStringLiteral("userOutputHoldingId"), userOutputHoldingId },
|
||||
{ QStringLiteral("amountIn"), amountInDecimal },
|
||||
{ QStringLiteral("minOut"), minOutDecimal },
|
||||
{ QStringLiteral("deadlineMs"), deadlineMs },
|
||||
});
|
||||
if (!planResult.ok
|
||||
|| planResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ready"))
|
||||
return {};
|
||||
|
||||
const QJsonObject plan = planResult.value;
|
||||
const WalletSubmission submission = m_wallet->submitPublicTransaction({
|
||||
plan.value(QStringLiteral("programId")).toString(),
|
||||
jsonStringList(plan.value(QStringLiteral("accountIds")).toArray()),
|
||||
jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()),
|
||||
jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()),
|
||||
});
|
||||
if (!submission.accepted())
|
||||
return {};
|
||||
return submission.nativeHash;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "ActiveNetwork.h"
|
||||
|
||||
class AmmClient;
|
||||
class WalletProvider;
|
||||
|
||||
// Off-chain orchestration for the Swap view: reads accounts through the wallet,
|
||||
// drives the amm_client swap ops, and submits the swap transaction. The AMM
|
||||
// domain logic lives in the amm_client crate; this class only glues account
|
||||
// reads and submission to it. Mirrors NewPositionRuntime.
|
||||
class SwapRuntime {
|
||||
public:
|
||||
SwapRuntime(WalletProvider* wallet, AmmClient* client);
|
||||
|
||||
// { exists, reserveA, reserveB, feeBps } for the (tokenIn, tokenOut) pool.
|
||||
// reserveA/reserveB follow the pool's canonical def order (the caller maps
|
||||
// them to sell/buy). exists=false when the pool is absent or has no liquidity.
|
||||
QVariantMap resolvePool(const QString& tokenInId,
|
||||
const QString& tokenOutId,
|
||||
const ActiveNetworkSnapshot& network);
|
||||
|
||||
// Builds and submits a SwapExactInput transaction; returns the native tx
|
||||
// hash on success, an empty string on any failure.
|
||||
QString swap(const QString& tokenInId,
|
||||
const QString& tokenOutId,
|
||||
const QString& userInputHoldingId,
|
||||
const QString& userOutputHoldingId,
|
||||
const QString& amountInDecimal,
|
||||
const QString& minOutDecimal,
|
||||
const QString& deadlineMs,
|
||||
const ActiveNetworkSnapshot& network,
|
||||
bool walletOpen);
|
||||
|
||||
private:
|
||||
// Derives the config account id (config_id) and reads it. Returns an empty
|
||||
// object only when the config_id op itself fails.
|
||||
QJsonObject readConfig(const ActiveNetworkSnapshot& network) const;
|
||||
|
||||
WalletProvider* m_wallet;
|
||||
AmmClient* m_client;
|
||||
};
|
||||
@@ -1,230 +0,0 @@
|
||||
#include "AmmClient.h"
|
||||
#include "NewPositionRuntime.h"
|
||||
#include "WalletProvider.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user