fix(amm): order swap holdings/reserves by pool token order; accept base58 ids

This commit is contained in:
Andrea Franz 2026-07-14 10:29:55 +02:00 committed by r4bbit
parent 906aa65b4f
commit caf53d0409
3 changed files with 59 additions and 9 deletions

View File

@ -24,15 +24,16 @@ Rectangle {
property real slippageTolerancePercent: 0.5
// Pool resolution (backend.resolvePool)
// sellToken is always passed as defA and buyToken as defB, so reserveA
// always corresponds to sellToken and reserveB to buyToken no separate
// "direction" bookkeeping is needed (unlike a generic A/B picker): the
// sell/buy assignment *is* the direction.
// A pool's PoolDefinition stores def_a/def_b in the pool CREATOR's order
// (from NewDefinition), not sorted, and not necessarily in (sellToken,
// buyToken) order. reserveA/reserveB mirror that same canonical order, so
// they must be mapped to sell/buy via poolDefAHex below never assumed.
property bool poolLoading: false
property bool poolResolved: false
property bool poolExists: false
property string poolReserveA: "0"
property string poolReserveB: "0"
property string poolDefAHex: ""
property int poolFeeBps: 30
property string poolError: ""
@ -95,6 +96,7 @@ Rectangle {
root.poolExists = !!(pool && pool.exists)
root.poolReserveA = (pool && pool.reserveA) || "0"
root.poolReserveB = (pool && pool.reserveB) || "0"
root.poolDefAHex = (pool && pool.defAHex) || ""
root.poolFeeBps = (pool && pool.feeBps) || 30
root.poolError = ""
},
@ -111,8 +113,12 @@ Rectangle {
// drive the *estimate* (expected output / min received / price impact),
// never the actual swap amount the sell amount sent to the backend is
// the user's raw input text, passed through verbatim.
readonly property real sellReserveNum: Number(root.poolReserveA) || 0
readonly property real buyReserveNum: Number(root.poolReserveB) || 0
// The pool's reserveA/reserveB follow the pool's canonical def_a/def_b
// order (see poolDefAHex above), which may or may not match sell/buy
// map them explicitly rather than assuming reserveA == sell.
readonly property bool sellIsPoolA: !!root.sellToken && root.sellToken.definitionId === root.poolDefAHex
readonly property real sellReserveNum: Number(sellIsPoolA ? root.poolReserveA : root.poolReserveB) || 0
readonly property real buyReserveNum: Number(sellIsPoolA ? root.poolReserveB : root.poolReserveA) || 0
readonly property real parsedSellInput: {
var amt = parseFloat(sellInput)

View File

@ -1,5 +1,8 @@
#include "AmmUiBackend.h"
#include <cctype>
#include <cstring>
#include <QClipboard>
#include <QCoreApplication>
#include <QDebug>
@ -484,6 +487,26 @@ void AmmUiBackend::copyToClipboard(QString text)
QGuiApplication::clipboard()->setText(text);
}
QString AmmUiBackend::normalizeAccountId(const QString& id)
{
const QString t = id.trimmed();
// Already 64 hex chars?
if (t.size() == 64) {
bool allHex = true;
for (const QChar c : t) {
if (!std::isxdigit(static_cast<unsigned char>(c.toLatin1()))) {
allHex = false;
break;
}
}
if (allHex)
return t.toLower();
}
// Try base58 -> hex via the wallet module.
const QString hex = m_logos->logos_execution_zone.account_id_from_base58(t);
return hex.toLower(); // account_id_from_base58 returns "" on failure
}
QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
{
QVariantMap out;
@ -574,6 +597,8 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
out[QStringLiteral("exists")] = true;
out[QStringLiteral("configHex")] = configHex;
out[QStringLiteral("poolIdHex")] = poolHex;
out[QStringLiteral("defAHex")] = bytes32ToHex(poolView.def_a);
out[QStringLiteral("defBHex")] = bytes32ToHex(poolView.def_b);
out[QStringLiteral("vaultAHex")] = bytes32ToHex(poolView.vault_a);
out[QStringLiteral("vaultBHex")] = bytes32ToHex(poolView.vault_b);
out[QStringLiteral("currentTickHex")] = bytes32ToHex(currentTickPda);
@ -713,11 +738,26 @@ QVariantList AmmUiBackend::tokenList()
continue;
}
const QJsonObject obj = entry.toObject();
const QString symbol = obj.value(QStringLiteral("symbol")).toString();
// TOKENS_CONFIG entries may give definitionId/holding as hex or
// base58 (the wallet/runbook display base58) — normalize both to
// lowercase hex here so every downstream consumer (resolvePool,
// swapExactInput, and the QML's hex comparisons) can assume hex.
const QString definitionId =
normalizeAccountId(obj.value(QStringLiteral("definitionId")).toString());
const QString holding = normalizeAccountId(obj.value(QStringLiteral("holding")).toString());
if (definitionId.isEmpty() || holding.isEmpty()) {
qWarning() << "AmmUiBackend::tokenList: skipping token" << symbol
<< "— cannot normalize definitionId/holding to hex (not valid hex or base58)";
continue;
}
QVariantMap token;
token[QStringLiteral("symbol")] = obj.value(QStringLiteral("symbol")).toString();
token[QStringLiteral("symbol")] = symbol;
token[QStringLiteral("name")] = obj.value(QStringLiteral("name")).toString();
token[QStringLiteral("definitionId")] = obj.value(QStringLiteral("definitionId")).toString();
token[QStringLiteral("holding")] = obj.value(QStringLiteral("holding")).toString();
token[QStringLiteral("definitionId")] = definitionId;
token[QStringLiteral("holding")] = holding;
token[QStringLiteral("decimals")] = obj.value(QStringLiteral("decimals")).toInt();
out.append(token);
}

View File

@ -68,6 +68,10 @@ private:
void persistConfigPath(const QString& path);
void persistStoragePath(const QString& path);
// 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.
QString normalizeAccountId(const QString& id);
void openOrAdoptWallet();
// True when the shared core already has a wallet open — including a freshly
// created one with zero accounts. See the definition for why list_accounts()