mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-28 01:33:13 +00:00
feat(amm): byte-encode swap instruction for QtRO + AMM_DEBUG tracing + token config
This commit is contained in:
parent
caf53d0409
commit
c0568e3a88
22
README.md
22
README.md
@ -30,16 +30,30 @@ mkdir -p ~/.config/nix && echo "experimental-features = nix-command flakes" >> ~
|
|||||||
|
|
||||||
### Example (`apps/amm`)
|
### Example (`apps/amm`)
|
||||||
|
|
||||||
```bash
|
The AMM UI is built from the **repository-root** flake (which also provides the
|
||||||
cd apps/amm
|
`amm_client_ffi` library it links). Run it from the repo root by its named
|
||||||
|
attribute — there is no bare `nix run .` default, so future apps are
|
||||||
|
`nix run .#<name>`:
|
||||||
|
|
||||||
# Run the app
|
```bash
|
||||||
nix run .
|
# Run the app (from the repo root)
|
||||||
|
nix run .#amm-ui
|
||||||
|
|
||||||
# Update pinned dependencies
|
# Update pinned dependencies
|
||||||
nix flake update
|
nix flake update
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use the Swap view, also set `AMM_PROGRAM_BIN` (your deployed `amm.bin`) and
|
||||||
|
`TOKENS_CONFIG` (your token list) — use absolute paths, from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \
|
||||||
|
TOKENS_CONFIG=$(pwd)/amm-tokens.json \
|
||||||
|
nix run .#amm-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
See `apps/amm/README.md` for full details on both variables.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Rust** — install via [rustup](https://rustup.rs/). The pinned toolchain version is set in `rust-toolchain.toml`.
|
- **Rust** — install via [rustup](https://rustup.rs/). The pinned toolchain version is set in `rust-toolchain.toml`.
|
||||||
|
|||||||
@ -131,6 +131,20 @@ 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`
|
4. Choose the core module `.lgx` from `result-core/`, then the UI plugin `.lgx`
|
||||||
from `result-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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \
|
||||||
|
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.
|
||||||
|
|
||||||
### AMM program binary (required for swaps)
|
### AMM program binary (required for swaps)
|
||||||
|
|
||||||
To execute a swap, the app must submit a transaction against the **exact AMM
|
To execute a swap, the app must submit a transaction against the **exact AMM
|
||||||
@ -178,10 +192,16 @@ account the wallet will sign transfers from/to for that token):
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `TOKENS_CONFIG` is unset, unreadable, or not a valid JSON array, the token
|
If `TOKENS_CONFIG` is unset, unreadable, or not a valid JSON array, the token
|
||||||
picker stays empty (a `qWarning` is logged; no swap can be started).
|
picker stays empty (a `qWarning` naming the exact cause is logged to stderr; no
|
||||||
|
swap can be started). `definitionId`/`holding` may be given as base58 (as the
|
||||||
|
wallet/runbook display them) or hex — the app normalizes both to hex.
|
||||||
|
|
||||||
|
Full command with both variables set (absolute paths, from the repo root):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AMM_PROGRAM_BIN=/abs/path/to/amm.bin TOKENS_CONFIG=/abs/path/to/tokens.json nix run .#amm-ui
|
AMM_PROGRAM_BIN=$(pwd)/programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \
|
||||||
|
TOKENS_CONFIG=$(pwd)/amm-tokens.json \
|
||||||
|
nix run .#amm-ui
|
||||||
```
|
```
|
||||||
|
|
||||||
## Updating Dependencies
|
## Updating Dependencies
|
||||||
|
|||||||
@ -97,8 +97,10 @@ Rectangle {
|
|||||||
root.poolReserveA = (pool && pool.reserveA) || "0"
|
root.poolReserveA = (pool && pool.reserveA) || "0"
|
||||||
root.poolReserveB = (pool && pool.reserveB) || "0"
|
root.poolReserveB = (pool && pool.reserveB) || "0"
|
||||||
root.poolDefAHex = (pool && pool.defAHex) || ""
|
root.poolDefAHex = (pool && pool.defAHex) || ""
|
||||||
root.poolFeeBps = (pool && pool.feeBps) || 30
|
// feeBps === 0 is a legitimate zero-fee pool; only fall back
|
||||||
root.poolError = ""
|
// to the 30bps default when the backend didn't send a value.
|
||||||
|
root.poolFeeBps = (pool && pool.feeBps !== undefined) ? pool.feeBps : 30
|
||||||
|
root.poolError = (pool && pool.error && pool.error !== "no_pool") ? pool.error : ""
|
||||||
},
|
},
|
||||||
function (error) {
|
function (error) {
|
||||||
console.warn("resolvePool error:", error)
|
console.warn("resolvePool error:", error)
|
||||||
@ -294,6 +296,9 @@ Rectangle {
|
|||||||
amount: root.sellDisplay
|
amount: root.sellDisplay
|
||||||
token: root.sellToken
|
token: root.sellToken
|
||||||
active: root.editingSide === "sell"
|
active: root.editingSide === "sell"
|
||||||
|
// Sell amount is sent to the backend as a raw base-units integer
|
||||||
|
// string; reject fractional entry rather than fail opaquely.
|
||||||
|
digitsOnly: true
|
||||||
onInputEdited: function(v) {
|
onInputEdited: function(v) {
|
||||||
root.sellInput = v
|
root.sellInput = v
|
||||||
if (root.editingSide !== "sell") root.editingSide = "sell"
|
if (root.editingSide !== "sell") root.editingSide = "sell"
|
||||||
|
|||||||
@ -11,6 +11,11 @@ Rectangle {
|
|||||||
property string usdValue: ""
|
property string usdValue: ""
|
||||||
property var token: null
|
property var token: null
|
||||||
property bool active: true
|
property bool active: true
|
||||||
|
// When true, restrict input to digits only — used for the sell-amount
|
||||||
|
// field, whose value is sent to the backend as a raw base-units integer
|
||||||
|
// string (see decimalToU128Le in AmmUiBackend.cpp); fractional/decimal
|
||||||
|
// input there fails opaquely rather than being scaled.
|
||||||
|
property bool digitsOnly: false
|
||||||
|
|
||||||
signal tokenClicked()
|
signal tokenClicked()
|
||||||
signal inputEdited(string newValue)
|
signal inputEdited(string newValue)
|
||||||
@ -57,9 +62,21 @@ Rectangle {
|
|||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
selectionColor: theme.colors.selection
|
selectionColor: theme.colors.selection
|
||||||
clip: true
|
clip: true
|
||||||
onTextEdited: root.inputEdited(text)
|
onTextEdited: {
|
||||||
|
if (root.digitsOnly) {
|
||||||
|
// Amounts are base units (integers) — strip any
|
||||||
|
// character that slips past the validator (e.g.
|
||||||
|
// via paste) before it reaches the backend.
|
||||||
|
var filtered = text.replace(/[^0-9]/g, "")
|
||||||
|
if (filtered !== text)
|
||||||
|
text = filtered // does not re-trigger onTextEdited
|
||||||
|
root.inputEdited(filtered)
|
||||||
|
} else {
|
||||||
|
root.inputEdited(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
validator: RegularExpressionValidator {
|
validator: RegularExpressionValidator {
|
||||||
regularExpression: /^[0-9]*\.?[0-9]*$/
|
regularExpression: root.digitsOnly ? /^[0-9]*$/ : /^[0-9]*\.?[0-9]*$/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,28 @@ extern "C" {
|
|||||||
#include "amm_client_ffi.h"
|
#include "amm_client_ffi.h"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug tracing for the swap path (resolvePool / swapExactInput), gated behind
|
||||||
|
// the AMM_DEBUG env var so normal runs stay quiet. When disabled, the streamed
|
||||||
|
// arguments — including the account_id_to_base58 round-trips used to render
|
||||||
|
// accounts — are NOT evaluated, so there's no per-call overhead.
|
||||||
|
//
|
||||||
|
// NOTE: `send_generic_public_transaction` takes `instruction` and
|
||||||
|
// `program_dependencies` as byte strings (`bstr`). Declaring them as Vec<u32> /
|
||||||
|
// Vec<Vec<u8>> makes the module's Qt/QtRO glue downgrade them to an opaque `any`
|
||||||
|
// the separate module process can't deserialize, silently dropping every
|
||||||
|
// argument. We therefore send `instruction` as the little-endian bytes of the
|
||||||
|
// u32 words and `program_dependencies` empty. Requires the wallet module built
|
||||||
|
// with byte-string params; see docs/amm-swap-qtro-serialization-bug.md.
|
||||||
|
static bool ammDebugEnabled()
|
||||||
|
{
|
||||||
|
static const bool on = qEnvironmentVariableIsSet("AMM_DEBUG");
|
||||||
|
return on;
|
||||||
|
}
|
||||||
|
// AMM_DBG() << ... behaves like qWarning().noquote() but only when AMM_DEBUG is
|
||||||
|
// set; otherwise the whole statement (and its arguments) is skipped.
|
||||||
|
#define AMM_DBG() \
|
||||||
|
if (ammDebugEnabled()) qWarning().noquote()
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const char SETTINGS_ORG[] = "Logos";
|
const char SETTINGS_ORG[] = "Logos";
|
||||||
const char SETTINGS_APP[] = "AmmUI";
|
const char SETTINGS_APP[] = "AmmUI";
|
||||||
@ -98,11 +120,15 @@ namespace {
|
|||||||
if (trimmed.isEmpty())
|
if (trimmed.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const unsigned __int128 kMax = ~static_cast<unsigned __int128>(0);
|
||||||
unsigned __int128 value = 0;
|
unsigned __int128 value = 0;
|
||||||
for (const QChar ch : trimmed) {
|
for (const QChar ch : trimmed) {
|
||||||
if (!ch.isDigit())
|
if (!ch.isDigit())
|
||||||
return false;
|
return false;
|
||||||
value = value * 10 + static_cast<unsigned int>(ch.digitValue());
|
const unsigned int d = static_cast<unsigned int>(ch.digitValue());
|
||||||
|
if (value > (kMax - d) / 10)
|
||||||
|
return false; // would overflow u128
|
||||||
|
value = value * 10 + d;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 16; ++i) {
|
for (int i = 0; i < 16; ++i) {
|
||||||
@ -246,16 +272,16 @@ void AmmUiBackend::openOrAdoptWallet()
|
|||||||
|
|
||||||
bool AmmUiBackend::sharedWalletIsOpen()
|
bool AmmUiBackend::sharedWalletIsOpen()
|
||||||
{
|
{
|
||||||
// list_accounts() is non-empty only once the wallet holds accounts, so it
|
// Treat the shared core as "already open" ONLY when it actually holds
|
||||||
// can't distinguish "no wallet open" from "open but empty" (a wallet that
|
// accounts. We used to also treat a non-empty sequencer address as proof of
|
||||||
// was just created and hasn't had an account added yet). Fall back to a
|
// an open wallet ("a closed core returns an empty string"), but the wallet
|
||||||
// handle-dependent, account-independent signal: an open wallet always has a
|
// module now reports a DEFAULT sequencer (e.g. http://localhost:…) even on a
|
||||||
// sequencer address (from its config, defaulted on open), while a closed
|
// CLOSED core. That made standalone launches wrongly take the adopt path —
|
||||||
// core returns an empty string. This lets us adopt a freshly-created shared
|
// mirroring an empty account list and returning early — instead of opening
|
||||||
// wallet instead of falling through and re-opening it from disk.
|
// the real on-disk wallet, so the user saw no accounts. Keying off accounts
|
||||||
if (!QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()).isEmpty())
|
// alone means a genuinely-open shared wallet (Basecamp) is still adopted,
|
||||||
return true;
|
// while a closed standalone core correctly falls through to open-from-disk.
|
||||||
return !m_logos->logos_execution_zone.get_sequencer_addr().isEmpty();
|
return !QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AmmUiBackend::createNewDefault(QString password)
|
QString AmmUiBackend::createNewDefault(QString password)
|
||||||
@ -507,6 +533,27 @@ QString AmmUiBackend::normalizeAccountId(const QString& id)
|
|||||||
return hex.toLower(); // account_id_from_base58 returns "" on failure
|
return hex.toLower(); // account_id_from_base58 returns "" on failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QByteArray AmmUiBackend::loadAmmElf()
|
||||||
|
{
|
||||||
|
const QByteArray binPath = qgetenv(AMM_PROGRAM_BIN_ENV);
|
||||||
|
if (binPath.isEmpty()) {
|
||||||
|
qWarning() << "AmmUiBackend::loadAmmElf: AMM_PROGRAM_BIN not set";
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
QFile elfFile(QString::fromLocal8Bit(binPath));
|
||||||
|
if (!elfFile.open(QIODevice::ReadOnly)) {
|
||||||
|
qWarning() << "AmmUiBackend::loadAmmElf: cannot read AMM_PROGRAM_BIN at" << elfFile.fileName();
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
const QByteArray elf = elfFile.readAll();
|
||||||
|
elfFile.close();
|
||||||
|
if (elf.isEmpty()) {
|
||||||
|
qWarning() << "AmmUiBackend::loadAmmElf: AMM_PROGRAM_BIN is empty";
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
return elf;
|
||||||
|
}
|
||||||
|
|
||||||
QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
||||||
{
|
{
|
||||||
QVariantMap out;
|
QVariantMap out;
|
||||||
@ -514,20 +561,9 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
|
|
||||||
// 1. Load the deployed AMM program's ELF. We can't derive this ourselves —
|
// 1. Load the deployed AMM program's ELF. We can't derive this ourselves —
|
||||||
// it must match whatever is actually deployed on the target sequencer.
|
// it must match whatever is actually deployed on the target sequencer.
|
||||||
const QByteArray binPath = qgetenv(AMM_PROGRAM_BIN_ENV);
|
const QByteArray elf = loadAmmElf();
|
||||||
if (binPath.isEmpty()) {
|
|
||||||
qWarning() << "AmmUiBackend::resolvePool: AMM_PROGRAM_BIN not set";
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
QFile elfFile(QString::fromLocal8Bit(binPath));
|
|
||||||
if (!elfFile.open(QIODevice::ReadOnly)) {
|
|
||||||
qWarning() << "AmmUiBackend::resolvePool: cannot read AMM_PROGRAM_BIN at" << elfFile.fileName();
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
const QByteArray elf = elfFile.readAll();
|
|
||||||
elfFile.close();
|
|
||||||
if (elf.isEmpty()) {
|
if (elf.isEmpty()) {
|
||||||
qWarning() << "AmmUiBackend::resolvePool: AMM_PROGRAM_BIN is empty";
|
out[QStringLiteral("error")] = QStringLiteral("no_program_bin");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,6 +572,7 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
if (!amm_client_program_id_from_elf(reinterpret_cast<const uint8_t*>(elf.constData()),
|
if (!amm_client_program_id_from_elf(reinterpret_cast<const uint8_t*>(elf.constData()),
|
||||||
static_cast<uintptr_t>(elf.size()), &ammId)) {
|
static_cast<uintptr_t>(elf.size()), &ammId)) {
|
||||||
qWarning() << "AmmUiBackend::resolvePool: amm_client_program_id_from_elf failed";
|
qWarning() << "AmmUiBackend::resolvePool: amm_client_program_id_from_elf failed";
|
||||||
|
out[QStringLiteral("error")] = QStringLiteral("bad_program_bin");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,10 +583,26 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
amm_client_config_pda(&ammId, &configPda);
|
amm_client_config_pda(&ammId, &configPda);
|
||||||
const QString configHex = bytes32ToHex(configPda);
|
const QString configHex = bytes32ToHex(configPda);
|
||||||
|
|
||||||
|
// Debug: log every derived account (base58 + hex) and every on-chain read so
|
||||||
|
// a failed resolve can be traced directly against `spel inspect`.
|
||||||
|
// account_id_to_base58 is a LOCAL encode (safe even when on-chain reads
|
||||||
|
// time out).
|
||||||
|
auto b58 = [this](const QString& hex) {
|
||||||
|
const QString s = m_logos->logos_execution_zone.account_id_to_base58(hex);
|
||||||
|
return s.isEmpty() ? hex : QStringLiteral("%1 (hex %2)").arg(s, hex);
|
||||||
|
};
|
||||||
|
AMM_DBG() << "[amm-debug] resolvePool: defA=" << b58(defAHex)
|
||||||
|
<< "defB=" << b58(defBHex);
|
||||||
|
AMM_DBG() << "[amm-debug] resolvePool: reading config account" << b58(configHex);
|
||||||
|
|
||||||
const QString configJson = m_logos->logos_execution_zone.get_account_public(configHex);
|
const QString configJson = m_logos->logos_execution_zone.get_account_public(configHex);
|
||||||
|
AMM_DBG() << "[amm-debug] resolvePool: config get_account_public ->"
|
||||||
|
<< configJson.size() << "chars; raw:" << configJson.left(400);
|
||||||
const QString configDataHex = accountDataHex(configJson);
|
const QString configDataHex = accountDataHex(configJson);
|
||||||
if (configDataHex.isEmpty()) {
|
if (configDataHex.isEmpty()) {
|
||||||
qWarning() << "AmmUiBackend::resolvePool: AMM config account has no data (not initialized)";
|
qWarning() << "AmmUiBackend::resolvePool: AMM config read returned no data"
|
||||||
|
<< "(a failed/timed-out RPC or an uninitialized account)";
|
||||||
|
out[QStringLiteral("error")] = QStringLiteral("amm_not_initialized");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,6 +611,7 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
if (!amm_client_decode_config(reinterpret_cast<const uint8_t*>(configBytes.constData()),
|
if (!amm_client_decode_config(reinterpret_cast<const uint8_t*>(configBytes.constData()),
|
||||||
static_cast<uintptr_t>(configBytes.size()), &configView)) {
|
static_cast<uintptr_t>(configBytes.size()), &configView)) {
|
||||||
qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_config failed";
|
qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_config failed";
|
||||||
|
out[QStringLiteral("error")] = QStringLiteral("bad_config");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,6 +621,7 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
uint8_t defB[32];
|
uint8_t defB[32];
|
||||||
if (!hexToBytes32(defAHex, defA) || !hexToBytes32(defBHex, defB)) {
|
if (!hexToBytes32(defAHex, defA) || !hexToBytes32(defBHex, defB)) {
|
||||||
qWarning() << "AmmUiBackend::resolvePool: invalid defAHex/defBHex";
|
qWarning() << "AmmUiBackend::resolvePool: invalid defAHex/defBHex";
|
||||||
|
out[QStringLiteral("error")] = QStringLiteral("bad_config");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -574,10 +629,14 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
amm_client_pool_pda(&ammId, &defA, &defB, &poolPda);
|
amm_client_pool_pda(&ammId, &defA, &defB, &poolPda);
|
||||||
const QString poolHex = bytes32ToHex(poolPda);
|
const QString poolHex = bytes32ToHex(poolPda);
|
||||||
|
|
||||||
|
AMM_DBG() << "[amm-debug] resolvePool: reading pool account" << b58(poolHex);
|
||||||
const QString poolJson = m_logos->logos_execution_zone.get_account_public(poolHex);
|
const QString poolJson = m_logos->logos_execution_zone.get_account_public(poolHex);
|
||||||
|
AMM_DBG() << "[amm-debug] resolvePool: pool get_account_public ->"
|
||||||
|
<< poolJson.size() << "chars; raw:" << poolJson.left(400);
|
||||||
const QString poolDataHex = accountDataHex(poolJson);
|
const QString poolDataHex = accountDataHex(poolJson);
|
||||||
if (poolDataHex.isEmpty()) {
|
if (poolDataHex.isEmpty()) {
|
||||||
// Not a warning: this is the normal "no liquidity yet" state.
|
// Not a warning: this is the normal "no liquidity yet" state.
|
||||||
|
out[QStringLiteral("error")] = QStringLiteral("no_pool");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,6 +645,7 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
if (!amm_client_decode_pool(reinterpret_cast<const uint8_t*>(poolBytes.constData()),
|
if (!amm_client_decode_pool(reinterpret_cast<const uint8_t*>(poolBytes.constData()),
|
||||||
static_cast<uintptr_t>(poolBytes.size()), &poolView)) {
|
static_cast<uintptr_t>(poolBytes.size()), &poolView)) {
|
||||||
qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_pool failed";
|
qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_pool failed";
|
||||||
|
out[QStringLiteral("error")] = QStringLiteral("bad_config");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -605,6 +665,17 @@ QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
|||||||
out[QStringLiteral("reserveA")] = u128leToDecimal(poolView.reserve_a);
|
out[QStringLiteral("reserveA")] = u128leToDecimal(poolView.reserve_a);
|
||||||
out[QStringLiteral("reserveB")] = u128leToDecimal(poolView.reserve_b);
|
out[QStringLiteral("reserveB")] = u128leToDecimal(poolView.reserve_b);
|
||||||
out[QStringLiteral("feeBps")] = static_cast<int>(poolView.fees);
|
out[QStringLiteral("feeBps")] = static_cast<int>(poolView.fees);
|
||||||
|
|
||||||
|
AMM_DBG() << "[amm-debug] resolvePool: RESOLVED"
|
||||||
|
<< "pool=" << b58(poolHex)
|
||||||
|
<< "poolDefA=" << b58(out[QStringLiteral("defAHex")].toString())
|
||||||
|
<< "poolDefB=" << b58(out[QStringLiteral("defBHex")].toString())
|
||||||
|
<< "vaultA=" << b58(out[QStringLiteral("vaultAHex")].toString())
|
||||||
|
<< "vaultB=" << b58(out[QStringLiteral("vaultBHex")].toString())
|
||||||
|
<< "currentTick=" << b58(out[QStringLiteral("currentTickHex")].toString())
|
||||||
|
<< "reserveA=" << out[QStringLiteral("reserveA")].toString()
|
||||||
|
<< "reserveB=" << out[QStringLiteral("reserveB")].toString()
|
||||||
|
<< "feeBps=" << out[QStringLiteral("feeBps")].toInt();
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,6 +683,13 @@ QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString u
|
|||||||
QString userOutputHoldingHex, QString amountInDecimal,
|
QString userOutputHoldingHex, QString amountInDecimal,
|
||||||
QString minOutDecimal, QString deadlineDecimal)
|
QString minOutDecimal, QString deadlineDecimal)
|
||||||
{
|
{
|
||||||
|
AMM_DBG() << "[amm-debug] swapExactInput: ARGS"
|
||||||
|
<< "defA=" << defAHex << "defB=" << defBHex
|
||||||
|
<< "userInputHolding=" << userInputHoldingHex
|
||||||
|
<< "userOutputHolding=" << userOutputHoldingHex
|
||||||
|
<< "amountIn=" << amountInDecimal << "minOut=" << minOutDecimal
|
||||||
|
<< "deadline=" << deadlineDecimal;
|
||||||
|
|
||||||
// 1. Resolve the pool's PDAs; refuse if there's no pool (or no liquidity)
|
// 1. Resolve the pool's PDAs; refuse if there's no pool (or no liquidity)
|
||||||
// for this token pair.
|
// for this token pair.
|
||||||
const QVariantMap pool = resolvePool(defAHex, defBHex);
|
const QVariantMap pool = resolvePool(defAHex, defBHex);
|
||||||
@ -623,20 +701,9 @@ QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString u
|
|||||||
// 2. Load the deployed AMM program's ELF (must match resolvePool's — both
|
// 2. Load the deployed AMM program's ELF (must match resolvePool's — both
|
||||||
// read the same AMM_PROGRAM_BIN — since the instruction is proven against
|
// read the same AMM_PROGRAM_BIN — since the instruction is proven against
|
||||||
// this exact binary's image id).
|
// this exact binary's image id).
|
||||||
const QByteArray binPath = qgetenv(AMM_PROGRAM_BIN_ENV);
|
const QByteArray elf = loadAmmElf();
|
||||||
if (binPath.isEmpty()) {
|
|
||||||
qWarning() << "AmmUiBackend::swapExactInput: AMM_PROGRAM_BIN not set";
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
QFile elfFile(QString::fromLocal8Bit(binPath));
|
|
||||||
if (!elfFile.open(QIODevice::ReadOnly)) {
|
|
||||||
qWarning() << "AmmUiBackend::swapExactInput: cannot read AMM_PROGRAM_BIN at" << elfFile.fileName();
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
const QByteArray elf = elfFile.readAll();
|
|
||||||
elfFile.close();
|
|
||||||
if (elf.isEmpty()) {
|
if (elf.isEmpty()) {
|
||||||
qWarning() << "AmmUiBackend::swapExactInput: AMM_PROGRAM_BIN is empty";
|
qWarning() << "AmmUiBackend::swapExactInput: failed to load AMM_PROGRAM_BIN";
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,12 +755,37 @@ QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString u
|
|||||||
};
|
};
|
||||||
const QVariantList signers = { false, false, false, false, true, false, false, false };
|
const QVariantList signers = { false, false, false, false, true, false, false, false };
|
||||||
|
|
||||||
|
// Debug: dump the exact accounts/signers we submit (base58 for `spel`
|
||||||
|
// comparison), plus the instruction/elf sizes.
|
||||||
|
auto b58s = [this](const QString& hex) {
|
||||||
|
const QString s = m_logos->logos_execution_zone.account_id_to_base58(hex);
|
||||||
|
return s.isEmpty() ? hex : QStringLiteral("%1 (hex %2)").arg(s, hex);
|
||||||
|
};
|
||||||
|
static const char* const kSlot[] = { "config", "pool",
|
||||||
|
"vault_a", "vault_b",
|
||||||
|
"user_input_holding", "user_output_holding",
|
||||||
|
"current_tick", "clock" };
|
||||||
|
AMM_DBG() << "[amm-debug] swapExactInput: SUBMIT"
|
||||||
|
<< "instruction_words=" << static_cast<int>(instruction.size())
|
||||||
|
<< "elf_bytes=" << elf.size();
|
||||||
|
for (int i = 0; i < accounts.size(); ++i) {
|
||||||
|
AMM_DBG() << "[amm-debug] account[" << i << "]"
|
||||||
|
<< (i < 8 ? kSlot[i] : "?") << "=" << b58s(accounts[i])
|
||||||
|
<< "signer=" << (i < signers.size() && signers[i].toBool());
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Submit through the wallet module's generic public-transaction entry
|
// 6. Submit through the wallet module's generic public-transaction entry
|
||||||
// point. program_dependencies is empty: the sequencer resolves the AMM's
|
// point. `instruction` is sent as the little-endian bytes of the u32 words
|
||||||
// chained token/twap calls on-chain for a public tx.
|
// (see the note atop this file); program_dependencies is empty because the
|
||||||
|
// sequencer resolves the AMM's chained token/twap calls on-chain for a
|
||||||
|
// public tx.
|
||||||
|
const QByteArray instructionBytes(reinterpret_cast<const char*>(instruction.data()),
|
||||||
|
static_cast<int>(instruction.size() * sizeof(uint32_t)));
|
||||||
const QString resultJson = m_logos->logos_execution_zone.send_generic_public_transaction(
|
const QString resultJson = m_logos->logos_execution_zone.send_generic_public_transaction(
|
||||||
accounts, signers, QVariant::fromValue(instruction), QVariant::fromValue(elf),
|
accounts, signers, QVariant::fromValue(instructionBytes), QVariant::fromValue(elf),
|
||||||
QVariant::fromValue(std::vector<std::vector<uint8_t>>()));
|
QVariant::fromValue(QByteArray()));
|
||||||
|
AMM_DBG() << "[amm-debug] swapExactInput: send_generic_public_transaction ->"
|
||||||
|
<< resultJson.size() << "chars; raw:" << resultJson.left(600);
|
||||||
|
|
||||||
const QJsonObject obj = QJsonDocument::fromJson(resultJson.toUtf8()).object();
|
const QJsonObject obj = QJsonDocument::fromJson(resultJson.toUtf8()).object();
|
||||||
if (!obj.value(QStringLiteral("success")).toBool()) {
|
if (!obj.value(QStringLiteral("success")).toBool()) {
|
||||||
|
|||||||
@ -81,6 +81,10 @@ private:
|
|||||||
void refreshSequencerAddr();
|
void refreshSequencerAddr();
|
||||||
void saveWallet();
|
void saveWallet();
|
||||||
|
|
||||||
|
// Returns the deployed AMM program ELF bytes from $AMM_PROGRAM_BIN, or an empty
|
||||||
|
// QByteArray (with a qWarning) if the env var is unset/unreadable/empty.
|
||||||
|
QByteArray loadAmmElf();
|
||||||
|
|
||||||
// Probe the configured sequencer over HTTP and update sequencerReachable.
|
// Probe the configured sequencer over HTTP and update sequencerReachable.
|
||||||
void checkReachability();
|
void checkReachability();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user