lez-programs/apps/amm/src/AmmUiBackend.cpp
Ricardo Guilherme Schmidt 21f6d818a7
feat(amm-ui): complete new position liquidity flow
Implement one create-pool and add-liquidity workflow. Add searchable token resolution, direct opening-price and deposit editing, optimistic pool activation, and base58 transaction IDs.

Isolate network, wallet, AMM client, and runtime boundaries. Stabilize quote, submission, refresh, and pool-probe state while consolidating liquidity tests and removing obsolete liquidity paths.
2026-07-15 15:07:36 -03:00

261 lines
8.3 KiB
C++

#include "AmmUiBackend.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <QUrl>
#include "AmmClient.h"
#include "LogosWalletProvider.h"
#include "NewPositionRuntime.h"
#include "WalletController.h"
#include "logos_api.h"
namespace {
const int CHECKPOINT_BLOCK_ID = 10;
const int BLOCK_HASH_OFFSET = 40;
const int BLOCK_HASH_SIZE = 32;
QByteArray jsonRpcBody(const QString& method, const QJsonArray& params)
{
return QJsonDocument(QJsonObject {
{ QStringLiteral("jsonrpc"), QStringLiteral("2.0") },
{ QStringLiteral("id"), 1 },
{ QStringLiteral("method"), method },
{ QStringLiteral("params"), params },
}).toJson(QJsonDocument::Compact);
}
QString blockHashFromResponse(const QByteArray& payload)
{
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject())
return {};
const QByteArray block =
QByteArray::fromBase64(document.object().value(QStringLiteral("result")).toString().toLatin1());
if (block.size() < BLOCK_HASH_OFFSET + BLOCK_HASH_SIZE)
return {};
return QString::fromLatin1(block.mid(BLOCK_HASH_OFFSET, BLOCK_HASH_SIZE).toHex());
}
QString channelIdFromResponse(const QByteArray& payload)
{
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject())
return {};
const QString channel = document.object().value(QStringLiteral("result")).toString();
return ActiveNetwork::isValidIdentity(channel) ? channel : QString();
}
}
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
: AmmUiBackendSimpleSource(parent),
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
m_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_net(new QNetworkAccessManager(this))
{
setWalletStateReady(false);
m_network.load();
setNewPositionContext(m_newPosition->context(
QVariantMap(), m_network.snapshot(), false, false));
connect(m_walletController.get(), &WalletController::stateChanged,
this, &AmmUiBackend::syncWalletState);
syncWalletState();
m_walletController->start();
QTimer::singleShot(0, this, [this]() {
setWalletStateReady(true);
syncWalletState();
});
}
AmmUiBackend::~AmmUiBackend() = default;
WalletAccountModel* AmmUiBackend::accountModel() const
{
return m_walletController->accountModel();
}
QString AmmUiBackend::createNewDefault(QString password)
{
setWalletStateReady(false);
const QString mnemonic = m_walletController->createDefaultWallet(password);
setWalletStateReady(true);
syncWalletState();
return mnemonic;
}
QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
{
setWalletStateReady(false);
const QString mnemonic =
m_walletController->createWallet(configPath, storagePath, password);
setWalletStateReady(true);
syncWalletState();
return mnemonic;
}
bool AmmUiBackend::openExisting()
{
setWalletStateReady(false);
const bool opened = m_walletController->open();
setWalletStateReady(true);
syncWalletState();
return opened;
}
void AmmUiBackend::disconnectWallet()
{
m_walletController->disconnect();
setWalletStateReady(true);
m_newPosition->clearWalletAccounts();
refreshNewPositionContext(QVariantMap());
}
QString AmmUiBackend::createAccountPublic()
{
return m_walletController->createAccount(true);
}
QString AmmUiBackend::createAccountPrivate()
{
return m_walletController->createAccount(false);
}
void AmmUiBackend::refreshAccounts()
{
m_walletController->refresh();
}
void AmmUiBackend::refreshBalances()
{
m_walletController->refresh();
}
QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
{
return m_walletController->balance(accountIdHex, isPublic);
}
void AmmUiBackend::refreshNewPositionContext(QVariantMap request)
{
const bool refreshWalletAccounts =
request.take(QStringLiteral("refreshWalletAccounts")).toBool();
if (request.contains(QStringLiteral("recentTokenIds"))
|| request.contains(QStringLiteral("resolvedTokenIds"))) {
m_newPositionHints = request;
}
else {
request = m_newPositionHints;
}
if (m_network.status() == QStringLiteral("network_unknown"))
probeNetworkIdentity();
setNewPositionContext(m_newPosition->context(
request, m_network.snapshot(), isWalletOpen(), refreshWalletAccounts));
}
QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request)
{
return m_newPosition->quote(request, m_network.snapshot(), isWalletOpen());
}
QVariantMap AmmUiBackend::submitNewPosition(QVariantMap request, QString quoteHash)
{
return m_newPosition->submit(
request, quoteHash, m_network.snapshot(), isWalletOpen());
}
void AmmUiBackend::syncWalletState()
{
const WalletUiState& state = m_walletController->state();
const bool walletWasOpen = isWalletOpen();
const bool wasReachable = sequencerReachable();
const QString previousAddress = sequencerAddr();
setIsWalletOpen(state.isWalletOpen);
setWalletExists(state.walletExists);
setConfigPath(state.configPath);
setStoragePath(state.storagePath);
setWalletHome(state.walletHome);
setLastSyncedBlock(state.lastSyncedBlock);
setCurrentBlockHeight(state.currentBlockHeight);
setSequencerAddr(state.sequencerAddress);
setSequencerReachable(state.sequencerReachable);
const bool addressChanged = previousAddress != state.sequencerAddress;
if (addressChanged)
m_network.sequencerChanged(!state.sequencerAddress.isEmpty());
if (addressChanged || wasReachable != state.sequencerReachable) {
m_network.reachabilityChanged(state.sequencerReachable, wasReachable);
}
if (walletWasOpen && !state.isWalletOpen)
m_newPosition->clearWalletAccounts();
publishNetworkContext();
if (state.sequencerReachable && m_network.needsIdentityProbe())
probeNetworkIdentity();
}
void AmmUiBackend::probeNetworkIdentity()
{
if (m_identityProbeInFlight
|| !m_network.isConfigured()
|| sequencerAddr().isEmpty()) {
return;
}
m_identityProbeInFlight = true;
m_network.beginIdentityProbe();
publishNetworkContext();
const QString address = sequencerAddr();
const bool devnet = m_network.isDevnet();
const QString method = devnet
? QStringLiteral("getChannelId")
: QStringLiteral("getBlock");
const QJsonArray params = devnet
? QJsonArray()
: QJsonArray { CHECKPOINT_BLOCK_ID };
QNetworkRequest request{QUrl(address)};
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
request.setTransferTimeout(4000);
QNetworkReply* reply = m_net->post(request, jsonRpcBody(method, params));
connect(reply, &QNetworkReply::finished, this, [this, reply, address, devnet]() {
m_identityProbeInFlight = false;
if (address != sequencerAddr()) {
reply->deleteLater();
probeNetworkIdentity();
return;
}
if (!sequencerReachable()) {
reply->deleteLater();
return;
}
const QByteArray payload = reply->readAll();
const QString actual = devnet
? channelIdFromResponse(payload)
: blockHashFromResponse(payload);
m_network.finishIdentityProbe(actual);
reply->deleteLater();
publishNetworkContext();
});
}
void AmmUiBackend::publishNetworkContext()
{
setNewPositionContext(m_newPosition->context(
m_newPositionHints, m_network.snapshot(), isWalletOpen(), false));
}