mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
1106 lines
48 KiB
C++
1106 lines
48 KiB
C++
#include "AmmUiBackend.h"
|
|
|
|
#include <QClipboard>
|
|
#include <QCoreApplication>
|
|
#include <QCryptographicHash>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QGuiApplication>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QSettings>
|
|
#include <QTimer>
|
|
#include <QUrl>
|
|
|
|
#include "logos_api.h"
|
|
#include "logos_sdk.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
const char SETTINGS_ORG[] = "Logos";
|
|
const char SETTINGS_APP[] = "AmmUI";
|
|
// Sticky "user pressed Disconnect" flag so the wallet stays locked across
|
|
// relaunches until the user reconnects.
|
|
const char DISCONNECTED_KEY[] = "disconnected";
|
|
const int WALLET_FFI_SUCCESS = 0;
|
|
|
|
// Wallet home env override. Mirrors LEZ's own var so the app shares the
|
|
// canonical wallet (~/.lee/wallet) used by the wallet UI and other apps.
|
|
const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
|
|
|
|
// Normalise file:// URLs and OS paths to a plain local path.
|
|
QString toLocalPath(const QString& path) {
|
|
if (path.startsWith("file://") || path.contains("/"))
|
|
return QUrl::fromUserInput(path).toLocalFile();
|
|
return path;
|
|
}
|
|
|
|
QString stableId(const QString& key)
|
|
{
|
|
const QByteArray digest =
|
|
QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Sha256).toHex();
|
|
return QString::fromLatin1(digest);
|
|
}
|
|
|
|
QString shortId(const QString& id)
|
|
{
|
|
if (id.length() <= 14)
|
|
return id;
|
|
return id.left(6) + QStringLiteral("...") + id.right(4);
|
|
}
|
|
|
|
double parsePositiveAmount(QString value)
|
|
{
|
|
value.remove(QLatin1Char(','));
|
|
bool ok = false;
|
|
const double parsed = value.toDouble(&ok);
|
|
return ok && std::isfinite(parsed) && parsed > 0.0 ? parsed : 0.0;
|
|
}
|
|
|
|
QString formatAmount(double amount)
|
|
{
|
|
amount = std::max(0.0, amount);
|
|
const int decimals = amount >= 1000.0 ? 2 : amount >= 1.0 ? 4 : 6;
|
|
QString text = QString::number(amount, 'f', decimals);
|
|
while (text.contains(QLatin1Char('.')) && text.endsWith(QLatin1Char('0')))
|
|
text.chop(1);
|
|
if (text.endsWith(QLatin1Char('.')))
|
|
text.chop(1);
|
|
return text;
|
|
}
|
|
|
|
QString formatTokenAmount(double amount, const QString& symbol)
|
|
{
|
|
return QStringLiteral("%1 %2").arg(formatAmount(amount), symbol);
|
|
}
|
|
|
|
QVariantMap amountValue(double amount, const QString& symbol)
|
|
{
|
|
QVariantMap value;
|
|
value.insert(QStringLiteral("value"), amount);
|
|
value.insert(QStringLiteral("input"), formatAmount(amount));
|
|
value.insert(QStringLiteral("display"), formatTokenAmount(amount, symbol));
|
|
value.insert(QStringLiteral("symbol"), symbol);
|
|
return value;
|
|
}
|
|
|
|
QString feeLabel(int bps)
|
|
{
|
|
if (bps == 1)
|
|
return QStringLiteral("0.01%");
|
|
if (bps == 5)
|
|
return QStringLiteral("0.05%");
|
|
if (bps == 30)
|
|
return QStringLiteral("0.30%");
|
|
if (bps == 100)
|
|
return QStringLiteral("1.00%");
|
|
return QStringLiteral("%1 bps").arg(bps);
|
|
}
|
|
|
|
bool isSupportedFeeTier(int bps)
|
|
{
|
|
return bps == 1 || bps == 5 || bps == 30 || bps == 100;
|
|
}
|
|
|
|
QVariantList feeTiers()
|
|
{
|
|
QVariantList tiers;
|
|
for (const int bps : {1, 5, 25, 30, 100}) {
|
|
QVariantMap tier;
|
|
tier.insert(QStringLiteral("bps"), bps);
|
|
tier.insert(QStringLiteral("label"), feeLabel(bps));
|
|
tier.insert(QStringLiteral("supported"), isSupportedFeeTier(bps));
|
|
tiers.append(tier);
|
|
}
|
|
return tiers;
|
|
}
|
|
|
|
double devnetBalance(const QString& symbol)
|
|
{
|
|
if (symbol == QStringLiteral("USDC"))
|
|
return 12450.0;
|
|
if (symbol == QStringLiteral("LOGOS"))
|
|
return 850000.0;
|
|
if (symbol == QStringLiteral("WETH"))
|
|
return 3.25;
|
|
return 0.0;
|
|
}
|
|
|
|
QString accentForSymbol(const QString& symbol)
|
|
{
|
|
if (symbol == QStringLiteral("USDC"))
|
|
return QStringLiteral("#2E7CF6");
|
|
if (symbol == QStringLiteral("LOGOS"))
|
|
return QStringLiteral("#F26A21");
|
|
if (symbol == QStringLiteral("WETH"))
|
|
return QStringLiteral("#B7C2D8");
|
|
return QStringLiteral("#343434");
|
|
}
|
|
|
|
QVariantMap devnetHolding(const QString& owner, const QString& symbol, const QString& name)
|
|
{
|
|
const double balance = devnetBalance(symbol);
|
|
const QString definitionId = stableId(QStringLiteral("devnet:token-definition:%1").arg(symbol));
|
|
QVariantMap holding;
|
|
holding.insert(QStringLiteral("symbol"), symbol);
|
|
holding.insert(QStringLiteral("name"), name);
|
|
holding.insert(QStringLiteral("definitionId"), definitionId);
|
|
holding.insert(QStringLiteral("holdingId"),
|
|
stableId(QStringLiteral("devnet:token-holding:%1:%2").arg(owner, symbol)));
|
|
holding.insert(QStringLiteral("balance"), balance);
|
|
holding.insert(QStringLiteral("balanceText"), formatTokenAmount(balance, symbol));
|
|
holding.insert(QStringLiteral("accent"), accentForSymbol(symbol));
|
|
return holding;
|
|
}
|
|
|
|
QVariantList devnetHoldings(const QString& owner)
|
|
{
|
|
if (owner.isEmpty())
|
|
return {};
|
|
|
|
QVariantList holdings;
|
|
holdings.append(devnetHolding(owner, QStringLiteral("USDC"), QStringLiteral("USD Coin")));
|
|
holdings.append(devnetHolding(owner, QStringLiteral("LOGOS"), QStringLiteral("Logos")));
|
|
holdings.append(devnetHolding(owner, QStringLiteral("WETH"), QStringLiteral("Wrapped Ether")));
|
|
return holdings;
|
|
}
|
|
|
|
QVariantMap holdingBySymbol(const QVariantList& holdings, const QString& symbol)
|
|
{
|
|
for (const QVariant& item : holdings) {
|
|
const QVariantMap holding = item.toMap();
|
|
if (holding.value(QStringLiteral("symbol")).toString() == symbol)
|
|
return holding;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
QString unorderedPairKey(const QString& symbolA, const QString& symbolB)
|
|
{
|
|
return symbolA < symbolB
|
|
? QStringLiteral("%1/%2").arg(symbolA, symbolB)
|
|
: QStringLiteral("%1/%2").arg(symbolB, symbolA);
|
|
}
|
|
|
|
double activeRatio(const QString& symbolA, const QString& symbolB)
|
|
{
|
|
if (symbolA == QStringLiteral("USDC") && symbolB == QStringLiteral("LOGOS"))
|
|
return 8.0;
|
|
if (symbolA == QStringLiteral("LOGOS") && symbolB == QStringLiteral("USDC"))
|
|
return 0.125;
|
|
return 1.0;
|
|
}
|
|
|
|
double defaultInitialPrice(const QString& symbolA, const QString& symbolB)
|
|
{
|
|
if (symbolA == QStringLiteral("USDC") && symbolB == QStringLiteral("WETH"))
|
|
return 2500.0;
|
|
if (symbolA == QStringLiteral("WETH") && symbolB == QStringLiteral("USDC"))
|
|
return 0.0004;
|
|
return 1.0;
|
|
}
|
|
|
|
QVariantMap poolContext(const QString& symbolA, const QString& symbolB)
|
|
{
|
|
QVariantMap context;
|
|
context.insert(QStringLiteral("poolStatus"), QStringLiteral("unavailable_pool"));
|
|
context.insert(QStringLiteral("statusLabel"), QStringLiteral("Unavailable"));
|
|
context.insert(QStringLiteral("detail"), QStringLiteral("Choose two different assets from the active account."));
|
|
context.insert(QStringLiteral("instruction"), QString());
|
|
context.insert(QStringLiteral("storedFeeBps"), 0);
|
|
context.insert(QStringLiteral("poolId"), QString());
|
|
context.insert(QStringLiteral("priceText"), QString());
|
|
context.insert(QStringLiteral("reserveText"), QString());
|
|
|
|
if (symbolA.isEmpty() || symbolB.isEmpty() || symbolA == symbolB)
|
|
return context;
|
|
|
|
const QString pairKey = unorderedPairKey(symbolA, symbolB);
|
|
const QString poolId = stableId(QStringLiteral("devnet:amm-pool:%1").arg(pairKey));
|
|
context.insert(QStringLiteral("poolId"), poolId);
|
|
|
|
if (pairKey == QStringLiteral("LOGOS/USDC")) {
|
|
context.insert(QStringLiteral("poolStatus"), QStringLiteral("active_pool"));
|
|
context.insert(QStringLiteral("statusLabel"), QStringLiteral("Active pool"));
|
|
context.insert(QStringLiteral("detail"), QStringLiteral("Deposits are quoted against the existing pool ratio. Nonmatching fee tiers are locked."));
|
|
context.insert(QStringLiteral("instruction"), QStringLiteral("add_liquidity"));
|
|
context.insert(QStringLiteral("storedFeeBps"), 30);
|
|
context.insert(QStringLiteral("priceText"),
|
|
symbolA == QStringLiteral("USDC")
|
|
? QStringLiteral("1 USDC = 8 LOGOS")
|
|
: QStringLiteral("1 LOGOS = 0.125 USDC"));
|
|
context.insert(QStringLiteral("reserveText"),
|
|
symbolA == QStringLiteral("USDC")
|
|
? QStringLiteral("1,250,000 USDC / 10,000,000 LOGOS")
|
|
: QStringLiteral("10,000,000 LOGOS / 1,250,000 USDC"));
|
|
return context;
|
|
}
|
|
|
|
if (pairKey == QStringLiteral("USDC/WETH")) {
|
|
context.insert(QStringLiteral("poolStatus"), QStringLiteral("missing_pool"));
|
|
context.insert(QStringLiteral("statusLabel"), QStringLiteral("Missing pool"));
|
|
context.insert(QStringLiteral("detail"), QStringLiteral("Set the initial price first, then scale both deposits together."));
|
|
context.insert(QStringLiteral("instruction"), QStringLiteral("new_definition"));
|
|
context.insert(QStringLiteral("priceText"), QStringLiteral("No reserves yet"));
|
|
context.insert(QStringLiteral("reserveText"), QStringLiteral("Pool account is empty"));
|
|
return context;
|
|
}
|
|
|
|
context.insert(QStringLiteral("detail"), QStringLiteral("A pool account exists, but it cannot be quoted safely for this devnet state."));
|
|
context.insert(QStringLiteral("priceText"), QStringLiteral("Quote disabled"));
|
|
context.insert(QStringLiteral("reserveText"), QStringLiteral("Unsupported stored pool state"));
|
|
return context;
|
|
}
|
|
|
|
QString quoteHash(const QVariantMap& request)
|
|
{
|
|
const QStringList parts = {
|
|
request.value(QStringLiteral("tokenA")).toString(),
|
|
request.value(QStringLiteral("tokenB")).toString(),
|
|
QString::number(request.value(QStringLiteral("feeBps")).toInt()),
|
|
request.value(QStringLiteral("editedSide")).toString(),
|
|
request.value(QStringLiteral("amountA")).toString(),
|
|
request.value(QStringLiteral("amountB")).toString(),
|
|
request.value(QStringLiteral("initialPrice")).toString(),
|
|
QString::number(request.value(QStringLiteral("depositScale")).toInt()),
|
|
QString::number(request.value(QStringLiteral("slippageBps")).toInt()),
|
|
};
|
|
const QByteArray digest =
|
|
QCryptographicHash::hash(parts.join(QLatin1Char('|')).toUtf8(),
|
|
QCryptographicHash::Sha256).toHex();
|
|
return QStringLiteral("sha256-%1").arg(QString::fromLatin1(digest));
|
|
}
|
|
|
|
QVariantMap accountChange(const QString& role, const QString& id, const QString& action)
|
|
{
|
|
QVariantMap change;
|
|
change.insert(QStringLiteral("role"), role);
|
|
change.insert(QStringLiteral("id"), id);
|
|
change.insert(QStringLiteral("action"), action);
|
|
return change;
|
|
}
|
|
|
|
QVariantList accountChanges(const QVariantMap& request, const QVariantMap& context)
|
|
{
|
|
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
|
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
|
const QString poolId = context.value(QStringLiteral("poolId")).toString();
|
|
const bool missingPool =
|
|
context.value(QStringLiteral("poolStatus")).toString() == QStringLiteral("missing_pool");
|
|
|
|
QVariantList changes;
|
|
changes.append(accountChange(QStringLiteral("Config"),
|
|
stableId(QStringLiteral("devnet:amm-config")),
|
|
QStringLiteral("Read")));
|
|
changes.append(accountChange(QStringLiteral("Pool"),
|
|
poolId,
|
|
missingPool ? QStringLiteral("Create") : QStringLiteral("Update")));
|
|
changes.append(accountChange(QStringLiteral("Vault A"),
|
|
stableId(QStringLiteral("devnet:vault:%1:%2").arg(poolId, tokenA)),
|
|
missingPool ? QStringLiteral("Update or create") : QStringLiteral("Update")));
|
|
changes.append(accountChange(QStringLiteral("Vault B"),
|
|
stableId(QStringLiteral("devnet:vault:%1:%2").arg(poolId, tokenB)),
|
|
missingPool ? QStringLiteral("Update or create") : QStringLiteral("Update")));
|
|
if (missingPool) {
|
|
changes.append(accountChange(QStringLiteral("LP definition"),
|
|
stableId(QStringLiteral("devnet:lp-definition:%1").arg(poolId)),
|
|
QStringLiteral("Create")));
|
|
changes.append(accountChange(QStringLiteral("LP lock holding"),
|
|
stableId(QStringLiteral("devnet:lp-lock:%1").arg(poolId)),
|
|
QStringLiteral("Create")));
|
|
}
|
|
changes.append(accountChange(QStringLiteral("User LP holding"),
|
|
stableId(QStringLiteral("devnet:user-lp:%1").arg(poolId)),
|
|
QStringLiteral("Update or create")));
|
|
changes.append(accountChange(QStringLiteral("Current tick"),
|
|
stableId(QStringLiteral("devnet:current-tick:%1").arg(poolId)),
|
|
missingPool ? QStringLiteral("Create") : QStringLiteral("Update")));
|
|
changes.append(accountChange(QStringLiteral("Clock"),
|
|
stableId(QStringLiteral("devnet:clock:canonical")),
|
|
QStringLiteral("Read")));
|
|
return changes;
|
|
}
|
|
|
|
QVariantMap baseQuote(const QVariantMap& context, const QVariantMap& request)
|
|
{
|
|
QVariantMap quote;
|
|
quote.insert(QStringLiteral("poolStatus"), context.value(QStringLiteral("poolStatus")));
|
|
quote.insert(QStringLiteral("statusLabel"), context.value(QStringLiteral("statusLabel")));
|
|
quote.insert(QStringLiteral("statusDetail"), context.value(QStringLiteral("detail")));
|
|
quote.insert(QStringLiteral("instruction"), context.value(QStringLiteral("instruction")));
|
|
quote.insert(QStringLiteral("storedFeeBps"), context.value(QStringLiteral("storedFeeBps")));
|
|
quote.insert(QStringLiteral("feeBps"), request.value(QStringLiteral("feeBps")).toInt());
|
|
quote.insert(QStringLiteral("feeLabel"), feeLabel(request.value(QStringLiteral("feeBps")).toInt()));
|
|
quote.insert(QStringLiteral("quoteHash"), quoteHash(request));
|
|
|
|
QVariantMap pool;
|
|
pool.insert(QStringLiteral("id"), context.value(QStringLiteral("poolId")));
|
|
pool.insert(QStringLiteral("priceText"), context.value(QStringLiteral("priceText")));
|
|
pool.insert(QStringLiteral("reserveText"), context.value(QStringLiteral("reserveText")));
|
|
quote.insert(QStringLiteral("pool"), pool);
|
|
return quote;
|
|
}
|
|
|
|
QVariantMap quoteOk(const QVariantMap& context,
|
|
const QVariantMap& request,
|
|
double maxA,
|
|
double maxB,
|
|
double actualA,
|
|
double actualB,
|
|
double expectedLp,
|
|
double minimumLp,
|
|
double lockedLp,
|
|
const QVariantMap& position)
|
|
{
|
|
QVariantMap quote = baseQuote(context, request);
|
|
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
|
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
|
quote.insert(QStringLiteral("status"), QStringLiteral("ok"));
|
|
quote.insert(QStringLiteral("error"), QString());
|
|
|
|
QVariantMap deposit;
|
|
deposit.insert(QStringLiteral("maxA"), amountValue(maxA, tokenA));
|
|
deposit.insert(QStringLiteral("maxB"), amountValue(maxB, tokenB));
|
|
deposit.insert(QStringLiteral("actualA"), amountValue(actualA, tokenA));
|
|
deposit.insert(QStringLiteral("actualB"), amountValue(actualB, tokenB));
|
|
quote.insert(QStringLiteral("deposit"), deposit);
|
|
|
|
QVariantMap lp;
|
|
lp.insert(QStringLiteral("expected"), amountValue(expectedLp, QStringLiteral("LP")));
|
|
lp.insert(QStringLiteral("minimum"), amountValue(minimumLp, QStringLiteral("LP")));
|
|
lp.insert(QStringLiteral("locked"), amountValue(lockedLp, QStringLiteral("LP")));
|
|
quote.insert(QStringLiteral("lp"), lp);
|
|
|
|
quote.insert(QStringLiteral("position"), position);
|
|
quote.insert(QStringLiteral("accountChanges"), accountChanges(request, context));
|
|
|
|
QVariantMap transaction;
|
|
transaction.insert(QStringLiteral("instruction"), context.value(QStringLiteral("instruction")));
|
|
transaction.insert(QStringLiteral("ready"), false);
|
|
transaction.insert(QStringLiteral("reason"), QStringLiteral("Submission requires real token holding account ids from wallet discovery."));
|
|
quote.insert(QStringLiteral("transaction"), transaction);
|
|
return quote;
|
|
}
|
|
|
|
QVariantMap quoteError(const QVariantMap& context, const QVariantMap& request, const QString& errorText)
|
|
{
|
|
QVariantMap quote = baseQuote(context, request);
|
|
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
|
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
|
quote.insert(QStringLiteral("status"), QStringLiteral("error"));
|
|
quote.insert(QStringLiteral("error"), errorText);
|
|
|
|
QVariantMap deposit;
|
|
deposit.insert(QStringLiteral("maxA"), amountValue(0, tokenA));
|
|
deposit.insert(QStringLiteral("maxB"), amountValue(0, tokenB));
|
|
deposit.insert(QStringLiteral("actualA"), amountValue(0, tokenA));
|
|
deposit.insert(QStringLiteral("actualB"), amountValue(0, tokenB));
|
|
quote.insert(QStringLiteral("deposit"), deposit);
|
|
|
|
QVariantMap lp;
|
|
lp.insert(QStringLiteral("expected"), amountValue(0, QStringLiteral("LP")));
|
|
lp.insert(QStringLiteral("minimum"), amountValue(0, QStringLiteral("LP")));
|
|
const bool missingPool =
|
|
context.value(QStringLiteral("poolStatus")).toString() == QStringLiteral("missing_pool");
|
|
lp.insert(QStringLiteral("locked"), amountValue(missingPool ? 1000.0 : 0.0, QStringLiteral("LP")));
|
|
quote.insert(QStringLiteral("lp"), lp);
|
|
|
|
QVariantMap position;
|
|
position.insert(QStringLiteral("userLp"), QStringLiteral("0 LP"));
|
|
position.insert(QStringLiteral("share"), QStringLiteral("-"));
|
|
position.insert(QStringLiteral("ownedA"), formatTokenAmount(0, tokenA));
|
|
position.insert(QStringLiteral("ownedB"), formatTokenAmount(0, tokenB));
|
|
quote.insert(QStringLiteral("position"), position);
|
|
quote.insert(QStringLiteral("accountChanges"), QVariantList());
|
|
return quote;
|
|
}
|
|
}
|
|
|
|
QString AmmUiBackend::defaultWalletHome()
|
|
{
|
|
const QByteArray override = qgetenv(WALLET_HOME_ENV);
|
|
if (!override.isEmpty())
|
|
return QString::fromLocal8Bit(override);
|
|
// LEZ's canonical wallet home, shared with the wallet UI and other LEZ apps
|
|
// (matches lez/wallet get_home_default_path()).
|
|
return QDir::homePath() + QStringLiteral("/.lee/wallet");
|
|
}
|
|
|
|
QString AmmUiBackend::defaultConfigPath() const
|
|
{
|
|
return defaultWalletHome() + QStringLiteral("/wallet_config.json");
|
|
}
|
|
|
|
QString AmmUiBackend::defaultStoragePath() const
|
|
{
|
|
return defaultWalletHome() + QStringLiteral("/storage.json");
|
|
}
|
|
|
|
namespace {
|
|
constexpr int CHECKPOINT_BLOCK_ID = 10;
|
|
constexpr int BLOCK_HASH_OFFSET = 40;
|
|
constexpr int BLOCK_HASH_SIZE = 32;
|
|
const QString DEFAULT_PROGRAM_OWNER(64, QLatin1Char('0'));
|
|
|
|
QByteArray resource(const QString& path)
|
|
{
|
|
QFile file(path);
|
|
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
|
|
}
|
|
|
|
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 error;
|
|
const QJsonDocument document = QJsonDocument::fromJson(payload, &error);
|
|
if (error.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 error;
|
|
const QJsonDocument document = QJsonDocument::fromJson(payload, &error);
|
|
if (error.error != QJsonParseError::NoError || !document.isObject())
|
|
return {};
|
|
const QString channel = document.object().value(QStringLiteral("result")).toString();
|
|
return ActiveNetwork::isValidIdentity(channel) ? channel : QString();
|
|
}
|
|
|
|
QString decimalAdd(const QString& left, const QString& right)
|
|
{
|
|
if (left.isEmpty() || right.isEmpty())
|
|
return {};
|
|
if (!std::all_of(left.cbegin(), left.cend(), [](QChar value) { return value.isDigit(); })
|
|
|| !std::all_of(right.cbegin(), right.cend(), [](QChar value) { return value.isDigit(); })) {
|
|
return {};
|
|
}
|
|
QString result;
|
|
result.reserve(std::max(left.size(), right.size()) + 1);
|
|
qsizetype leftIndex = left.size();
|
|
qsizetype rightIndex = right.size();
|
|
int carry = 0;
|
|
while (leftIndex > 0 || rightIndex > 0 || carry > 0) {
|
|
const int leftDigit = leftIndex > 0
|
|
? left.at(--leftIndex).digitValue() : 0;
|
|
const int rightDigit = rightIndex > 0
|
|
? right.at(--rightIndex).digitValue() : 0;
|
|
const int sum = leftDigit + rightDigit + carry;
|
|
result.prepend(QChar(QLatin1Char('0').unicode() + sum % 10));
|
|
carry = sum / 10;
|
|
}
|
|
while (result.size() > 1 && result.startsWith(QLatin1Char('0')))
|
|
result.remove(0, 1);
|
|
return result;
|
|
}
|
|
|
|
QJsonObject enumFields(const QJsonValue& value, const QString& variant)
|
|
{
|
|
return value.toObject().value(variant).toObject();
|
|
}
|
|
|
|
WalletAccountRead accountRead(const WalletAccount& account)
|
|
{
|
|
WalletAccountRead read;
|
|
read.accountId = account.address;
|
|
read.status = account.readStatus;
|
|
read.programOwner = account.programOwner;
|
|
read.dataHex = account.dataHex;
|
|
return read;
|
|
}
|
|
}
|
|
|
|
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
|
: AmmUiBackendSimpleSource(parent),
|
|
m_accountModel(new AccountModel(this)),
|
|
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
|
|
m_logos(new LogosModules(m_logosAPI)),
|
|
m_net(new QNetworkAccessManager(this)),
|
|
m_reachabilityTimer(new QTimer(this))
|
|
{
|
|
// PROP defaults via the generated setters.
|
|
setIsWalletOpen(false);
|
|
setLastSyncedBlock(0);
|
|
setCurrentBlockHeight(0);
|
|
setWalletHome(defaultWalletHome());
|
|
// Assume reachable until a probe proves otherwise (avoids a startup flash).
|
|
setSequencerReachable(true);
|
|
setNewPositionContext(buildNewPositionContext());
|
|
|
|
// Periodically re-probe the sequencer so the banner reacts to a node going
|
|
// up/down while the app is running. Probes are no-ops until a wallet (and
|
|
// thus a sequencer address) is open.
|
|
m_reachabilityTimer->setInterval(10000);
|
|
connect(m_reachabilityTimer, &QTimer::timeout, this, [this]() { checkReachability(); });
|
|
m_reachabilityTimer->start();
|
|
|
|
// Always resolve against the canonical wallet home (LEE_WALLET_HOME_DIR or
|
|
// ~/.lee/wallet). We intentionally don't seed config/storage paths from
|
|
// QSettings anymore: a previously-persisted per-app path (~/.lee/amm-wallet)
|
|
// would otherwise override the default and pin the app to the old keystore.
|
|
|
|
// A wallet exists on disk if its storage file is present (drives whether
|
|
// the navbar "Connect" reconnects or offers to create a wallet).
|
|
const QString effectiveStorage = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
|
setWalletExists(QFileInfo::exists(effectiveStorage));
|
|
|
|
// ui-host runs our constructor inside initLogos(), synchronously, BEFORE
|
|
// it enables remoting and emits READY. Any blocking RPC here would stall
|
|
// ui-host startup past its ready watchdog. Defer the open+refresh chain to
|
|
// the first event-loop tick so ui-host finishes wiring itself up first.
|
|
QTimer::singleShot(0, this, [this]() { openOrAdoptWallet(); });
|
|
|
|
// Save wallet on quit; host may not call destructors so this is best-effort.
|
|
connect(qApp, &QCoreApplication::aboutToQuit, this,
|
|
[this]() { saveWallet(); }, Qt::DirectConnection);
|
|
}
|
|
|
|
AmmUiBackend::~AmmUiBackend()
|
|
{
|
|
saveWallet();
|
|
delete m_logos;
|
|
}
|
|
|
|
void AmmUiBackend::openOrAdoptWallet()
|
|
{
|
|
// Respect an explicit user disconnect: stay locked, show "Connect".
|
|
if (QSettings(SETTINGS_ORG, SETTINGS_APP).value(DISCONNECTED_KEY, false).toBool())
|
|
return;
|
|
|
|
// In Basecamp the logos_execution_zone module is a single shared instance,
|
|
// so the wallet may already be open (e.g. opened by the dedicated wallet
|
|
// app). Adopt that wallet instead of fighting over it: mirror its state
|
|
// rather than re-opening from disk, which could clobber unsaved in-memory
|
|
// accounts the other app holds. A freshly-created shared wallet can be open
|
|
// with zero accounts, so we can't key off list_accounts() alone (see
|
|
// sharedWalletIsOpen).
|
|
if (sharedWalletIsOpen()) {
|
|
const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
|
|
qDebug() << "AmmUiBackend: adopting already-open shared wallet"
|
|
<< existing.size() << "accounts";
|
|
setIsWalletOpen(true);
|
|
m_accountModel->replaceFromJsonArray(existing);
|
|
refreshBalances();
|
|
refreshSequencerAddr();
|
|
refreshNewPositionContext();
|
|
return;
|
|
}
|
|
|
|
// Standalone (own core instance): auto-open a previously-created wallet.
|
|
// Use persisted paths if the user picked custom ones, else the per-app
|
|
// default. Only open if the storage actually exists, otherwise stay closed
|
|
// so QML shows the "Connect" entry point (no noisy FFI errors on first run).
|
|
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
|
const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
|
if (!QFileInfo::exists(stg))
|
|
return; // No wallet yet — QML shows "Connect".
|
|
|
|
qDebug() << "AmmUiBackend: opening wallet with config" << cfg << "storage" << stg;
|
|
const int err = m_logos->logos_execution_zone.open(cfg, stg);
|
|
if (err == WALLET_FFI_SUCCESS) {
|
|
persistConfigPath(cfg);
|
|
persistStoragePath(stg);
|
|
setIsWalletOpen(true);
|
|
refreshAccounts();
|
|
refreshBlockHeights();
|
|
refreshSequencerAddr();
|
|
} else {
|
|
qWarning() << "AmmUiBackend: wallet open failed, code" << err;
|
|
refreshNewPositionContext();
|
|
}
|
|
}
|
|
|
|
bool AmmUiBackend::sharedWalletIsOpen()
|
|
{
|
|
// list_accounts() is non-empty only once the wallet holds accounts, so it
|
|
// can't distinguish "no wallet open" from "open but empty" (a wallet that
|
|
// was just created and hasn't had an account added yet). Fall back to a
|
|
// handle-dependent, account-independent signal: an open wallet always has a
|
|
// sequencer address (from its config, defaulted on open), while a closed
|
|
// core returns an empty string. This lets us adopt a freshly-created shared
|
|
// wallet instead of falling through and re-opening it from disk.
|
|
if (!QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()).isEmpty())
|
|
return true;
|
|
return !m_logos->logos_execution_zone.get_sequencer_addr().isEmpty();
|
|
}
|
|
|
|
QString AmmUiBackend::createNewDefault(QString password)
|
|
{
|
|
QDir().mkpath(defaultWalletHome());
|
|
return createNew(defaultConfigPath(), defaultStoragePath(), password);
|
|
}
|
|
|
|
QString AmmUiBackend::createNew(QString configPath, QString storagePath, QString password)
|
|
{
|
|
const QString localConfig = toLocalPath(configPath);
|
|
const QString localStorage = toLocalPath(storagePath);
|
|
// create_new returns the new wallet's BIP39 mnemonic (empty on failure). We
|
|
// hand it back to the caller instead of discarding it: wallet creation is
|
|
// the only moment the seed phrase is recoverable, so the UI must force a
|
|
// backup step before the user can proceed.
|
|
const QString mnemonic = m_logos->logos_execution_zone.create_new(localConfig, localStorage, password);
|
|
if (mnemonic.isEmpty()) {
|
|
qWarning() << "AmmUiBackend: create_new failed (empty mnemonic)";
|
|
return QString();
|
|
}
|
|
|
|
persistConfigPath(localConfig);
|
|
persistStoragePath(localStorage);
|
|
setWalletExists(true);
|
|
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
|
setIsWalletOpen(true);
|
|
refreshAccounts();
|
|
refreshBlockHeights();
|
|
refreshSequencerAddr();
|
|
refreshNewPositionContext();
|
|
return mnemonic;
|
|
}
|
|
|
|
bool AmmUiBackend::openExisting()
|
|
{
|
|
// Adopt a shared open wallet (Basecamp), else open our own from disk. A
|
|
// freshly-created shared wallet can be open with zero accounts, so probe
|
|
// open-ness rather than keying off list_accounts() alone.
|
|
if (sharedWalletIsOpen()) {
|
|
const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
|
|
setIsWalletOpen(true);
|
|
m_accountModel->replaceFromJsonArray(existing);
|
|
refreshBalances();
|
|
refreshSequencerAddr();
|
|
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
|
refreshNewPositionContext();
|
|
return true;
|
|
}
|
|
|
|
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
|
const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
|
if (!QFileInfo::exists(stg))
|
|
return false;
|
|
|
|
const int err = m_logos->logos_execution_zone.open(cfg, stg);
|
|
if (err != WALLET_FFI_SUCCESS) {
|
|
qWarning() << "AmmUiBackend: openExisting failed, code" << err;
|
|
return false;
|
|
}
|
|
persistConfigPath(cfg);
|
|
persistStoragePath(stg);
|
|
setIsWalletOpen(true);
|
|
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
|
refreshAccounts();
|
|
refreshBlockHeights();
|
|
refreshSequencerAddr();
|
|
refreshNewPositionContext();
|
|
return true;
|
|
}
|
|
|
|
void AmmUiBackend::disconnectWallet()
|
|
{
|
|
// UI-local lock: persist wallet state, drop our view of it, and remember
|
|
// the choice. We do NOT close the core module's wallet handle — in Basecamp
|
|
// that instance is shared with other apps.
|
|
saveWallet();
|
|
setIsWalletOpen(false);
|
|
m_accountModel->replaceFromJsonArray(QJsonArray());
|
|
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, true);
|
|
refreshNewPositionContext();
|
|
}
|
|
|
|
QString AmmUiBackend::createAccountPublic()
|
|
{
|
|
const QString result = m_logos->logos_execution_zone.create_account_public();
|
|
if (!result.isEmpty())
|
|
refreshAccounts();
|
|
return result;
|
|
}
|
|
|
|
QString AmmUiBackend::createAccountPrivate()
|
|
{
|
|
const QString result = m_logos->logos_execution_zone.create_account_private();
|
|
if (!result.isEmpty())
|
|
refreshAccounts();
|
|
return result;
|
|
}
|
|
|
|
void AmmUiBackend::refreshAccounts()
|
|
{
|
|
const QJsonArray arr = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts());
|
|
m_accountModel->replaceFromJsonArray(arr);
|
|
refreshBalances();
|
|
refreshNewPositionContext();
|
|
}
|
|
|
|
void AmmUiBackend::refreshBalances()
|
|
{
|
|
refreshBlockHeights();
|
|
if (currentBlockHeight() > 0)
|
|
m_logos->logos_execution_zone.sync_to_block(static_cast<quint64>(currentBlockHeight()));
|
|
|
|
for (int i = 0; i < m_accountModel->count(); ++i) {
|
|
const QModelIndex idx = m_accountModel->index(i, 0);
|
|
const QString addr = m_accountModel->data(idx, AccountModel::AddressRole).toString();
|
|
const bool isPub = m_accountModel->data(idx, AccountModel::IsPublicRole).toBool();
|
|
m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub));
|
|
}
|
|
saveWallet();
|
|
refreshNewPositionContext();
|
|
}
|
|
|
|
QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
|
|
{
|
|
return m_logos->logos_execution_zone.get_balance(accountIdHex, isPublic);
|
|
}
|
|
|
|
QString AmmUiBackend::activeAccountAddress() const
|
|
{
|
|
if (m_accountModel->count() <= 0)
|
|
return {};
|
|
const QModelIndex idx = m_accountModel->index(0, 0);
|
|
return m_accountModel->data(idx, AccountModel::AddressRole).toString();
|
|
}
|
|
|
|
QVariantMap AmmUiBackend::buildNewPositionContext() const
|
|
{
|
|
QVariantMap context;
|
|
context.insert(QStringLiteral("minimumLiquidity"), 1000.0);
|
|
context.insert(QStringLiteral("feeTiers"), feeTiers());
|
|
|
|
QVariantMap network;
|
|
network.insert(QStringLiteral("id"), QStringLiteral("devnet"));
|
|
network.insert(QStringLiteral("name"), QStringLiteral("Local devnet"));
|
|
network.insert(QStringLiteral("selector"), QStringLiteral("temporary-file"));
|
|
context.insert(QStringLiteral("network"), network);
|
|
|
|
QVariantMap programIds;
|
|
programIds.insert(QStringLiteral("amm"), stableId(QStringLiteral("devnet:program:amm")));
|
|
programIds.insert(QStringLiteral("token"), stableId(QStringLiteral("devnet:program:token")));
|
|
programIds.insert(QStringLiteral("twapOracle"), stableId(QStringLiteral("devnet:program:twap-oracle")));
|
|
context.insert(QStringLiteral("programIds"), programIds);
|
|
|
|
QVariantMap activeAccount;
|
|
const bool hasAccount = isWalletOpen() && m_accountModel->count() > 0;
|
|
if (hasAccount) {
|
|
const QModelIndex idx = m_accountModel->index(0, 0);
|
|
const QString address = m_accountModel->data(idx, AccountModel::AddressRole).toString();
|
|
activeAccount.insert(QStringLiteral("name"), m_accountModel->data(idx, AccountModel::NameRole).toString());
|
|
activeAccount.insert(QStringLiteral("address"), address);
|
|
activeAccount.insert(QStringLiteral("display"), shortId(address));
|
|
activeAccount.insert(QStringLiteral("isPublic"), m_accountModel->data(idx, AccountModel::IsPublicRole).toBool());
|
|
activeAccount.insert(QStringLiteral("balance"), m_accountModel->data(idx, AccountModel::BalanceRole).toString());
|
|
context.insert(QStringLiteral("holdings"), devnetHoldings(address));
|
|
context.insert(QStringLiteral("status"), QStringLiteral("ready"));
|
|
context.insert(QStringLiteral("statusDetail"), QStringLiteral("Devnet token holdings resolved for the active wallet account."));
|
|
} else {
|
|
activeAccount.insert(QStringLiteral("name"), QString());
|
|
activeAccount.insert(QStringLiteral("address"), QString());
|
|
activeAccount.insert(QStringLiteral("display"), QStringLiteral("Not connected"));
|
|
activeAccount.insert(QStringLiteral("isPublic"), true);
|
|
activeAccount.insert(QStringLiteral("balance"), QString());
|
|
context.insert(QStringLiteral("holdings"), QVariantList());
|
|
context.insert(QStringLiteral("status"), isWalletOpen() ? QStringLiteral("no_account") : QStringLiteral("no_wallet"));
|
|
context.insert(QStringLiteral("statusDetail"),
|
|
isWalletOpen()
|
|
? QStringLiteral("Create an account before opening a liquidity position.")
|
|
: QStringLiteral("Connect a wallet before opening a liquidity position."));
|
|
}
|
|
context.insert(QStringLiteral("activeAccount"), activeAccount);
|
|
context.insert(QStringLiteral("activeAccountDisplay"), activeAccount.value(QStringLiteral("display")));
|
|
return context;
|
|
}
|
|
|
|
QVariant AmmUiBackend::refreshNewPositionContext()
|
|
{
|
|
const QVariantMap context = buildNewPositionContext();
|
|
setNewPositionContext(context);
|
|
return context;
|
|
}
|
|
|
|
QVariantMap AmmUiBackend::quoteNewPositionMap(const QVariantMap& request) const
|
|
{
|
|
const QString tokenA = request.value(QStringLiteral("tokenA")).toString();
|
|
const QString tokenB = request.value(QStringLiteral("tokenB")).toString();
|
|
const QVariantMap context = poolContext(tokenA, tokenB);
|
|
|
|
if (!isWalletOpen())
|
|
return quoteError(context, request, tr("Connect a wallet to preview this position."));
|
|
|
|
const QString owner = activeAccountAddress();
|
|
if (owner.isEmpty())
|
|
return quoteError(context, request, tr("Create an account before previewing this position."));
|
|
|
|
const QVariantList holdings = devnetHoldings(owner);
|
|
const QVariantMap holdingA = holdingBySymbol(holdings, tokenA);
|
|
const QVariantMap holdingB = holdingBySymbol(holdings, tokenB);
|
|
if (holdingA.isEmpty() || holdingB.isEmpty())
|
|
return quoteError(context, request, tr("Choose two token holdings from the active account."));
|
|
|
|
const int feeBps = request.value(QStringLiteral("feeBps")).toInt();
|
|
if (!isSupportedFeeTier(feeBps))
|
|
return quoteError(context, request, tr("Fee tier is not supported by the AMM program."));
|
|
|
|
const QString poolStatus = context.value(QStringLiteral("poolStatus")).toString();
|
|
const int storedFeeBps = context.value(QStringLiteral("storedFeeBps")).toInt();
|
|
if (poolStatus == QStringLiteral("active_pool") && feeBps != storedFeeBps)
|
|
return quoteError(context, request,
|
|
tr("Existing pool uses %1.").arg(feeLabel(storedFeeBps)));
|
|
|
|
const int slippageBps =
|
|
std::max(1, std::min(5000, request.value(QStringLiteral("slippageBps")).toInt()));
|
|
|
|
if (poolStatus == QStringLiteral("active_pool")) {
|
|
const double inputA = parsePositiveAmount(request.value(QStringLiteral("amountA")).toString());
|
|
const double inputB = parsePositiveAmount(request.value(QStringLiteral("amountB")).toString());
|
|
const bool editA = request.value(QStringLiteral("editedSide")).toString() != QStringLiteral("B");
|
|
const double ratio = activeRatio(tokenA, tokenB);
|
|
const double amountA = editA ? inputA : inputB / ratio;
|
|
const double amountB = editA ? inputA * ratio : inputB;
|
|
const double expectedLp = std::floor(std::min(amountA * 5.5, amountB * 0.69));
|
|
const double minimumLp = std::floor(expectedLp * (10000 - slippageBps) / 10000.0);
|
|
|
|
if (inputA <= 0.0 && inputB <= 0.0)
|
|
return quoteError(context, request, tr("Enter a deposit amount to preview LP output."));
|
|
if (amountA > holdingA.value(QStringLiteral("balance")).toDouble())
|
|
return quoteError(context, request, tr("Insufficient %1 balance.").arg(tokenA));
|
|
if (amountB > holdingB.value(QStringLiteral("balance")).toDouble())
|
|
return quoteError(context, request, tr("Insufficient %1 balance.").arg(tokenB));
|
|
if (minimumLp <= 0.0)
|
|
return quoteError(context, request, tr("LP minimum rounds to zero. Increase deposit amount."));
|
|
|
|
QVariantMap position;
|
|
position.insert(QStringLiteral("userLp"), QStringLiteral("148320 LP"));
|
|
position.insert(QStringLiteral("share"), QStringLiteral("1.18%"));
|
|
position.insert(QStringLiteral("ownedA"), formatTokenAmount(tokenA == QStringLiteral("USDC") ? 14750.0 : 118000.0, tokenA));
|
|
position.insert(QStringLiteral("ownedB"), formatTokenAmount(tokenB == QStringLiteral("LOGOS") ? 118000.0 : 14750.0, tokenB));
|
|
return quoteOk(context, request, amountA, amountB, amountA, amountB, expectedLp, minimumLp, 0.0, position);
|
|
}
|
|
|
|
if (poolStatus == QStringLiteral("missing_pool")) {
|
|
const double price =
|
|
parsePositiveAmount(request.value(QStringLiteral("initialPrice")).toString()) > 0.0
|
|
? parsePositiveAmount(request.value(QStringLiteral("initialPrice")).toString())
|
|
: defaultInitialPrice(tokenA, tokenB);
|
|
const double scale = std::max(1, request.value(QStringLiteral("depositScale")).toInt());
|
|
const double amountA = price >= 1.0 ? price * scale : scale;
|
|
const double amountB = price >= 1.0 ? scale : scale / price;
|
|
const double expectedLp = std::floor(std::sqrt(amountA * amountB) * 48.0);
|
|
const double userLp = std::max(0.0, expectedLp - 1000.0);
|
|
const double minimumLp = std::floor(userLp * (10000 - slippageBps) / 10000.0);
|
|
|
|
if (amountA > holdingA.value(QStringLiteral("balance")).toDouble())
|
|
return quoteError(context, request, tr("Initial deposit exceeds %1 balance.").arg(tokenA));
|
|
if (amountB > holdingB.value(QStringLiteral("balance")).toDouble())
|
|
return quoteError(context, request, tr("Initial deposit exceeds %1 balance.").arg(tokenB));
|
|
if (userLp <= 0.0)
|
|
return quoteError(context, request, tr("Deposit must mint more than the locked minimum liquidity."));
|
|
|
|
QVariantMap position;
|
|
position.insert(QStringLiteral("userLp"), QStringLiteral("0 LP"));
|
|
position.insert(QStringLiteral("share"), QStringLiteral("New pool"));
|
|
position.insert(QStringLiteral("ownedA"), formatTokenAmount(0, tokenA));
|
|
position.insert(QStringLiteral("ownedB"), formatTokenAmount(0, tokenB));
|
|
|
|
QVariantMap quote =
|
|
quoteOk(context, request, amountA, amountB, amountA, amountB, userLp, minimumLp, 1000.0, position);
|
|
QVariantMap pool = quote.value(QStringLiteral("pool")).toMap();
|
|
pool.insert(QStringLiteral("priceText"),
|
|
QStringLiteral("1 %1 = %2 %3")
|
|
.arg(tokenB, formatAmount(price), tokenA));
|
|
quote.insert(QStringLiteral("pool"), pool);
|
|
return quote;
|
|
}
|
|
|
|
return quoteError(context, request, context.value(QStringLiteral("detail")).toString());
|
|
}
|
|
|
|
QVariant AmmUiBackend::quoteNewPosition(QVariant request)
|
|
{
|
|
return quoteNewPositionMap(request.toMap());
|
|
}
|
|
|
|
QVariant AmmUiBackend::submitNewPosition(QVariant request, QString quoteHash)
|
|
{
|
|
const QVariantMap quote = quoteNewPositionMap(request.toMap());
|
|
if (quote.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
|
|
QVariantMap result;
|
|
result.insert(QStringLiteral("status"), QStringLiteral("error"));
|
|
result.insert(QStringLiteral("code"), QStringLiteral("quote_error"));
|
|
result.insert(QStringLiteral("error"), quote.value(QStringLiteral("error")));
|
|
result.insert(QStringLiteral("quote"), quote);
|
|
return result;
|
|
}
|
|
|
|
if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) {
|
|
QVariantMap result;
|
|
result.insert(QStringLiteral("status"), QStringLiteral("error"));
|
|
result.insert(QStringLiteral("code"), QStringLiteral("quote_changed"));
|
|
result.insert(QStringLiteral("error"), tr("Quote changed. Refresh preview before submitting."));
|
|
result.insert(QStringLiteral("quote"), quote);
|
|
return result;
|
|
}
|
|
|
|
const QVariantMap transaction = quote.value(QStringLiteral("transaction")).toMap();
|
|
if (!transaction.value(QStringLiteral("ready")).toBool()) {
|
|
QVariantMap result;
|
|
result.insert(QStringLiteral("status"), QStringLiteral("error"));
|
|
result.insert(QStringLiteral("code"), QStringLiteral("submit_unavailable"));
|
|
result.insert(QStringLiteral("error"), transaction.value(QStringLiteral("reason")).toString());
|
|
result.insert(QStringLiteral("quote"), quote);
|
|
return result;
|
|
}
|
|
|
|
QVariantMap result;
|
|
result.insert(QStringLiteral("status"), QStringLiteral("ok"));
|
|
result.insert(QStringLiteral("message"),
|
|
quote.value(QStringLiteral("instruction")).toString() == QStringLiteral("new_definition")
|
|
? tr("Pool creation submitted")
|
|
: tr("Liquidity deposit submitted"));
|
|
result.insert(QStringLiteral("detail"),
|
|
QStringLiteral("%1 / %2")
|
|
.arg(request.toMap().value(QStringLiteral("tokenA")).toString(),
|
|
request.toMap().value(QStringLiteral("tokenB")).toString()));
|
|
return result;
|
|
}
|
|
|
|
void AmmUiBackend::refreshBlockHeights()
|
|
{
|
|
const int lastVal = m_logos->logos_execution_zone.get_last_synced_block();
|
|
const int currentVal = m_logos->logos_execution_zone.get_current_block_height();
|
|
if (lastSyncedBlock() != lastVal)
|
|
setLastSyncedBlock(lastVal);
|
|
if (currentBlockHeight() != currentVal)
|
|
setCurrentBlockHeight(currentVal);
|
|
}
|
|
|
|
void AmmUiBackend::refreshSequencerAddr()
|
|
{
|
|
const QString addr = m_logos->logos_execution_zone.get_sequencer_addr();
|
|
if (sequencerAddr() != addr)
|
|
setSequencerAddr(addr);
|
|
// Probe right away so the banner reflects the (possibly new) endpoint
|
|
// without waiting for the next periodic tick.
|
|
checkReachability();
|
|
}
|
|
|
|
void AmmUiBackend::checkReachability()
|
|
{
|
|
const QString addr = sequencerAddr();
|
|
if (addr.isEmpty())
|
|
return;
|
|
|
|
QNetworkRequest req{QUrl(addr)};
|
|
req.setTransferTimeout(4000);
|
|
QNetworkReply* reply = m_net->get(req);
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
|
// Any HTTP response (even a 404) means the node is up; only a transport
|
|
// failure (connection refused, host not found, timeout) counts as down.
|
|
const bool gotHttpStatus =
|
|
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
|
|
const bool reachable = gotHttpStatus || reply->error() == QNetworkReply::NoError;
|
|
if (sequencerReachable() != reachable)
|
|
setSequencerReachable(reachable);
|
|
reply->deleteLater();
|
|
});
|
|
}
|
|
|
|
void AmmUiBackend::saveWallet()
|
|
{
|
|
if (isWalletOpen())
|
|
m_logos->logos_execution_zone.save();
|
|
}
|
|
|
|
// These only update the in-session PROPs (so subsequent open/refresh calls
|
|
// reuse the same path). They are no longer written to QSettings: the app
|
|
// always resolves against the canonical wallet home, so there's nothing to
|
|
// remember across launches.
|
|
void AmmUiBackend::persistConfigPath(const QString& path)
|
|
{
|
|
setConfigPath(toLocalPath(path));
|
|
}
|
|
|
|
void AmmUiBackend::persistStoragePath(const QString& path)
|
|
{
|
|
setStoragePath(toLocalPath(path));
|
|
}
|
|
|
|
bool AmmUiBackend::changeSequencerAddr(QString url)
|
|
{
|
|
QString normalized = url.trimmed();
|
|
if (normalized.isEmpty()) {
|
|
qWarning() << "AmmUiBackend: refusing to set empty sequencer_addr";
|
|
return false;
|
|
}
|
|
|
|
// The wallet config parses sequencer_addr as a strict URL — a missing
|
|
// scheme makes the whole config fail to deserialize (and would leave the
|
|
// wallet unopenable). Default to http:// so users can type just host:port,
|
|
// then validate before writing anything.
|
|
if (!normalized.contains(QStringLiteral("://")))
|
|
normalized.prepend(QStringLiteral("http://"));
|
|
|
|
const QUrl parsed(normalized, QUrl::StrictMode);
|
|
if (!parsed.isValid() || parsed.host().isEmpty()
|
|
|| (parsed.scheme() != QStringLiteral("http")
|
|
&& parsed.scheme() != QStringLiteral("https"))) {
|
|
qWarning() << "AmmUiBackend: invalid sequencer URL" << url;
|
|
return false;
|
|
}
|
|
normalized = parsed.toString();
|
|
|
|
const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
|
|
|
// Preserve the other config fields (poll timeouts, retries) — only swap the
|
|
// endpoint. The wallet reads this file on open via from_path_or_initialize_default.
|
|
QJsonObject obj;
|
|
QFile in(cfg);
|
|
if (in.open(QIODevice::ReadOnly)) {
|
|
obj = QJsonDocument::fromJson(in.readAll()).object();
|
|
in.close();
|
|
}
|
|
obj.insert(QStringLiteral("sequencer_addr"), normalized);
|
|
|
|
QFile out(cfg);
|
|
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
qWarning() << "AmmUiBackend: cannot write wallet config" << cfg;
|
|
return false;
|
|
}
|
|
out.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
|
|
out.close();
|
|
|
|
// Config is now the source of truth — reflect the change in the UI.
|
|
if (sequencerAddr() != normalized)
|
|
setSequencerAddr(normalized);
|
|
checkReachability();
|
|
|
|
// The module can't re-open an already-open wallet, so the new endpoint only
|
|
// takes effect on the next launch. The UI confirms a restart before calling
|
|
// this and closes the app afterwards.
|
|
return true;
|
|
}
|
|
|
|
void AmmUiBackend::copyToClipboard(QString text)
|
|
{
|
|
if (QGuiApplication::clipboard())
|
|
QGuiApplication::clipboard()->setText(text);
|
|
}
|