From f1a94c7f39c6555212d04cac81226761e2372eb4 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Sat, 18 Jul 2026 20:16:09 -0300 Subject: [PATCH] fix(wallet): decode advanced program accounts --- apps/amm/CMakeLists.txt | 13 ++++ apps/amm/flake.nix | 8 ++- apps/amm/metadata.json | 3 +- apps/amm/src/AmmUiBackend.cpp | 62 +++++++++++++++++-- apps/amm/src/AmmUiBackend.h | 8 ++- apps/amm/src/NewPositionRuntime.cpp | 14 +++-- apps/amm/src/NewPositionRuntime.h | 3 +- apps/amm/tests/cpp/NewPositionRuntimeTest.cpp | 25 ++++++-- flake.nix | 24 +++++-- 9 files changed, 136 insertions(+), 24 deletions(-) diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt index 3e10204..701e5bb 100644 --- a/apps/amm/CMakeLists.txt +++ b/apps/amm/CMakeLists.txt @@ -44,6 +44,8 @@ logos_module( src/NewPositionRuntime.cpp src/SequencerClient.h src/SequencerClient.cpp + src/WalletIdlDecoder.h + src/WalletIdlDecoder.cpp FIND_PACKAGES Qt6Gui Qt6Network @@ -55,12 +57,23 @@ logos_module( logos_wallet_access EXTERNAL_LIBS amm_client + wallet_idl_decoder ) +set_source_files_properties( + config/idl/token-idl.json + PROPERTIES QT_RESOURCE_ALIAS "idl/token-idl.json" +) +set_source_files_properties( + config/idl/amm-idl.json + PROPERTIES QT_RESOURCE_ALIAS "idl/amm-idl.json" +) qt_add_resources(amm_ui_module_plugin amm_ui_config PREFIX "/amm" FILES config/networks.json + config/idl/token-idl.json + config/idl/amm-idl.json ) if(BUILD_TESTING) diff --git a/apps/amm/flake.nix b/apps/amm/flake.nix index d5da892..8d6c712 100644 --- a/apps/amm/flake.nix +++ b/apps/amm/flake.nix @@ -72,7 +72,7 @@ logos_execution_zone = logosExecutionZoneModule; }; - ammClientInput = (import ../../flake.nix).outputs { + clientLibrariesInput = (import ../../flake.nix).outputs { inherit nixpkgs crane; }; moduleBuild = logos-module-builder.lib.mkLogosQmlModule { @@ -83,7 +83,11 @@ cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${../shared/wallet}") ''; externalLibInputs = { - amm_client = ammClientInput; + amm_client = clientLibrariesInput; + wallet_idl_decoder = { + input = clientLibrariesInput; + packages.default = "wallet_idl_decoder"; + }; }; postInstall = '' # The builder installs the view under lib/qml after this hook. Its diff --git a/apps/amm/metadata.json b/apps/amm/metadata.json index f0e663c..da064a8 100644 --- a/apps/amm/metadata.json +++ b/apps/amm/metadata.json @@ -15,7 +15,8 @@ "runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"] }, "external_libraries": [ - { "name": "amm_client" } + { "name": "amm_client" }, + { "name": "wallet_idl_decoder" } ], "cmake": { "find_packages": [], diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 06440e3..e592ab3 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -1,5 +1,6 @@ #include "AmmUiBackend.h" +#include #include #include #include @@ -17,7 +18,9 @@ #include "LogosWalletProvider.h" #include "NewPositionRuntime.h" #include "SequencerClient.h" +#include "WalletAccountId.h" #include "WalletController.h" +#include "WalletIdlDecoder.h" #include "logos_api.h" namespace { @@ -25,6 +28,12 @@ namespace { const int BLOCK_HASH_OFFSET = 40; const int BLOCK_HASH_SIZE = 32; + 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 { @@ -58,6 +67,19 @@ namespace { return ActiveNetwork::isValidIdentity(channel) ? channel : QString(); } + QString decodedDataText(const QJsonValue& value) + { + if (value.isObject()) { + return QString::fromUtf8( + QJsonDocument(value.toObject()).toJson(QJsonDocument::Indented)).trimmed(); + } + if (value.isArray()) { + return QString::fromUtf8( + QJsonDocument(value.toArray()).toJson(QJsonDocument::Indented)).trimmed(); + } + return {}; + } + } AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) @@ -72,7 +94,9 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) m_wallet.get(), m_ammClient.get(), m_sequencer.get())), m_net(new QNetworkAccessManager(this)), m_transactionTimer(new QTimer(this)), - m_identityRetryTimer(new QTimer(this)) + m_identityRetryTimer(new QTimer(this)), + m_tokenIdl(resource(QStringLiteral(":/amm/idl/token-idl.json"))), + m_ammIdl(resource(QStringLiteral(":/amm/idl/amm-idl.json"))) { setNewPositionQuoteResult({}); setNewPositionSubmitResult({}); @@ -86,6 +110,8 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) connect(m_identityRetryTimer, &QTimer::timeout, this, &AmmUiBackend::probeNetworkIdentity); m_network.load(); + m_idlRegistry.registerProgram( + m_network.snapshot().ammProgramId, QStringLiteral("AMM"), m_ammIdl); m_walletController->setDefaultSequencerAddress(m_network.snapshot().sequencerAddress); connect(m_walletController.get(), &WalletController::stateChanged, @@ -175,10 +201,10 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request) const quint64 generation = ++m_contextGeneration; m_newPosition->contextAsync( request, m_network.snapshot(), isWalletOpen(), refreshWalletAccounts, - [this, generation](QVariantMap result) { + [this, generation](QVariantMap result, QVector walletReads) { if (generation == m_contextGeneration) { result.insert(QStringLiteral("requestId"), generation); - publishWalletAssets(result); + publishWalletAssets(result, walletReads); setNewPositionContext(std::move(result)); } }); @@ -344,7 +370,9 @@ void AmmUiBackend::publishNetworkContext(bool refreshContext) refreshNewPositionContext(m_newPositionHints); } -void AmmUiBackend::publishWalletAssets(const QVariantMap& context) +void AmmUiBackend::publishWalletAssets( + const QVariantMap& context, + const QVector& programReads) { const QString contextStatus = context.value(QStringLiteral("status")).toString(); if (contextStatus == QStringLiteral("no_wallet")) { @@ -366,6 +394,29 @@ void AmmUiBackend::publishWalletAssets(const QVariantMap& context) QVariantList assets; QVariantList available; QVector presentations; + const QVariantMap programIds = context.value(QStringLiteral("programIds")).toMap(); + const QString tokenProgramId = walletAccountIdFromBase58( + programIds.value(QStringLiteral("token")).toString()); + m_idlRegistry.registerProgram( + m_network.snapshot().ammProgramId, QStringLiteral("AMM"), m_ammIdl); + m_idlRegistry.registerProgram(tokenProgramId, QStringLiteral("Token"), m_tokenIdl); + + QHash decodedDataByAccount; + for (const WalletDecodedProgram& program : m_idlRegistry.decode(programReads)) { + for (const WalletDecodedAccount& account : program.result.accounts) { + if (account.status != QStringLiteral("decoded")) + continue; + WalletAccountPresentation presentation; + presentation.address = account.id; + presentation.kind = QStringLiteral("program"); + presentation.semanticName = account.typeName; + presentation.programName = program.programName; + presentation.accountType = account.typeName; + presentation.decodedData = decodedDataText(account.value); + decodedDataByAccount.insert(presentation.address, presentation.decodedData); + presentations.append(std::move(presentation)); + } + } bool hasUnavailableToken = false; for (const QVariant& value : context.value(QStringLiteral("tokens")).toList()) { const QVariantMap token = value.toMap(); @@ -398,6 +449,7 @@ void AmmUiBackend::publishWalletAssets(const QVariantMap& context) QStringLiteral("holdingId")).toString(); if (holdingId.isEmpty()) continue; + const QString decodedHoldingId = walletAccountIdFromBase58(holdingId); presentations.append({ holdingId, QStringLiteral("token_holding"), @@ -406,6 +458,8 @@ void AmmUiBackend::publishWalletAssets(const QVariantMap& context) QStringLiteral("TokenHolding"), definitionId, true, + decodedDataByAccount.value( + decodedHoldingId.isEmpty() ? holdingId : decodedHoldingId), }); } hasUnavailableToken = hasUnavailableToken || !ready; diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 7d78469..d2b613c 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -3,6 +3,7 @@ #include +#include #include #include #include @@ -14,6 +15,7 @@ #include "ActiveNetwork.h" #include "WalletAccountModel.h" +#include "WalletIdlDecoder.h" class LogosAPI; class AmmClient; @@ -65,7 +67,8 @@ private: void syncWalletState(); void probeNetworkIdentity(); void publishNetworkContext(bool refreshContext = true); - void publishWalletAssets(const QVariantMap& context); + void publishWalletAssets(const QVariantMap& context, + const QVector& programReads); void watchTransaction(const QVariantMap& result); void pollTransactions(); void refreshAffectedAccounts(const QStringList& accountIds, int attempt = 0); @@ -82,6 +85,9 @@ private: QTimer* m_identityRetryTimer; ActiveNetwork m_network; + QByteArray m_tokenIdl; + QByteArray m_ammIdl; + WalletIdlRegistry m_idlRegistry; QVariantMap m_newPositionHints; bool m_identityProbeInFlight = false; bool m_walletSnapshotPending = false; diff --git a/apps/amm/src/NewPositionRuntime.cpp b/apps/amm/src/NewPositionRuntime.cpp index 62fdec0..ff12b39 100644 --- a/apps/amm/src/NewPositionRuntime.cpp +++ b/apps/amm/src/NewPositionRuntime.cpp @@ -344,16 +344,16 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, const ActiveNetworkSnapshot& network, bool walletOpen, bool refreshPublicData, - ResultCallback callback) + ContextCallback callback) { const quint64 contextGeneration = ++m_contextGeneration; if (network.status != QStringLiteral("ready")) { - callback(contextState(network.status, network).toVariantMap()); + callback(contextState(network.status, network).toVariantMap(), {}); return; } if (!m_sequencer || !m_sequencer->isConfigured()) { callback(contextState(QStringLiteral("error"), network, - QStringLiteral("sequencer_config_required")).toVariantMap()); + QStringLiteral("sequencer_config_required")).toVariantMap(), {}); return; } @@ -361,7 +361,7 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } }); if (!configResult.ok) { callback(contextState(QStringLiteral("error"), network, - QStringLiteral("backend_error")).toVariantMap()); + QStringLiteral("backend_error")).toVariantMap(), {}); return; } const QString configId = configResult.value.value( @@ -406,7 +406,7 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, : QStringLiteral("backend_error"); callback(contextState(QStringLiteral("error"), network, code.isEmpty() ? QStringLiteral("backend_error") : code) - .toVariantMap()); + .toVariantMap(), {}); return; } @@ -417,6 +417,7 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, } guard->m_sequencer->readAccounts(definitionIds, refreshPublicData, [guard, network, walletOpen, config, walletAccounts, + walletReads = std::move(walletReads), configured, recent, resolved, contextGeneration, callback = std::move(callback)]( QVector definitions) mutable { @@ -440,7 +441,8 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, callback((result.ok ? result.value : contextState(QStringLiteral("error"), network, - QStringLiteral("backend_error"))).toVariantMap()); + QStringLiteral("backend_error"))).toVariantMap(), + std::move(walletReads)); }); }); }); diff --git a/apps/amm/src/NewPositionRuntime.h b/apps/amm/src/NewPositionRuntime.h index dfc1ecc..e4f99e4 100644 --- a/apps/amm/src/NewPositionRuntime.h +++ b/apps/amm/src/NewPositionRuntime.h @@ -20,6 +20,7 @@ struct WalletAccountRead; class NewPositionRuntime : public QObject { public: using ResultCallback = std::function; + using ContextCallback = std::function)>; NewPositionRuntime(WalletProvider* wallet, AmmClient* client, @@ -32,7 +33,7 @@ public: const ActiveNetworkSnapshot& network, bool walletOpen, bool refreshPublicData, - ResultCallback callback); + ContextCallback callback); void quoteAsync(const QVariantMap& request, const ActiveNetworkSnapshot& network, bool walletOpen, diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp index 14e97b3..4fae1dd 100644 --- a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp +++ b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp @@ -441,7 +441,9 @@ namespace { }; } - bool waitForContext(NewPositionRuntime& runtime, bool forceRefresh) + bool waitForContext(NewPositionRuntime& runtime, + bool forceRefresh, + QVector* reads = nullptr) { bool completed = false; QEventLoop loop; @@ -449,7 +451,9 @@ namespace { timeout.setSingleShot(true); QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); runtime.contextAsync({}, readyNetwork(), true, forceRefresh, - [&](QVariantMap) { + [&](QVariantMap, QVector result) { + if (reads) + *reads = std::move(result); completed = true; loop.quit(); }); @@ -850,13 +854,13 @@ int main(int argc, char** argv) int latestContextCallbacks = 0; staleContextRuntime.contextAsync( {}, readyNetwork(), true, false, - [&](QVariantMap) { ++staleContextCallbacks; }); + [&](QVariantMap, QVector) { ++staleContextCallbacks; }); if (!expect(waitForRequestCount(staleContextServer, 1), "first context should begin the config read")) return 1; staleContextRuntime.contextAsync( {}, readyNetwork(), true, false, - [&](QVariantMap) { ++latestContextCallbacks; }); + [&](QVariantMap, QVector) { ++latestContextCallbacks; }); QCoreApplication::processEvents(QEventLoop::AllEvents, 10); if (!expect(staleContextServer.requestCount() == 1, "superseding context should share the active config read")) @@ -1091,12 +1095,23 @@ int main(int argc, char** argv) if (!expect(server.requestCount() == 2, "cached context should not reread accounts")) return 1; - if (!expect(waitForContext(refreshRuntime, true), + refreshClient.normalizedBalanceHex = QString(31, QLatin1Char('0')) + + QLatin1Char('1'); + QVector refreshedWalletReads; + if (!expect(waitForContext(refreshRuntime, true, &refreshedWalletReads), "forced context should complete")) return 1; if (!expect(server.requestCount() == 4, "forced context should reread config and wallet holding")) return 1; + if (!expect(refreshedWalletReads.size() == 1 + && refreshedWalletReads.first().accountId == holding.address + && refreshedWalletReads.first().ok() + && refreshedWalletReads.first().balanceHex + == refreshClient.normalizedBalanceHex, + "forced context should return fresh wallet reads")) + return 1; + refreshClient.normalizedBalanceHex = QString(32, QLatin1Char('0')); FakeWallet selectedWallet; NewPositionRuntime selectedRuntime( diff --git a/flake.nix b/flake.nix index 41ee985..3b2bcf6 100644 --- a/flake.nix +++ b/flake.nix @@ -23,23 +23,39 @@ src = craneLib.cleanCargoSource ./.; commonArgs = { inherit src; - pname = "amm_client"; version = "0.1.0"; strictDeps = true; + }; + ammClientArgs = commonArgs // { + pname = "amm_client"; cargoExtraArgs = "-p amm_client"; }; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - ammClient = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; + ammClientArtifacts = craneLib.buildDepsOnly ammClientArgs; + ammClient = craneLib.buildPackage (ammClientArgs // { + cargoArtifacts = ammClientArtifacts; doCheck = false; postInstall = '' install -Dm644 ${./apps/amm/client/include/amm_client.h} \ $out/include/amm_client.h ''; }); + walletDecoderArgs = commonArgs // { + pname = "wallet-idl-decoder"; + cargoExtraArgs = "-p wallet-idl-decoder"; + }; + walletDecoderArtifacts = craneLib.buildDepsOnly walletDecoderArgs; + walletDecoder = craneLib.buildPackage (walletDecoderArgs // { + cargoArtifacts = walletDecoderArtifacts; + doCheck = false; + postInstall = '' + install -Dm644 ${./tools/wallet-idl-decoder/include/wallet_idl_decoder.h} \ + $out/include/wallet_idl_decoder.h + ''; + }); in { default = ammClient; amm_client = ammClient; + wallet_idl_decoder = walletDecoder; }); }; }