fix(wallet): decode advanced program accounts

This commit is contained in:
Ricardo Guilherme Schmidt
2026-07-18 20:16:09 -03:00
parent ea9813cef0
commit f1a94c7f39
9 changed files with 136 additions and 24 deletions
+13
View File
@@ -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)
+6 -2
View File
@@ -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
+2 -1
View File
@@ -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": [],
+58 -4
View File
@@ -1,5 +1,6 @@
#include "AmmUiBackend.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
@@ -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<WalletAccountRead> 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<WalletAccountRead>& 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<WalletAccountPresentation> 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<QString, QString> 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;
+7 -1
View File
@@ -3,6 +3,7 @@
#include <memory>
#include <QByteArray>
#include <QObject>
#include <QHash>
#include <QSet>
@@ -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<WalletAccountRead>& 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;
+8 -6
View File
@@ -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<WalletAccountRead> 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));
});
});
});
+2 -1
View File
@@ -20,6 +20,7 @@ struct WalletAccountRead;
class NewPositionRuntime : public QObject {
public:
using ResultCallback = std::function<void(QVariantMap)>;
using ContextCallback = std::function<void(QVariantMap, QVector<WalletAccountRead>)>;
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,
+20 -5
View File
@@ -441,7 +441,9 @@ namespace {
};
}
bool waitForContext(NewPositionRuntime& runtime, bool forceRefresh)
bool waitForContext(NewPositionRuntime& runtime,
bool forceRefresh,
QVector<WalletAccountRead>* 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<WalletAccountRead> 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<WalletAccountRead>) { ++staleContextCallbacks; });
if (!expect(waitForRequestCount(staleContextServer, 1),
"first context should begin the config read"))
return 1;
staleContextRuntime.contextAsync(
{}, readyNetwork(), true, false,
[&](QVariantMap) { ++latestContextCallbacks; });
[&](QVariantMap, QVector<WalletAccountRead>) { ++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<WalletAccountRead> 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(
+20 -4
View File
@@ -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;
});
};
}