factor(amm-ui): source new-position network context from AMM_PROGRAM_BIN/TOKENS_CONFIG/wallet

The create-pool / new-position flow carried its own network layer:
AMM_UI_NETWORK + AMM_UI_DEVNET_FILE (a devnet.json) or a bundled
config/networks.json supplied the AMM program id and token set, and a
JSON-RPC channel/checkpoint "identity probe" gated the flow to a verified
network. Main already exposes all of this the way the Swap view consumes it,
so collapse onto those sources instead of a parallel system:

- ammProgramId  <- $AMM_PROGRAM_BIN (derived like swapExactInput's program id;
                   doubles as the quote's network fingerprint so a quote can't
                   be replayed against a different deployment)
- tokenIds      <- $TOKENS_CONFIG (amm-tokens.json), same as the Swap picker
- sequencer     <- the wallet config (already surfaced via syncWalletState)

networkSnapshot() builds the ActiveNetworkSnapshot from those; status is
"ready"/"config_missing", gated to "loading" until wallet state resolves so no
module reads happen during construction. The channel probe is gone — submit
needs no channelId (the wallet module supplies the channel via
submitPublicTransaction), so the whole verification apparatus was overhead.

Removes: AMM_UI_NETWORK / AMM_UI_DEVNET_FILE, devnet.json / networks.json, the
ActiveNetwork class (+ its test) and its QNetwork channel probe, and Qt6Network.
ActiveNetwork.h keeps only the ActiveNetworkSnapshot struct. Run command drops
the AMM_UI_* vars:
```
  LEE_WALLET_HOME_DIR=… AMM_PROGRAM_BIN=… TOKENS_CONFIG=… nix run .#amm-ui
```
This commit is contained in:
r4bbit
2026-07-27 12:14:27 +02:00
parent d7017a2515
commit e585489a60
8 changed files with 85 additions and 386 deletions
-141
View File
@@ -1,141 +0,0 @@
#include "ActiveNetwork.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace {
const char NETWORK_ENV[] = "AMM_UI_NETWORK";
const char DEVNET_FILE_ENV[] = "AMM_UI_DEVNET_FILE";
bool isLowerHex(const QString& value, int size)
{
if (value.size() != size)
return false;
for (const QChar character : value) {
const bool isAsciiDigit = character >= QLatin1Char('0')
&& character <= QLatin1Char('9');
if (!isAsciiDigit
&& (character < QLatin1Char('a') || character > QLatin1Char('f'))) {
return false;
}
}
return true;
}
}
bool ActiveNetwork::load()
{
m_network = {};
m_network.status = QStringLiteral("config_missing");
m_expectedIdentity.clear();
const QByteArray selected = qgetenv(NETWORK_ENV);
m_network.id = selected.isEmpty()
? QStringLiteral("testnet")
: QString::fromLocal8Bit(selected).trimmed();
QJsonObject entry;
if (isDevnet()) {
const QString path = QString::fromLocal8Bit(qgetenv(DEVNET_FILE_ENV));
QFile file(path);
if (path.isEmpty() || !file.open(QIODevice::ReadOnly))
return false;
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
if (!document.isObject())
return false;
entry = document.object();
m_expectedIdentity = entry.value(QStringLiteral("channelId")).toString();
} else {
QFile file(QStringLiteral(":/amm/config/networks.json"));
if (!file.open(QIODevice::ReadOnly))
return false;
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
if (!document.isObject())
return false;
const QJsonObject networks = document.object();
entry = networks.value(m_network.id).toObject();
m_expectedIdentity = entry.value(QStringLiteral("checkpointHash")).toString();
}
m_network.ammProgramId = entry.value(QStringLiteral("ammProgramId")).toString();
if (!isValidIdentity(m_expectedIdentity)
|| !isLowerHex(m_network.ammProgramId, 64)) {
return false;
}
for (const QJsonValue& value : entry.value(QStringLiteral("tokenDefinitionIds")).toArray()) {
const QString id = value.toString();
if (!isLowerHex(id, 64)) {
m_network.tokenIds.clear();
return false;
}
m_network.tokenIds.append(id);
}
m_network.status = QStringLiteral("network_unknown");
return true;
}
bool ActiveNetwork::isConfigured() const
{
return m_network.status != QStringLiteral("config_missing");
}
bool ActiveNetwork::isDevnet() const
{
return m_network.id == QStringLiteral("devnet");
}
bool ActiveNetwork::needsIdentityProbe() const
{
return m_network.status == QStringLiteral("loading")
|| m_network.status == QStringLiteral("network_unknown");
}
void ActiveNetwork::sequencerChanged(bool available)
{
if (isConfigured())
clearIdentity(available ? QStringLiteral("loading") : QStringLiteral("network_unknown"));
}
void ActiveNetwork::reachabilityChanged(bool reachable, bool wasReachable)
{
if (!isConfigured())
return;
if (!reachable)
clearIdentity(QStringLiteral("network_unknown"));
else if (!wasReachable)
clearIdentity(QStringLiteral("loading"));
}
void ActiveNetwork::beginIdentityProbe()
{
if (isConfigured())
clearIdentity(QStringLiteral("loading"));
}
void ActiveNetwork::finishIdentityProbe(const QString& identity)
{
if (identity.isEmpty()) {
clearIdentity(QStringLiteral("network_unknown"));
} else if (identity != m_expectedIdentity) {
clearIdentity(QStringLiteral("network_mismatch"));
} else {
m_network.status = QStringLiteral("ready");
m_network.fingerprint = (isDevnet() ? QStringLiteral("channel:")
: QStringLiteral("block10:"))
+ identity;
}
}
bool ActiveNetwork::isValidIdentity(const QString& value)
{
return isLowerHex(value, 64);
}
void ActiveNetwork::clearIdentity(const QString& status)
{
m_network.status = status;
m_network.fingerprint.clear();
}
+5 -24
View File
@@ -3,6 +3,11 @@
#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;
@@ -10,27 +15,3 @@ struct ActiveNetworkSnapshot {
QString ammProgramId;
QStringList tokenIds;
};
class ActiveNetwork final {
public:
bool load();
const QString& status() const { return m_network.status; }
bool isConfigured() const;
bool isDevnet() const;
bool needsIdentityProbe() const;
ActiveNetworkSnapshot snapshot() const { return m_network; }
void sequencerChanged(bool available);
void reachabilityChanged(bool reachable, bool wasReachable);
void beginIdentityProbe();
void finishIdentityProbe(const QString& identity);
static bool isValidIdentity(const QString& value);
private:
void clearIdentity(const QString& status);
ActiveNetworkSnapshot m_network;
QString m_expectedIdentity;
};
+61 -108
View File
@@ -14,9 +14,6 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include <QTimer>
#include <QUrl>
@@ -144,46 +141,6 @@ namespace {
}
}
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)),
@@ -192,13 +149,11 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
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))
m_newPosition(std::make_unique<NewPositionRuntime>(m_wallet.get(), m_ammClient.get()))
{
setWalletStateReady(false);
m_network.load();
setNewPositionContext(m_newPosition->context(
QVariantMap(), m_network.snapshot(), false, false));
QVariantMap(), networkSnapshot(), false, false));
connect(m_walletController.get(), &WalletController::stateChanged,
this, &AmmUiBackend::syncWalletState);
@@ -289,29 +244,25 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request)
else {
request = m_newPositionHints;
}
if (m_network.status() == QStringLiteral("network_unknown"))
probeNetworkIdentity();
setNewPositionContext(m_newPosition->context(
request, m_network.snapshot(), isWalletOpen(), refreshWalletAccounts));
request, networkSnapshot(), isWalletOpen(), refreshWalletAccounts));
}
QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request)
{
return m_newPosition->quote(request, m_network.snapshot(), isWalletOpen());
return m_newPosition->quote(request, networkSnapshot(), isWalletOpen());
}
QVariantMap AmmUiBackend::submitNewPosition(QVariantMap request, QString quoteHash)
{
return m_newPosition->submit(
request, quoteHash, m_network.snapshot(), isWalletOpen());
request, quoteHash, networkSnapshot(), 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);
@@ -323,69 +274,71 @@ void AmmUiBackend::syncWalletState()
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));
m_newPositionHints, networkSnapshot(), isWalletOpen(), false));
}
QString AmmUiBackend::ammProgramIdHex()
{
const QByteArray elf = loadAmmElf();
if (elf.isEmpty())
return QString();
ProgramId ammId{};
if (!amm_client_program_id_from_elf(reinterpret_cast<const uint8_t*>(elf.constData()),
static_cast<uintptr_t>(elf.size()), &ammId)) {
qWarning() << "AmmUiBackend::ammProgramIdHex: amm_client_program_id_from_elf failed";
return QString();
}
// 32 bytes, little-endian per u32 word, lowercase hex — same encoding as
// swapExactInput's program-id and `spel program-id`.
QByteArray bytes;
bytes.reserve(32);
for (int i = 0; i < 8; ++i) {
const uint32_t word = ammId[i];
bytes.append(static_cast<char>(word & 0xff));
bytes.append(static_cast<char>((word >> 8) & 0xff));
bytes.append(static_cast<char>((word >> 16) & 0xff));
bytes.append(static_cast<char>((word >> 24) & 0xff));
}
return QString::fromLatin1(bytes.toHex());
}
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;
}
snapshot.ammProgramId = ammProgramIdHex();
// 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 = snapshot.ammProgramId;
// 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())
snapshot.tokenIds.append(id);
}
snapshot.status = snapshot.ammProgramId.isEmpty()
? QStringLiteral("config_missing")
: QStringLiteral("ready");
return snapshot;
}
QString AmmUiBackend::normalizeAccountId(const QString& id)
+10 -6
View File
@@ -23,7 +23,6 @@ struct LogosModules;
class AmmClient;
class LogosWalletProvider;
class NewPositionRuntime;
class QNetworkAccessManager;
class WalletController;
// Source-side implementation of the AmmUiBackend .rep interface.
@@ -67,9 +66,18 @@ public slots:
private:
void syncWalletState();
void probeNetworkIdentity();
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.
@@ -92,11 +100,7 @@ private:
std::unique_ptr<AmmClient> m_ammClient;
std::unique_ptr<NewPositionRuntime> m_newPosition;
QNetworkAccessManager* m_net;
ActiveNetwork m_network;
QVariantMap m_newPositionHints;
bool m_identityProbeInFlight = false;
};
#endif // AMM_UI_BACKEND_H