diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt index 037867c..6e8e437 100644 --- a/apps/amm/CMakeLists.txt +++ b/apps/amm/CMakeLists.txt @@ -37,17 +37,14 @@ logos_module( src/AmmUiBackend.h src/AmmUiBackend.cpp src/ActiveNetwork.h - src/ActiveNetwork.cpp src/AmmClient.h src/AmmClient.cpp src/NewPositionRuntime.h src/NewPositionRuntime.cpp FIND_PACKAGES Qt6Gui - Qt6Network LINK_LIBRARIES Qt6::Gui - Qt6::Network PkgConfig::BASE58 LINK_TARGETS logos_wallet_access @@ -56,21 +53,7 @@ logos_module( amm_client ) -qt_add_resources(amm_ui_module_plugin amm_ui_config - PREFIX "/amm" - FILES - config/networks.json -) - if(BUILD_TESTING) - add_executable(amm_active_network_test - tests/cpp/ActiveNetworkTest.cpp - src/ActiveNetwork.cpp - ) - target_include_directories(amm_active_network_test PRIVATE src) - target_link_libraries(amm_active_network_test PRIVATE Qt6::Core) - add_test(NAME amm_active_network COMMAND amm_active_network_test) - add_executable(amm_new_position_runtime_test tests/cpp/NewPositionRuntimeTest.cpp src/NewPositionRuntime.cpp diff --git a/apps/amm/README.md b/apps/amm/README.md index 126e0f8..b33841a 100644 --- a/apps/amm/README.md +++ b/apps/amm/README.md @@ -135,10 +135,12 @@ core module from `result-core/` and the UI plugin from `result-lgx/`: 4. Choose the core module `.lgx` from `result-core/`, then the UI plugin `.lgx` from `result-lgx/` -To actually use the Swap view you must also set `AMM_PROGRAM_BIN` and -`TOKENS_CONFIG` (both explained below). Run this **from the repo root** — use -absolute paths (`$(pwd)/…`), because `nix run` may not preserve the working -directory, so relative paths won't resolve: +To actually use the on-chain views (**Swap** and **Liquidity**) you must also +set `AMM_PROGRAM_BIN` and `TOKENS_CONFIG` (both explained below). Both views read +the same two — the AMM program id is derived from `AMM_PROGRAM_BIN`, the token +set from `TOKENS_CONFIG`, and the sequencer from the wallet config. Run this +**from the repo root** — use absolute paths (`$(pwd)/…`), because `nix run` may +not preserve the working directory, so relative paths won't resolve: ```bash AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \ @@ -146,10 +148,10 @@ TOKENS_CONFIG=$(pwd)/amm-tokens.json \ nix run .#amm-ui ``` -Without `AMM_PROGRAM_BIN` the Swap view stays disabled; without `TOKENS_CONFIG` -the token picker is empty. Each is detailed below. +Without `AMM_PROGRAM_BIN` the Swap and Liquidity views stay disabled; without +`TOKENS_CONFIG` the token picker is empty. Each is detailed below. -### AMM program binary (required for swaps) +### AMM program binary (required for swaps and liquidity) To execute a swap, the app must submit a transaction against the **exact AMM program you deployed** (its ELF determines the program id, and therefore every diff --git a/apps/amm/config/networks.json b/apps/amm/config/networks.json deleted file mode 100644 index 75e03eb..0000000 --- a/apps/amm/config/networks.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "testnet": { - "checkpointHash": "0d25d71fca70d7008a892f6b3f768a4c66badbcd64e67d79ca595b92f1db544a", - "ammProgramId": "77eeaa23668ad2675fb768cd7ecb1893387be464b9a51f16756006c1d307db07", - "tokenDefinitionIds": [ - "7b464ff9dd0d3bc07f7e2e0b0667ccd066d85ad12be4c79fc55687a863910aa6", - "48c81cf032e601ca367fc9816b957dbf5c0e4c11cf7008e8f4581ec1a67aab42", - "159caef810ea545951b3bd913efe625ee45008c80865c330e72a72ed48b61649", - "75f33110b185717209e3955f228d4a4448801d0ce8ba438a4a268050eeff3f44", - "fbd107ca4bb66bc58f59ac2d32a759be3ee0fb453f8fecd1991c11837d9660c7", - "5547fcb72644d95a385d313b887a96be41ff263bce6150b49fd87276839822bf", - "fa43e74a97d79c5f907ff3edabda5ad89bfbd3b0922572e675d4ad3c7b6029c7", - "4f3231d8a01e1d79f163bc27fce0c860a4a2f6890280e9d135eafbde0d68ed79", - "fa32f354408857006f8ea396b0419823bd04436eadb2d273d2618a46b4793ed8", - "00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb" - ] - } -} diff --git a/apps/amm/src/ActiveNetwork.cpp b/apps/amm/src/ActiveNetwork.cpp deleted file mode 100644 index c92bc73..0000000 --- a/apps/amm/src/ActiveNetwork.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#include "ActiveNetwork.h" - -#include -#include -#include -#include - -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(); -} diff --git a/apps/amm/src/ActiveNetwork.h b/apps/amm/src/ActiveNetwork.h index 9b1db9b..771442b 100644 --- a/apps/amm/src/ActiveNetwork.h +++ b/apps/amm/src/ActiveNetwork.h @@ -3,6 +3,11 @@ #include #include +// 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; -}; diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 353c4a3..0800f7d 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -14,9 +14,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -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( *m_wallet, QStringLiteral("AmmUI"))), m_ammClient(std::make_unique()), - m_newPosition(std::make_unique(m_wallet.get(), m_ammClient.get())), - m_net(new QNetworkAccessManager(this)) + m_newPosition(std::make_unique(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(elf.constData()), + static_cast(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(word & 0xff)); + bytes.append(static_cast((word >> 8) & 0xff)); + bytes.append(static_cast((word >> 16) & 0xff)); + bytes.append(static_cast((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) diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index df694e6..a95cff5 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -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 m_ammClient; std::unique_ptr m_newPosition; - QNetworkAccessManager* m_net; - - ActiveNetwork m_network; QVariantMap m_newPositionHints; - bool m_identityProbeInFlight = false; }; #endif // AMM_UI_BACKEND_H diff --git a/apps/amm/tests/cpp/ActiveNetworkTest.cpp b/apps/amm/tests/cpp/ActiveNetworkTest.cpp deleted file mode 100644 index 239f3e8..0000000 --- a/apps/amm/tests/cpp/ActiveNetworkTest.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "ActiveNetwork.h" - -#include -#include -#include -#include - -namespace { - bool expect(bool condition, const char* message) - { - if (!condition) - qCritical("%s", message); - return condition; - } -} - -int main() -{ - const QString identity(64, QLatin1Char('a')); - const QString programId(64, QLatin1Char('b')); - const QString tokenId(64, QLatin1Char('c')); - - QTemporaryFile config; - if (!config.open()) - return 1; - config.write(QJsonDocument(QJsonObject { - { QStringLiteral("channelId"), identity }, - { QStringLiteral("ammProgramId"), programId }, - { QStringLiteral("tokenDefinitionIds"), QJsonArray { tokenId } }, - }).toJson(QJsonDocument::Compact)); - config.flush(); - - qputenv("AMM_UI_NETWORK", "devnet"); - qputenv("AMM_UI_DEVNET_FILE", config.fileName().toLocal8Bit()); - - ActiveNetwork network; - if (!expect(network.load(), "devnet config should load")) - return 1; - if (!expect(network.snapshot().status == QStringLiteral("network_unknown"), - "loaded network should await identity")) - return 1; - - network.sequencerChanged(true); - network.finishIdentityProbe(QString(64, QLatin1Char('d'))); - if (!expect(network.status() == QStringLiteral("network_mismatch"), - "wrong identity should block the network")) - return 1; - - network.reachabilityChanged(false, true); - network.reachabilityChanged(true, false); - network.finishIdentityProbe(identity); - const ActiveNetworkSnapshot snapshot = network.snapshot(); - if (!expect(snapshot.status == QStringLiteral("ready"), - "matching identity should ready the network")) - return 1; - if (!expect(snapshot.fingerprint == QStringLiteral("channel:") + identity, - "devnet fingerprint should use channel identity")) - return 1; - if (!expect(snapshot.ammProgramId == programId - && snapshot.tokenIds == QStringList { tokenId }, - "network snapshot should preserve configured program and tokens")) - return 1; - - return 0; -}