2026-07-22 13:56:42 +02:00
|
|
|
#include "amm_module_impl.h"
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cctype>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <iterator>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
|
|
|
|
|
// Generated at build time by logos-cpp-generator. Defines `LogosModules` with
|
|
|
|
|
// one std-typed accessor per metadata.json dependency — here
|
|
|
|
|
// `logos_execution_zone`. Included only in the .cpp so the impl header the
|
|
|
|
|
// generator parses stays free of Qt and codegen types.
|
|
|
|
|
#include "logos_sdk.h"
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
2026-07-29 15:57:24 +02:00
|
|
|
#include "amm_ffi.h"
|
2026-07-22 13:56:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
using json = nlohmann::json;
|
|
|
|
|
|
|
|
|
|
// AMM_DEBUG-gated tracing. The module runs in its own logos_host process whose
|
|
|
|
|
// stderr the daemon captures, so these lines surface in the daemon log.
|
|
|
|
|
bool ammDebug() {
|
|
|
|
|
static const bool on = std::getenv("AMM_DEBUG") != nullptr;
|
|
|
|
|
return on;
|
|
|
|
|
}
|
|
|
|
|
#define AMM_TRACE(msg) \
|
|
|
|
|
do { \
|
|
|
|
|
if (ammDebug()) std::cerr << "[amm-debug] " << msg << std::endl; \
|
|
|
|
|
} while (0)
|
|
|
|
|
|
|
|
|
|
// Absolute path to the deployed AMM program's compiled binary (amm.bin). The
|
|
|
|
|
// module can't derive this itself: the wallet module's bundled AMM program may
|
|
|
|
|
// differ from whatever is deployed on the target sequencer, and the bytes are
|
|
|
|
|
// what determine the program id (and every PDA derived from it).
|
|
|
|
|
constexpr char AMM_PROGRAM_BIN_ENV[] = "AMM_PROGRAM_BIN";
|
|
|
|
|
|
|
|
|
|
int hexVal(char c) {
|
|
|
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
|
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
|
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// True when `s` is exactly `len` hex digits (case-insensitive).
|
|
|
|
|
bool isHexLen(const std::string& s, size_t len) {
|
|
|
|
|
if (s.size() != len) return false;
|
|
|
|
|
for (const char c : s)
|
|
|
|
|
if (hexVal(c) < 0) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// True when `s` is an even-length run of hex digits (case-insensitive).
|
|
|
|
|
bool isHexEven(const std::string& s) {
|
|
|
|
|
if (s.size() % 2 != 0) return false;
|
|
|
|
|
for (const char c : s)
|
|
|
|
|
if (hexVal(c) < 0) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string toHex(const uint8_t* p, size_t n) {
|
|
|
|
|
static const char* const kDigits = "0123456789abcdef";
|
|
|
|
|
std::string s;
|
|
|
|
|
s.reserve(n * 2);
|
|
|
|
|
for (size_t i = 0; i < n; ++i) {
|
|
|
|
|
s.push_back(kDigits[p[i] >> 4]);
|
|
|
|
|
s.push_back(kDigits[p[i] & 0x0f]);
|
|
|
|
|
}
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exception-safe field accessor over a json object: returns "" when the key is
|
|
|
|
|
// missing or not a string (nlohmann's value()/get() throw on a type mismatch).
|
|
|
|
|
std::string jStr(const json& obj, const char* key) {
|
|
|
|
|
const auto it = obj.find(key);
|
|
|
|
|
return (it != obj.end() && it->is_string()) ? it->get<std::string>() : std::string();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Coerce a swap amount arg — arriving as EITHER a JSON number (bare `1000` on
|
|
|
|
|
// the CLI) or a decimal string ("1000", a big u128 the UI passes, or a
|
|
|
|
|
// quote-wrapped big value on the CLI) — to its canonical decimal-string form.
|
|
|
|
|
// Small values fit a JSON integer; big values (amounts above the JSON/int64
|
|
|
|
|
// range, and unix-ms deadlines) must be strings. Returns false for JSON floats,
|
|
|
|
|
// negatives, or non-numeric strings, leaving `out` unset.
|
|
|
|
|
bool jsonAmountToDecimal(const json& j, std::string& out) {
|
|
|
|
|
if (j.is_string()) {
|
|
|
|
|
std::string s = j.get<std::string>();
|
|
|
|
|
// Trim whitespace, then a single pair of wrapping double quotes if
|
|
|
|
|
// present. This is what lets an EXACT u128 above 2^53 be passed from the
|
|
|
|
|
// logoscore CLI: such a value can't be a JSON number (a bare number that
|
|
|
|
|
// large is a lossy double), so it must be a string — but the CLI folds a
|
|
|
|
|
// quoted arg's quotes INTO the value (`"1e18"` arrives as the literal
|
|
|
|
|
// chars "\"1e18\""). Stripping the wrapper recovers the clean digits. A
|
|
|
|
|
// clean string from the UI (no wrapping quotes) is unaffected.
|
|
|
|
|
auto trim = [](std::string& x) {
|
|
|
|
|
const size_t a = x.find_first_not_of(" \t\n\r");
|
|
|
|
|
const size_t b = x.find_last_not_of(" \t\n\r");
|
|
|
|
|
x = (a == std::string::npos) ? std::string() : x.substr(a, b - a + 1);
|
|
|
|
|
};
|
|
|
|
|
trim(s);
|
|
|
|
|
if (s.size() >= 2 && s.front() == '"' && s.back() == '"') {
|
|
|
|
|
s = s.substr(1, s.size() - 2);
|
|
|
|
|
trim(s);
|
|
|
|
|
}
|
2026-08-04 17:27:02 +02:00
|
|
|
// A valid base-unit amount is a non-empty run of decimal digits. Reject
|
|
|
|
|
// anything else (empty, signs, decimal points, exponents, letters) here
|
|
|
|
|
// rather than passing it downstream to surface as an opaque backend error.
|
|
|
|
|
if (s.empty() || s.find_first_not_of("0123456789") != std::string::npos)
|
|
|
|
|
return false;
|
2026-07-22 13:56:42 +02:00
|
|
|
out = s;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (j.is_number_unsigned()) {
|
|
|
|
|
out = std::to_string(j.get<uint64_t>());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (j.is_number_integer()) {
|
|
|
|
|
const int64_t v = j.get<int64_t>();
|
|
|
|
|
if (v < 0) return false;
|
|
|
|
|
out = std::to_string(v);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Reject JSON floats (and null/object/array). logoscore promotes any bare
|
|
|
|
|
// number beyond ~2^31 to a double, so a large bare value arrives here as a
|
|
|
|
|
// float — already rounded upstream. Rather than silently accept it, require
|
|
|
|
|
// such big numbers as a (quote-wrapped) STRING, which is bit-exact.
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// json string/bool arrays -> std vectors, and a u32-word array -> the
|
|
|
|
|
// little-endian bytes the wallet module's byte-string `instruction` expects.
|
|
|
|
|
std::vector<std::string> jsonStrVec(const json& arr) {
|
|
|
|
|
std::vector<std::string> out;
|
|
|
|
|
if (!arr.is_array()) return out;
|
|
|
|
|
out.reserve(arr.size());
|
|
|
|
|
for (const auto& v : arr)
|
|
|
|
|
if (v.is_string()) out.push_back(v.get<std::string>());
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<bool> jsonBoolVec(const json& arr) {
|
|
|
|
|
std::vector<bool> out;
|
|
|
|
|
if (!arr.is_array()) return out;
|
|
|
|
|
out.reserve(arr.size());
|
|
|
|
|
for (const auto& v : arr)
|
|
|
|
|
out.push_back(v.is_boolean() && v.get<bool>());
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> jsonWordsToLeBytes(const json& arr) {
|
|
|
|
|
std::vector<uint8_t> out;
|
|
|
|
|
if (!arr.is_array()) return out;
|
|
|
|
|
out.reserve(arr.size() * sizeof(uint32_t));
|
|
|
|
|
for (const auto& v : arr) {
|
|
|
|
|
const uint32_t word = v.is_number() ? static_cast<uint32_t>(v.get<uint64_t>()) : 0;
|
|
|
|
|
out.push_back(static_cast<uint8_t>(word & 0xff));
|
|
|
|
|
out.push_back(static_cast<uint8_t>((word >> 8) & 0xff));
|
|
|
|
|
out.push_back(static_cast<uint8_t>((word >> 16) & 0xff));
|
|
|
|
|
out.push_back(static_cast<uint8_t>((word >> 24) & 0xff));
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 17:27:02 +02:00
|
|
|
// Result of an amm_ffi JSON op: the `{ ok, value, error }` envelope decoded.
|
|
|
|
|
// `error` carries the op's failure code (e.g. "no_pool") when `ok` is false, so
|
|
|
|
|
// callers can surface it in their own response envelope.
|
2026-07-22 13:56:42 +02:00
|
|
|
struct FfiResult {
|
|
|
|
|
bool ok = false;
|
|
|
|
|
json value;
|
2026-08-04 17:27:02 +02:00
|
|
|
std::string error;
|
2026-07-22 13:56:42 +02:00
|
|
|
};
|
|
|
|
|
|
2026-07-29 15:57:24 +02:00
|
|
|
// Serialize `request`, hand it to an amm_ffi op, and decode its
|
2026-07-22 13:56:42 +02:00
|
|
|
// `{ ok, value, error }` envelope. Mirrors apps/amm/src/AmmClient.cpp. `value`
|
|
|
|
|
// is only populated (and `ok` true) when the op reports success with an object.
|
|
|
|
|
FfiResult call(char* (*op)(const char*), const json& request) {
|
|
|
|
|
const std::string payload = request.dump();
|
|
|
|
|
char* raw = op(payload.c_str());
|
|
|
|
|
if (raw == nullptr) {
|
2026-07-29 15:57:24 +02:00
|
|
|
AMM_TRACE("amm_ffi op returned null");
|
2026-07-22 13:56:42 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const std::string response(raw);
|
|
|
|
|
amm_free(raw);
|
|
|
|
|
|
|
|
|
|
const auto doc = json::parse(response, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!doc.is_object()) {
|
2026-07-29 15:57:24 +02:00
|
|
|
AMM_TRACE("amm_ffi op returned invalid JSON");
|
2026-07-22 13:56:42 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
if (!doc.value("ok", false)) {
|
2026-08-04 17:27:02 +02:00
|
|
|
std::string error = doc.value("error", std::string());
|
|
|
|
|
AMM_TRACE("amm_ffi op failure: " << error);
|
|
|
|
|
return {false, json(), std::move(error)};
|
2026-07-22 13:56:42 +02:00
|
|
|
}
|
|
|
|
|
const auto it = doc.find("value");
|
|
|
|
|
if (it == doc.end() || !it->is_object()) {
|
2026-07-29 15:57:24 +02:00
|
|
|
AMM_TRACE("amm_ffi op value is not an object");
|
2026-07-22 13:56:42 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
2026-08-04 17:27:02 +02:00
|
|
|
return {true, *it, {}};
|
2026-07-22 13:56:42 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-03 15:36:59 +02:00
|
|
|
// new-position response envelope builders.
|
2026-07-22 13:56:42 +02:00
|
|
|
json issue(const std::string& code, const json& blockingFields = json::array()) {
|
|
|
|
|
return {
|
|
|
|
|
{"code", code},
|
|
|
|
|
{"recoverable", true},
|
|
|
|
|
{"blockingFields", blockingFields},
|
|
|
|
|
{"details", json::object()},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
json publicError(const std::string& code,
|
|
|
|
|
const json& blockingFields = json::array(),
|
|
|
|
|
const json& details = json::object()) {
|
|
|
|
|
json error = issue(code, blockingFields);
|
|
|
|
|
error["details"] = details;
|
|
|
|
|
return {
|
|
|
|
|
{"status", "error"},
|
|
|
|
|
{"canSubmit", false},
|
|
|
|
|
{"code", code},
|
|
|
|
|
{"errors", json::array({error})},
|
|
|
|
|
{"warnings", json::array()},
|
|
|
|
|
{"accountPreview", json::array()},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> AmmModuleImpl::loadAmmElf() {
|
|
|
|
|
const char* path = std::getenv(AMM_PROGRAM_BIN_ENV);
|
|
|
|
|
if (path == nullptr || *path == '\0') return {};
|
|
|
|
|
std::ifstream file(path, std::ios::binary);
|
|
|
|
|
if (!file) return {};
|
|
|
|
|
return std::vector<uint8_t>((std::istreambuf_iterator<char>(file)),
|
|
|
|
|
std::istreambuf_iterator<char>());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string AmmModuleImpl::ammProgramId() {
|
|
|
|
|
const std::vector<uint8_t> elf = loadAmmElf();
|
|
|
|
|
if (elf.empty()) return {};
|
2026-07-29 15:57:24 +02:00
|
|
|
// Hand the deployed binary to the amm_ffi program_id op, which decodes it
|
2026-07-22 13:56:42 +02:00
|
|
|
// and computes the Image ID — 64-char lowercase hex, little-endian per u32
|
|
|
|
|
// word (matches `spel program-id` and the on-chain *_program_id fields).
|
|
|
|
|
const FfiResult r = call(amm_program_id, json{{"elf", toHex(elf.data(), elf.size())}});
|
|
|
|
|
if (!r.ok) {
|
|
|
|
|
AMM_TRACE("ammProgramId: amm_program_id op failed");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
return jStr(r.value, "programId");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string AmmModuleImpl::normalizeAccountId(const std::string& id) {
|
|
|
|
|
size_t start = 0;
|
|
|
|
|
size_t end = id.size();
|
|
|
|
|
while (start < end && std::isspace(static_cast<unsigned char>(id[start]))) ++start;
|
|
|
|
|
while (end > start && std::isspace(static_cast<unsigned char>(id[end - 1]))) --end;
|
|
|
|
|
std::string t = id.substr(start, end - start);
|
|
|
|
|
|
|
|
|
|
if (isHexLen(t, 64)) {
|
|
|
|
|
std::transform(t.begin(), t.end(), t.begin(),
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
return t;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try base58 -> hex via the wallet module ("" on failure).
|
|
|
|
|
std::string hex = modules().logos_execution_zone.account_id_from_base58(t);
|
|
|
|
|
std::transform(hex.begin(), hex.end(), hex.begin(),
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
return hex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nlohmann::json AmmModuleImpl::readPublicAccount(const std::string& account_id) {
|
|
|
|
|
// Matches the app-side accountReadJson: WalletAccountRead defaults status to
|
|
|
|
|
// "read_failed" and only flips to "ok" when every field is well-formed hex;
|
|
|
|
|
// the `account` key is present only for an ok read.
|
|
|
|
|
json result = {{"id", account_id}, {"status", "read_failed"}};
|
|
|
|
|
|
|
|
|
|
const std::string account_json =
|
|
|
|
|
modules().logos_execution_zone.get_account_public(account_id);
|
|
|
|
|
const auto obj = json::parse(account_json, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object()) return result;
|
|
|
|
|
|
|
|
|
|
const std::string owner = jStr(obj, "program_owner");
|
|
|
|
|
const std::string balance = jStr(obj, "balance");
|
|
|
|
|
const std::string nonce = jStr(obj, "nonce");
|
|
|
|
|
const std::string data = jStr(obj, "data");
|
|
|
|
|
if (!isHexLen(owner, 64) || !isHexLen(balance, 32) || !isHexLen(nonce, 32)
|
|
|
|
|
|| !isHexEven(data)) {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result["status"] = "ok";
|
|
|
|
|
result["account"] = {
|
|
|
|
|
{"program_owner", owner},
|
|
|
|
|
{"balance", balance},
|
|
|
|
|
{"nonce", nonce},
|
|
|
|
|
{"data", data},
|
|
|
|
|
};
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
nlohmann::json AmmModuleImpl::walletAccountReads(bool wallet_open) {
|
|
|
|
|
if (!wallet_open)
|
|
|
|
|
return json::array(); // nothing to read while closed
|
2026-07-22 13:56:42 +02:00
|
|
|
|
|
|
|
|
// Normalize the wallet module's [any] return (a vector or a json array)
|
|
|
|
|
// through json so we can iterate/type-check it uniformly.
|
|
|
|
|
json accounts = modules().logos_execution_zone.list_accounts();
|
|
|
|
|
if (!accounts.is_array())
|
2026-08-12 16:19:40 +02:00
|
|
|
return json::array();
|
2026-07-22 13:56:42 +02:00
|
|
|
|
|
|
|
|
json out = json::array();
|
|
|
|
|
for (const auto& entry : accounts) {
|
|
|
|
|
if (!entry.is_object()) continue;
|
|
|
|
|
// The AMM path uses only public accounts (LP holdings / token balances).
|
|
|
|
|
if (!entry.value("is_public", true)) continue;
|
|
|
|
|
const std::string id = jStr(entry, "account_id");
|
|
|
|
|
if (id.empty()) continue;
|
|
|
|
|
out.push_back(readPublicAccount(id));
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
nlohmann::json AmmModuleImpl::readConfig(const std::string& amm_program_id) {
|
2026-07-22 13:56:42 +02:00
|
|
|
const FfiResult configResult =
|
2026-08-12 16:19:40 +02:00
|
|
|
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
2026-07-22 13:56:42 +02:00
|
|
|
if (!configResult.ok) return json(); // null: config_id op failed
|
|
|
|
|
return readPublicAccount(jStr(configResult.value, "configId"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 10:12:11 +02:00
|
|
|
LogosMap AmmModuleImpl::resolvePoolAccount(const std::string& def_a_hex,
|
|
|
|
|
const std::string& def_b_hex) {
|
2026-07-22 13:56:42 +02:00
|
|
|
// A hard failure carries a stable `error` code (no_program_bin /
|
2026-08-13 10:12:11 +02:00
|
|
|
// amm_not_initialized / bad_config) so callers can surface it. `no_pool` is the
|
|
|
|
|
// ordinary "no pool / no liquidity yet" state — also a `status:"error"` result
|
|
|
|
|
// (SwapCard treats it as its normal empty state via `error !== "no_pool"`, the
|
|
|
|
|
// flow routes it to create-pool). The underlying FFI error is AMM_TRACE'd.
|
2026-07-22 13:56:42 +02:00
|
|
|
auto failed = [](const std::string& error) {
|
2026-08-13 10:12:11 +02:00
|
|
|
return LogosMap{{"status", "error"}, {"error", error}};
|
2026-07-22 13:56:42 +02:00
|
|
|
};
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
// no program id from AMM_PROGRAM_BIN (unset/unreadable/bad).
|
2026-07-22 13:56:42 +02:00
|
|
|
return failed("no_program_bin");
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
const json config = readConfig(amm_program_id);
|
2026-07-22 13:56:42 +02:00
|
|
|
if (config.is_null())
|
|
|
|
|
return failed("bad_config"); // amm_config_id op failed (malformed program id)
|
|
|
|
|
|
2026-08-10 11:01:06 +02:00
|
|
|
// The liquidity view passes base58 ids; the swap card passes hex. Normalize both to hex
|
|
|
|
|
// (idempotent for hex) so the FFI id derivation works either way.
|
|
|
|
|
const std::string token_a = normalizeAccountId(def_a_hex);
|
|
|
|
|
const std::string token_b = normalizeAccountId(def_b_hex);
|
|
|
|
|
|
2026-07-22 13:56:42 +02:00
|
|
|
const FfiResult pairResult = call(amm_swap_pair, json{
|
2026-08-12 16:19:40 +02:00
|
|
|
{"ammProgramId", amm_program_id},
|
2026-08-10 11:01:06 +02:00
|
|
|
{"tokenInId", token_a},
|
|
|
|
|
{"tokenOutId", token_b},
|
2026-07-22 13:56:42 +02:00
|
|
|
{"config", config},
|
|
|
|
|
});
|
|
|
|
|
if (!pairResult.ok)
|
|
|
|
|
return failed("bad_config"); // FFI op failed (e.g. bad token-id hex)
|
|
|
|
|
if (jStr(pairResult.value, "status") != "ok") {
|
|
|
|
|
// config_unavailable (undecodable config) surfaces as amm_not_initialized;
|
|
|
|
|
// same_token_pair is passed through as-is.
|
|
|
|
|
const std::string code = jStr(pairResult.value, "code");
|
|
|
|
|
if (code == "config_unavailable")
|
|
|
|
|
return failed("amm_not_initialized");
|
|
|
|
|
return failed(code.empty() ? "bad_config" : code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const json pool = readPublicAccount(jStr(pairResult.value, "poolId"));
|
|
|
|
|
const FfiResult resolveResult = call(amm_resolve_pool, json{{"pool", pool}});
|
|
|
|
|
if (!resolveResult.ok)
|
|
|
|
|
return failed("bad_config"); // amm_resolve_pool op failed
|
2026-08-13 10:12:11 +02:00
|
|
|
// resolve_pool returns status:"error"/no_pool for a missing pool (pass through) or
|
|
|
|
|
// status:"ok" with the decoded state. It labels reserves/vaults in the pool's STORED
|
|
|
|
|
// order (reserveA is defAHex's); orient them to the CALLER's requested order so reserveA /
|
|
|
|
|
// vaultAId are token_a's — the stored order needn't match (it can be non-canonical, e.g.
|
|
|
|
|
// the testnet setup's pool). Callers then read A/B as their own token-a/token-b directly.
|
2026-08-10 11:01:06 +02:00
|
|
|
json resolved = resolveResult.value;
|
2026-08-13 10:12:11 +02:00
|
|
|
if (jStr(resolved, "status") == "ok" && jStr(resolved, "defAHex") != token_a) {
|
2026-08-10 11:01:06 +02:00
|
|
|
resolved["reserveA"].swap(resolved["reserveB"]);
|
|
|
|
|
resolved["defAHex"].swap(resolved["defBHex"]);
|
2026-08-13 10:12:11 +02:00
|
|
|
resolved["vaultAId"].swap(resolved["vaultBId"]);
|
2026-08-10 11:01:06 +02:00
|
|
|
}
|
2026-08-13 10:12:11 +02:00
|
|
|
return resolved;
|
2026-07-22 13:56:42 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-13 10:15:10 +02:00
|
|
|
LogosMap AmmModuleImpl::configAccount() {
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", "config_missing"}};
|
|
|
|
|
|
|
|
|
|
const json config = readConfig(amm_program_id);
|
|
|
|
|
if (config.is_null())
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", "backend_error"}};
|
|
|
|
|
|
|
|
|
|
const FfiResult result = call(amm_config_account, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
});
|
|
|
|
|
if (!result.ok)
|
|
|
|
|
return LogosMap{{"status", "error"},
|
|
|
|
|
{"error", result.error.empty() ? "backend_error" : result.error}};
|
|
|
|
|
return result.value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 10:47:00 +02:00
|
|
|
LogosMap AmmModuleImpl::transferOwnership(const LogosMap& request) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
// The plan needs the config account to decode the CURRENT admin (the sole signer).
|
|
|
|
|
const json config = readConfig(amm_program_id);
|
|
|
|
|
if (config.is_null())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
const std::string new_authority = normalizeAccountId(jStr(request, "newAuthorityId"));
|
|
|
|
|
if (new_authority.empty())
|
|
|
|
|
return error("invalid_account_id");
|
|
|
|
|
|
|
|
|
|
const FfiResult planResult = call(amm_transfer_ownership_plan, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"newAuthorityId", new_authority},
|
|
|
|
|
});
|
|
|
|
|
if (!planResult.ok)
|
|
|
|
|
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("transferOwnership: SUBMIT programId=" << program_id
|
|
|
|
|
<< " accounts=" << accounts.size());
|
|
|
|
|
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("transferOwnership: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false))
|
|
|
|
|
return error("wallet_submission_failed");
|
|
|
|
|
|
|
|
|
|
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 11:28:08 +02:00
|
|
|
LogosMap AmmModuleImpl::createPriceObservations(const LogosMap& request) {
|
|
|
|
|
return oracleSetupSubmit(request, /*observations=*/true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosMap AmmModuleImpl::createOraclePriceAccount(const LogosMap& request) {
|
|
|
|
|
return oracleSetupSubmit(request, /*observations=*/false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosMap AmmModuleImpl::oracleSetupSubmit(const LogosMap& request, bool observations) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
// The plan derives the feed PDAs from the config's twap_oracle_program_id + the pool.
|
|
|
|
|
const json config = readConfig(amm_program_id);
|
|
|
|
|
if (config.is_null())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty())
|
|
|
|
|
return error("invalid_token_id");
|
|
|
|
|
|
|
|
|
|
// windowDurationMs may arrive as a JSON number (UI) or a decimal string (CLI); the FFI wants
|
|
|
|
|
// a u64. A zero / unparsable window is rejected — each window is a distinct feed PDA.
|
|
|
|
|
const json window_json = request.value("windowDurationMs", json());
|
|
|
|
|
uint64_t window = 0;
|
|
|
|
|
if (window_json.is_number_unsigned()) {
|
|
|
|
|
window = window_json.get<uint64_t>();
|
|
|
|
|
} else if (window_json.is_number_integer() && window_json.get<int64_t>() > 0) {
|
|
|
|
|
window = window_json.get<uint64_t>();
|
|
|
|
|
} else if (window_json.is_string()) {
|
|
|
|
|
try {
|
|
|
|
|
window = std::stoull(window_json.get<std::string>());
|
|
|
|
|
} catch (...) {
|
|
|
|
|
window = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (window == 0)
|
|
|
|
|
return error("invalid_window");
|
|
|
|
|
|
|
|
|
|
auto* const plan_op =
|
|
|
|
|
observations ? amm_create_price_observations_plan : amm_create_oracle_price_account_plan;
|
|
|
|
|
const FfiResult planResult = call(plan_op, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
|
|
|
|
{"windowDurationMs", window},
|
|
|
|
|
});
|
|
|
|
|
if (!planResult.ok)
|
|
|
|
|
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction =
|
|
|
|
|
jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
// Surface a stable error code when the target PDA already exists instead of failing at submit.
|
|
|
|
|
const size_t target_index = observations ? 3 : 2;
|
|
|
|
|
if (accounts.size() > target_index) {
|
|
|
|
|
const json existing = readPublicAccount(accounts[target_index]);
|
|
|
|
|
if (jStr(existing, "status") == "ok")
|
|
|
|
|
return error("already_exists");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("oracleSetup(" << (observations ? "observations" : "priceAccount")
|
|
|
|
|
<< "): SUBMIT programId=" << program_id << " accounts=" << accounts.size());
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("oracleSetup: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false))
|
|
|
|
|
return error("wallet_submission_failed");
|
|
|
|
|
|
|
|
|
|
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 17:27:02 +02:00
|
|
|
LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
|
|
|
|
|
const std::string& token_out_hex,
|
|
|
|
|
const nlohmann::json& amount_in,
|
|
|
|
|
int64_t slippage_bps) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// amountIn arrives as a JSON number (CLI) or decimal string (UI); coerce to a
|
|
|
|
|
// canonical decimal string (rejects floats — see jsonAmountToDecimal).
|
|
|
|
|
std::string amount_in_decimal;
|
|
|
|
|
if (!jsonAmountToDecimal(amount_in, amount_in_decimal))
|
|
|
|
|
return error("bad_amount");
|
|
|
|
|
|
|
|
|
|
// slippageBps is a fraction of 100% in basis points. The FFI request field is
|
|
|
|
|
// a u32, so a negative value would fail deserialization with an opaque serde
|
|
|
|
|
// message; gate the full range here for a stable code (100% = 10000 bps, the
|
|
|
|
|
// FFI's FEE_BPS_DENOMINATOR, which also rejects the upper bound as a backstop).
|
|
|
|
|
if (slippage_bps < 0 || slippage_bps >= 10000)
|
|
|
|
|
return error("invalid_slippage");
|
|
|
|
|
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
2026-08-13 22:42:26 +02:00
|
|
|
// tokenList() moved app-side, so token ids arrive as configured (base58 or
|
|
|
|
|
// hex); normalize to the hex the FFI expects.
|
|
|
|
|
const std::string token_in = normalizeAccountId(token_in_hex);
|
|
|
|
|
const std::string token_out = normalizeAccountId(token_out_hex);
|
|
|
|
|
if (token_in.empty() || token_out.empty())
|
|
|
|
|
return error("invalid_token_id");
|
|
|
|
|
|
2026-08-04 17:27:02 +02:00
|
|
|
// Derive the pool id (config-free) and read the pool account; its raw data is
|
|
|
|
|
// handed to the pricing op. An absent account has no data → `no_pool`.
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", token_in},
|
|
|
|
|
{"tokenOutId", token_out},
|
2026-08-04 17:27:02 +02:00
|
|
|
});
|
|
|
|
|
if (!poolId.ok)
|
|
|
|
|
return error(poolId.error.empty() ? "backend_error" : poolId.error);
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
const FfiResult quoteResult = call(amm_swap_exact_in_quote, json{
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", token_in},
|
|
|
|
|
{"tokenOutId", token_out},
|
2026-08-17 13:24:17 +02:00
|
|
|
{"amountIn", amount_in_decimal},
|
2026-08-04 17:27:02 +02:00
|
|
|
{"slippageBps", slippage_bps},
|
|
|
|
|
{"poolData", pool_data},
|
|
|
|
|
});
|
|
|
|
|
if (!quoteResult.ok)
|
|
|
|
|
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
|
|
|
|
|
|
2026-08-17 13:24:17 +02:00
|
|
|
// Success: wrap the priced payload { expectedOut, minReceived,
|
2026-08-04 17:27:02 +02:00
|
|
|
// priceImpactBps } in the standard envelope.
|
|
|
|
|
LogosMap out = quoteResult.value;
|
|
|
|
|
out["status"] = "ok";
|
|
|
|
|
out["error"] = "";
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 19:05:06 +02:00
|
|
|
LogosMap AmmModuleImpl::swapExactOutQuote(const std::string& token_in_hex,
|
|
|
|
|
const std::string& token_out_hex,
|
|
|
|
|
const nlohmann::json& amount_out,
|
|
|
|
|
int64_t slippage_bps) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::string amount_out_decimal;
|
|
|
|
|
if (!jsonAmountToDecimal(amount_out, amount_out_decimal))
|
|
|
|
|
return error("bad_amount");
|
|
|
|
|
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
2026-08-13 22:42:26 +02:00
|
|
|
// tokenList() moved app-side, so token ids arrive as configured (base58 or
|
|
|
|
|
// hex); normalize to the hex the FFI expects.
|
|
|
|
|
const std::string token_in = normalizeAccountId(token_in_hex);
|
|
|
|
|
const std::string token_out = normalizeAccountId(token_out_hex);
|
|
|
|
|
|
2026-08-04 19:05:06 +02:00
|
|
|
// Derive the pool id (config-free) and read the pool account; its raw data is
|
|
|
|
|
// handed to the pricing op. An absent account has no data → `no_pool`.
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", token_in},
|
|
|
|
|
{"tokenOutId", token_out},
|
2026-08-04 19:05:06 +02:00
|
|
|
});
|
|
|
|
|
if (!poolId.ok)
|
|
|
|
|
return error(poolId.error.empty() ? "backend_error" : poolId.error);
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
const FfiResult quoteResult = call(amm_swap_exact_out_quote, json{
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", token_in},
|
|
|
|
|
{"tokenOutId", token_out},
|
2026-08-17 13:24:17 +02:00
|
|
|
{"amountOut", amount_out_decimal},
|
2026-08-04 19:05:06 +02:00
|
|
|
{"slippageBps", slippage_bps},
|
|
|
|
|
{"poolData", pool_data},
|
|
|
|
|
});
|
|
|
|
|
if (!quoteResult.ok)
|
|
|
|
|
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
|
|
|
|
|
|
2026-08-17 13:24:17 +02:00
|
|
|
// Success: wrap { requiredIn, maxIn, priceImpactBps } in the envelope.
|
2026-08-04 19:05:06 +02:00
|
|
|
LogosMap out = quoteResult.value;
|
|
|
|
|
out["status"] = "ok";
|
|
|
|
|
out["error"] = "";
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 13:56:42 +02:00
|
|
|
std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
|
|
|
|
|
const std::string& def_b_hex,
|
|
|
|
|
const std::string& user_input_holding_hex,
|
|
|
|
|
const std::string& user_output_holding_hex,
|
|
|
|
|
const nlohmann::json& amount_in,
|
|
|
|
|
const nlohmann::json& min_out,
|
|
|
|
|
const nlohmann::json& deadline) {
|
|
|
|
|
std::string amount_in_decimal;
|
|
|
|
|
std::string min_out_decimal;
|
|
|
|
|
std::string deadline_decimal;
|
|
|
|
|
if (!jsonAmountToDecimal(amount_in, amount_in_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(min_out, min_out_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(deadline, deadline_decimal)) {
|
|
|
|
|
AMM_TRACE("swapExactInput: FAIL amount/deadline not a number or decimal string");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty()) {
|
|
|
|
|
AMM_TRACE("swapExactInput: FAIL no program id (AMM_PROGRAM_BIN unset/unreadable)");
|
2026-07-22 13:56:42 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
const json config = readConfig(amm_program_id);
|
2026-07-22 13:56:42 +02:00
|
|
|
if (config.is_null()) {
|
|
|
|
|
AMM_TRACE("swapExactInput: FAIL config_id op failed");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 22:42:26 +02:00
|
|
|
// tokenList() moved app-side, so token/holding ids arrive as configured
|
|
|
|
|
// (base58 or hex); normalize to the hex the FFI expects.
|
|
|
|
|
const std::string def_a = normalizeAccountId(def_a_hex);
|
|
|
|
|
const std::string def_b = normalizeAccountId(def_b_hex);
|
|
|
|
|
const std::string input_holding = normalizeAccountId(user_input_holding_hex);
|
|
|
|
|
const std::string output_holding = normalizeAccountId(user_output_holding_hex);
|
|
|
|
|
if (def_a.empty() || def_b.empty() || input_holding.empty() || output_holding.empty()) {
|
|
|
|
|
AMM_TRACE("swapExactInput: FAIL invalid token/holding id");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 14:14:15 +02:00
|
|
|
// Read the pool so the plan can use its stored vault ids (the guest asserts
|
|
|
|
|
// the vaults in the pool's creation order — see amm_swap_exact_in_plan).
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
2026-08-12 16:19:40 +02:00
|
|
|
{"ammProgramId", amm_program_id},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", def_a},
|
|
|
|
|
{"tokenOutId", def_b},
|
2026-08-06 14:14:15 +02:00
|
|
|
});
|
|
|
|
|
if (!poolId.ok) {
|
|
|
|
|
AMM_TRACE("swapExactInput: FAIL amm_pool_id");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
// amm_swap_exact_in_plan resolves the pool accounts, encodes SwapExactInput,
|
|
|
|
|
// and returns a ready-to-submit plan.
|
2026-08-04 19:21:29 +02:00
|
|
|
const FfiResult planResult = call(amm_swap_exact_in_plan, json{
|
2026-08-12 16:19:40 +02:00
|
|
|
{"ammProgramId", amm_program_id},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", def_a},
|
|
|
|
|
{"tokenOutId", def_b},
|
2026-07-22 13:56:42 +02:00
|
|
|
{"config", config},
|
2026-08-06 14:14:15 +02:00
|
|
|
{"poolData", pool_data},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"userInputHoldingId", input_holding},
|
|
|
|
|
{"userOutputHoldingId", output_holding},
|
2026-07-22 13:56:42 +02:00
|
|
|
{"amountIn", amount_in_decimal},
|
|
|
|
|
{"minOut", min_out_decimal},
|
|
|
|
|
{"deadlineMs", deadline_decimal},
|
|
|
|
|
});
|
2026-08-04 22:02:22 +02:00
|
|
|
if (!planResult.ok) {
|
|
|
|
|
AMM_TRACE("swapExactInput: FAIL amm_swap_exact_in_plan: " << planResult.error);
|
2026-07-22 13:56:42 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("swapExactInput: SUBMIT programId=" << program_id
|
|
|
|
|
<< " instrBytes=" << instruction.size() << " accounts=" << accounts.size());
|
|
|
|
|
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("swapExactInput: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false)) {
|
|
|
|
|
AMM_TRACE("swapExactInput: FAIL tx not successful");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
return jStr(obj, "tx_hash");
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 22:05:35 +02:00
|
|
|
std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
|
|
|
|
|
const std::string& def_b_hex,
|
|
|
|
|
const std::string& user_input_holding_hex,
|
|
|
|
|
const std::string& user_output_holding_hex,
|
|
|
|
|
const nlohmann::json& amount_out,
|
|
|
|
|
const nlohmann::json& max_in,
|
|
|
|
|
const nlohmann::json& deadline) {
|
|
|
|
|
std::string amount_out_decimal;
|
|
|
|
|
std::string max_in_decimal;
|
|
|
|
|
std::string deadline_decimal;
|
|
|
|
|
if (!jsonAmountToDecimal(amount_out, amount_out_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(max_in, max_in_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(deadline, deadline_decimal)) {
|
|
|
|
|
AMM_TRACE("swapExactOutput: FAIL amount/deadline not a number or decimal string");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty()) {
|
|
|
|
|
AMM_TRACE("swapExactOutput: FAIL no program id (AMM_PROGRAM_BIN unset/unreadable)");
|
2026-08-04 22:05:35 +02:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 16:19:40 +02:00
|
|
|
const json config = readConfig(amm_program_id);
|
2026-08-04 22:05:35 +02:00
|
|
|
if (config.is_null()) {
|
|
|
|
|
AMM_TRACE("swapExactOutput: FAIL config_id op failed");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 22:42:26 +02:00
|
|
|
// tokenList() moved app-side, so token/holding ids arrive as configured
|
|
|
|
|
// (base58 or hex); normalize to the hex the FFI expects.
|
|
|
|
|
const std::string def_a = normalizeAccountId(def_a_hex);
|
|
|
|
|
const std::string def_b = normalizeAccountId(def_b_hex);
|
|
|
|
|
const std::string input_holding = normalizeAccountId(user_input_holding_hex);
|
|
|
|
|
const std::string output_holding = normalizeAccountId(user_output_holding_hex);
|
|
|
|
|
|
2026-08-04 22:05:35 +02:00
|
|
|
// Read the pool so the plan can use its stored vault ids (the guest asserts
|
|
|
|
|
// the vaults in the pool's creation order — see amm_swap_exact_out_plan).
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
2026-08-12 16:19:40 +02:00
|
|
|
{"ammProgramId", amm_program_id},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", def_a},
|
|
|
|
|
{"tokenOutId", def_b},
|
2026-08-04 22:05:35 +02:00
|
|
|
});
|
|
|
|
|
if (!poolId.ok) {
|
|
|
|
|
AMM_TRACE("swapExactOutput: FAIL amm_pool_id");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
// amm_swap_exact_out_plan resolves the pool accounts, encodes SwapExactOutput,
|
|
|
|
|
// and returns a ready-to-submit plan.
|
|
|
|
|
const FfiResult planResult = call(amm_swap_exact_out_plan, json{
|
2026-08-12 16:19:40 +02:00
|
|
|
{"ammProgramId", amm_program_id},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"tokenInId", def_a},
|
|
|
|
|
{"tokenOutId", def_b},
|
2026-08-04 22:05:35 +02:00
|
|
|
{"config", config},
|
|
|
|
|
{"poolData", pool_data},
|
2026-08-13 22:42:26 +02:00
|
|
|
{"userInputHoldingId", input_holding},
|
|
|
|
|
{"userOutputHoldingId", output_holding},
|
2026-08-04 22:05:35 +02:00
|
|
|
{"amountOut", amount_out_decimal},
|
|
|
|
|
{"maxIn", max_in_decimal},
|
|
|
|
|
{"deadlineMs", deadline_decimal},
|
|
|
|
|
});
|
|
|
|
|
if (!planResult.ok) {
|
|
|
|
|
AMM_TRACE("swapExactOutput: FAIL amm_swap_exact_out_plan: " << planResult.error);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("swapExactOutput: SUBMIT programId=" << program_id
|
|
|
|
|
<< " instrBytes=" << instruction.size() << " accounts=" << accounts.size());
|
|
|
|
|
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("swapExactOutput: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false)) {
|
|
|
|
|
AMM_TRACE("swapExactOutput: FAIL tx not successful");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
return jStr(obj, "tx_hash");
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 19:29:39 +02:00
|
|
|
LogosMap AmmModuleImpl::createPoolQuote(const LogosMap& request) {
|
2026-08-07 11:56:37 +02:00
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Pure preview — no program id / chain reads / fee. Normalize the pair to hex (the
|
2026-08-12 16:19:40 +02:00
|
|
|
// liquidity UI sources base58 ids from resolveTokens).
|
2026-08-07 11:56:37 +02:00
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty())
|
|
|
|
|
return error("invalid_token_id");
|
|
|
|
|
|
2026-08-17 13:24:17 +02:00
|
|
|
// amountA/amountB arrive as a JSON number (CLI) or decimal string (UI);
|
2026-08-07 11:56:37 +02:00
|
|
|
// coerce to canonical decimal strings (rejects floats — see jsonAmountToDecimal).
|
|
|
|
|
// If an amount field is present but malformed, return bad_amount; otherwise leave it
|
|
|
|
|
// out so the FFI returns amount_required.
|
|
|
|
|
json quoteRequest = {
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
|
|
|
|
};
|
2026-08-17 13:24:17 +02:00
|
|
|
// price is the Q64.64 opening price; used when no amounts are supplied
|
2026-08-10 15:11:35 +02:00
|
|
|
// (price-only ⇒ the op returns the minimum opening deposit). Left out if absent.
|
|
|
|
|
std::string price_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (jsonAmountToDecimal(request.value("price", json()), price_decimal))
|
|
|
|
|
quoteRequest["price"] = price_decimal;
|
|
|
|
|
if (request.contains("amountA")) {
|
2026-08-07 11:56:37 +02:00
|
|
|
std::string amount_a_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (!jsonAmountToDecimal(request.at("amountA"), amount_a_decimal))
|
2026-08-07 11:56:37 +02:00
|
|
|
return error("bad_amount");
|
2026-08-17 13:24:17 +02:00
|
|
|
quoteRequest["amountA"] = amount_a_decimal;
|
2026-08-07 11:56:37 +02:00
|
|
|
}
|
2026-08-17 13:24:17 +02:00
|
|
|
if (request.contains("amountB")) {
|
2026-08-07 11:56:37 +02:00
|
|
|
std::string amount_b_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (!jsonAmountToDecimal(request.at("amountB"), amount_b_decimal))
|
2026-08-07 11:56:37 +02:00
|
|
|
return error("bad_amount");
|
2026-08-17 13:24:17 +02:00
|
|
|
quoteRequest["amountB"] = amount_b_decimal;
|
2026-08-07 11:56:37 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 19:29:39 +02:00
|
|
|
const FfiResult quoteResult = call(amm_create_pool_quote, quoteRequest);
|
2026-08-07 11:56:37 +02:00
|
|
|
if (!quoteResult.ok)
|
|
|
|
|
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
|
|
|
|
|
|
2026-08-17 13:24:17 +02:00
|
|
|
// Success: wrap { actualAmountA, actualAmountB, minimumAmountA,
|
|
|
|
|
// minimumAmountB, expectedLp, lockedLp, price } in the envelope.
|
2026-08-07 11:56:37 +02:00
|
|
|
LogosMap out = quoteResult.value;
|
|
|
|
|
out["status"] = "ok";
|
|
|
|
|
out["error"] = "";
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosMap AmmModuleImpl::createPool(const LogosMap& request) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// config_missing == no program id from AMM_PROGRAM_BIN (same as swapExactInQuote).
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
// amm_create_pool_plan needs the config account for the twap program id the
|
|
|
|
|
// current-tick PDA derives from; a bad/absent config surfaces from the plan as
|
|
|
|
|
// config_unavailable (no bespoke check here — same as the swap plans).
|
|
|
|
|
const FfiResult configResult =
|
|
|
|
|
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
|
|
|
|
if (!configResult.ok)
|
|
|
|
|
return error("backend_error");
|
|
|
|
|
const json config = readPublicAccount(jStr(configResult.value, "configId"));
|
|
|
|
|
|
|
|
|
|
// Normalize the pair + user holdings (incl. the caller-provided LP holding) to hex
|
|
|
|
|
// (base58 tolerated — transitional). A new pool has no pre-existing LP holding, so
|
|
|
|
|
// lpHoldingId is a fresh account the caller supplies; an empty/invalid id fails here.
|
|
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
const std::string holding_a = normalizeAccountId(jStr(request, "holdingAId"));
|
|
|
|
|
const std::string holding_b = normalizeAccountId(jStr(request, "holdingBId"));
|
|
|
|
|
const std::string user_lp = normalizeAccountId(jStr(request, "lpHoldingId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty() || holding_a.empty() || holding_b.empty()
|
|
|
|
|
|| user_lp.empty())
|
|
|
|
|
return error("invalid_account_id");
|
|
|
|
|
|
|
|
|
|
std::string amount_a_decimal;
|
|
|
|
|
std::string amount_b_decimal;
|
|
|
|
|
std::string deadline_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (!jsonAmountToDecimal(request.value("amountA", json()), amount_a_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(request.value("amountB", json()), amount_b_decimal)
|
2026-08-07 11:56:37 +02:00
|
|
|
|| !jsonAmountToDecimal(request.value("deadlineMs", json()), deadline_decimal))
|
|
|
|
|
return error("bad_amount");
|
|
|
|
|
|
|
|
|
|
// feeBps deserializes into a u32 in the plan request, so a missing / null / float / string
|
|
|
|
|
// value would fail the FFI's serde parse and leak an "invalid request JSON" error instead of
|
|
|
|
|
// a stable code. Require a JSON integer here; fee-tier support is validated in the plan op.
|
|
|
|
|
const json fee_val = request.value("feeBps", json());
|
|
|
|
|
if (!fee_val.is_number_integer())
|
|
|
|
|
return error("bad_fee_bps_amount");
|
|
|
|
|
|
|
|
|
|
// amm_create_pool_plan resolves the pool accounts (canonicalizing the pair),
|
|
|
|
|
// encodes NewDefinition (with the fee), and returns a ready-to-submit plan.
|
|
|
|
|
const FfiResult planResult = call(amm_create_pool_plan, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
2026-08-17 13:24:17 +02:00
|
|
|
{"amountA", amount_a_decimal},
|
|
|
|
|
{"amountB", amount_b_decimal},
|
2026-08-07 11:56:37 +02:00
|
|
|
{"feeBps", fee_val},
|
|
|
|
|
{"deadlineMs", deadline_decimal},
|
|
|
|
|
{"userHoldingAId", holding_a},
|
|
|
|
|
{"userHoldingBId", holding_b},
|
|
|
|
|
{"userHoldingLpId", user_lp},
|
|
|
|
|
});
|
|
|
|
|
if (!planResult.ok)
|
|
|
|
|
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("createPool: SUBMIT programId=" << program_id
|
|
|
|
|
<< " instrBytes=" << instruction.size() << " accounts=" << accounts.size());
|
|
|
|
|
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("createPool: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false))
|
|
|
|
|
return error("wallet_submission_failed");
|
|
|
|
|
|
|
|
|
|
// The native tx hash (64-char hex) is returned as-is — hex everywhere.
|
|
|
|
|
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 22:33:25 +02:00
|
|
|
LogosMap AmmModuleImpl::addLiquidityQuote(const LogosMap& request) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Normalize the pair to hex (the liquidity UI still sources base58 ids; transitional).
|
|
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty())
|
|
|
|
|
return error("invalid_token_id");
|
|
|
|
|
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
std::string max_a_decimal;
|
|
|
|
|
std::string max_b_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (!jsonAmountToDecimal(request.value("maxAmountA", json()), max_a_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(request.value("maxAmountB", json()), max_b_decimal))
|
2026-08-08 22:33:25 +02:00
|
|
|
return error("bad_amount");
|
|
|
|
|
|
|
|
|
|
// Derive the pool id (config-free) and read the pool account; its raw data is handed
|
|
|
|
|
// to the pricing op. An absent account has no data → `no_pool`.
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"tokenInId", token_a},
|
|
|
|
|
{"tokenOutId", token_b},
|
|
|
|
|
});
|
|
|
|
|
if (!poolId.ok)
|
|
|
|
|
return error(poolId.error.empty() ? "backend_error" : poolId.error);
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
2026-08-10 11:03:16 +02:00
|
|
|
// slippageBps is a fraction of 100% in basis points; the pricing op uses it to derive
|
2026-08-17 13:24:17 +02:00
|
|
|
// minimumLp (the LP floor the submit accepts). Require an integer JSON number and reject
|
2026-08-10 11:03:16 +02:00
|
|
|
// everything else with a stable invalid_slippage: is_number() would also accept a float
|
|
|
|
|
// (and get<int64_t>() on a number_float THROWS, terminating the module), while a string /
|
|
|
|
|
// bool would otherwise fall through to a silent 0. A missing field defaults to 0 (no
|
|
|
|
|
// slippage). A negative or >= 100% value is likewise invalid_slippage.
|
|
|
|
|
const json slippage_val = request.value("slippageBps", json(0));
|
|
|
|
|
if (!slippage_val.is_number_integer())
|
|
|
|
|
return error("invalid_slippage");
|
|
|
|
|
const int64_t slippage_bps = slippage_val.get<int64_t>();
|
|
|
|
|
if (slippage_bps < 0 || slippage_bps >= 10000)
|
|
|
|
|
return error("invalid_slippage");
|
|
|
|
|
|
2026-08-08 22:33:25 +02:00
|
|
|
const FfiResult quoteResult = call(amm_add_liquidity_quote, json{
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
2026-08-17 13:24:17 +02:00
|
|
|
{"maxAmountA", max_a_decimal},
|
|
|
|
|
{"maxAmountB", max_b_decimal},
|
2026-08-10 11:03:16 +02:00
|
|
|
{"slippageBps", slippage_bps},
|
2026-08-08 22:33:25 +02:00
|
|
|
{"poolData", pool_data},
|
|
|
|
|
});
|
|
|
|
|
if (!quoteResult.ok)
|
|
|
|
|
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
|
|
|
|
|
|
2026-08-21 09:58:27 +02:00
|
|
|
// Success: wrap { amountA, amountB, expectedLp, minimumLp, price, lpDefinitionId }.
|
|
|
|
|
// (lpDefinitionId is the pool's LP token so the UI can offer existing LP holdings.)
|
2026-08-08 22:33:25 +02:00
|
|
|
LogosMap out = quoteResult.value;
|
|
|
|
|
out["status"] = "ok";
|
|
|
|
|
out["error"] = "";
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosMap AmmModuleImpl::addLiquidity(const LogosMap& request) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// config_missing == no program id from AMM_PROGRAM_BIN (same as createPool).
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
// amm_add_liquidity_plan needs the config account for the twap program id the
|
|
|
|
|
// current-tick PDA derives from; a bad/absent config surfaces from the plan.
|
|
|
|
|
const FfiResult configResult =
|
|
|
|
|
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
|
|
|
|
if (!configResult.ok)
|
|
|
|
|
return error("backend_error");
|
|
|
|
|
const json config = readPublicAccount(jStr(configResult.value, "configId"));
|
|
|
|
|
|
|
|
|
|
// Normalize the pair + user holdings (incl. the LP holding that receives the minted LP)
|
|
|
|
|
// to hex (base58 tolerated — transitional).
|
|
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
const std::string holding_a = normalizeAccountId(jStr(request, "holdingAId"));
|
|
|
|
|
const std::string holding_b = normalizeAccountId(jStr(request, "holdingBId"));
|
|
|
|
|
const std::string user_lp = normalizeAccountId(jStr(request, "lpHoldingId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty() || holding_a.empty() || holding_b.empty()
|
|
|
|
|
|| user_lp.empty())
|
|
|
|
|
return error("invalid_account_id");
|
|
|
|
|
|
|
|
|
|
std::string max_a_decimal;
|
|
|
|
|
std::string max_b_decimal;
|
|
|
|
|
std::string min_lp_decimal;
|
|
|
|
|
std::string deadline_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (!jsonAmountToDecimal(request.value("maxAmountA", json()), max_a_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(request.value("maxAmountB", json()), max_b_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(request.value("minLp", json()), min_lp_decimal)
|
2026-08-08 22:33:25 +02:00
|
|
|
|| !jsonAmountToDecimal(request.value("deadlineMs", json()), deadline_decimal))
|
|
|
|
|
return error("bad_amount");
|
|
|
|
|
|
|
|
|
|
// Read the pool so the plan can use its stored vault / LP-definition ids (the guest
|
|
|
|
|
// asserts the provided vaults/LP against them — see amm_add_liquidity_plan).
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"tokenInId", token_a},
|
|
|
|
|
{"tokenOutId", token_b},
|
|
|
|
|
});
|
|
|
|
|
if (!poolId.ok)
|
|
|
|
|
return error(poolId.error.empty() ? "backend_error" : poolId.error);
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
// amm_add_liquidity_plan resolves the pool accounts (canonicalizing the pair), encodes
|
|
|
|
|
// AddLiquidity (with the slippage floor), and returns a ready-to-submit plan.
|
|
|
|
|
const FfiResult planResult = call(amm_add_liquidity_plan, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
2026-08-17 13:24:17 +02:00
|
|
|
{"maxAmountA", max_a_decimal},
|
|
|
|
|
{"maxAmountB", max_b_decimal},
|
|
|
|
|
{"minLp", min_lp_decimal},
|
2026-08-08 22:33:25 +02:00
|
|
|
{"deadlineMs", deadline_decimal},
|
|
|
|
|
{"userHoldingAId", holding_a},
|
|
|
|
|
{"userHoldingBId", holding_b},
|
|
|
|
|
{"userHoldingLpId", user_lp},
|
|
|
|
|
{"poolData", pool_data},
|
|
|
|
|
});
|
|
|
|
|
if (!planResult.ok)
|
|
|
|
|
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("addLiquidity: SUBMIT programId=" << program_id
|
|
|
|
|
<< " instrBytes=" << instruction.size() << " accounts=" << accounts.size());
|
|
|
|
|
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("addLiquidity: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false))
|
|
|
|
|
return error("wallet_submission_failed");
|
|
|
|
|
|
|
|
|
|
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 17:58:45 +02:00
|
|
|
LogosMap AmmModuleImpl::removeLiquidityQuote(const LogosMap& request) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Normalize the pair to hex (the liquidity UI still sources base58 ids; transitional).
|
|
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty())
|
|
|
|
|
return error("invalid_token_id");
|
|
|
|
|
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
std::string lp_amount_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (!jsonAmountToDecimal(request.value("lpAmount", json()), lp_amount_decimal))
|
2026-08-10 17:58:45 +02:00
|
|
|
return error("bad_amount");
|
|
|
|
|
|
|
|
|
|
// Derive the pool id (config-free) and read the pool account; its raw data is handed to
|
|
|
|
|
// the pricing op. An absent account has no data → `no_pool`.
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"tokenInId", token_a},
|
|
|
|
|
{"tokenOutId", token_b},
|
|
|
|
|
});
|
|
|
|
|
if (!poolId.ok)
|
|
|
|
|
return error(poolId.error.empty() ? "backend_error" : poolId.error);
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
// slippageBps derives the minimumAmount*Raw floors the submit enforces. Require an integer
|
|
|
|
|
// JSON number and reject everything else with a stable invalid_slippage (same as
|
|
|
|
|
// addLiquidityQuote): is_number() would also accept a float (and get<int64_t>() on a
|
|
|
|
|
// number_float THROWS, terminating the module), while a string / bool would fall through to a
|
|
|
|
|
// silent 0. A missing field defaults to 0 (no slippage). Negative or >= 100% is likewise
|
|
|
|
|
// invalid_slippage.
|
|
|
|
|
const json slippage_val = request.value("slippageBps", json(0));
|
|
|
|
|
if (!slippage_val.is_number_integer())
|
|
|
|
|
return error("invalid_slippage");
|
|
|
|
|
const int64_t slippage_bps = slippage_val.get<int64_t>();
|
|
|
|
|
if (slippage_bps < 0 || slippage_bps >= 10000)
|
|
|
|
|
return error("invalid_slippage");
|
|
|
|
|
|
|
|
|
|
const FfiResult quoteResult = call(amm_remove_liquidity_quote, json{
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
2026-08-17 13:24:17 +02:00
|
|
|
{"lpAmount", lp_amount_decimal},
|
2026-08-10 17:58:45 +02:00
|
|
|
{"slippageBps", slippage_bps},
|
|
|
|
|
{"poolData", pool_data},
|
|
|
|
|
});
|
|
|
|
|
if (!quoteResult.ok)
|
|
|
|
|
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
|
|
|
|
|
|
2026-08-17 13:24:17 +02:00
|
|
|
// Success: wrap { amountA, amountB, minimumAmountA, minimumAmountB, price }.
|
2026-08-10 17:58:45 +02:00
|
|
|
LogosMap out = quoteResult.value;
|
|
|
|
|
out["status"] = "ok";
|
|
|
|
|
out["error"] = "";
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosMap AmmModuleImpl::removeLiquidity(const LogosMap& request) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// config_missing == no program id from AMM_PROGRAM_BIN (same as addLiquidity).
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
// amm_remove_liquidity_plan needs the config account for the twap program id the
|
|
|
|
|
// current-tick PDA derives from; a bad/absent config surfaces from the plan.
|
|
|
|
|
const FfiResult configResult =
|
|
|
|
|
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
|
|
|
|
if (!configResult.ok)
|
|
|
|
|
return error("backend_error");
|
|
|
|
|
const json config = readPublicAccount(jStr(configResult.value, "configId"));
|
|
|
|
|
|
|
|
|
|
// Normalize the pair + user holdings. Unlike add/create there is no fresh account: the LP
|
|
|
|
|
// holding already exists (it is burned) and the token a/b holdings receive the withdrawal.
|
|
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
const std::string holding_a = normalizeAccountId(jStr(request, "holdingAId"));
|
|
|
|
|
const std::string holding_b = normalizeAccountId(jStr(request, "holdingBId"));
|
|
|
|
|
const std::string user_lp = normalizeAccountId(jStr(request, "lpHoldingId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty() || holding_a.empty() || holding_b.empty()
|
|
|
|
|
|| user_lp.empty())
|
|
|
|
|
return error("invalid_account_id");
|
|
|
|
|
|
|
|
|
|
std::string lp_amount_decimal;
|
|
|
|
|
std::string min_a_decimal;
|
|
|
|
|
std::string min_b_decimal;
|
|
|
|
|
std::string deadline_decimal;
|
2026-08-17 13:24:17 +02:00
|
|
|
if (!jsonAmountToDecimal(request.value("lpAmount", json()), lp_amount_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(request.value("minAmountA", json()), min_a_decimal)
|
|
|
|
|
|| !jsonAmountToDecimal(request.value("minAmountB", json()), min_b_decimal)
|
2026-08-10 17:58:45 +02:00
|
|
|
|| !jsonAmountToDecimal(request.value("deadlineMs", json()), deadline_decimal))
|
|
|
|
|
return error("bad_amount");
|
|
|
|
|
|
|
|
|
|
// Read the pool so the plan can use its stored vault / LP-definition ids (the guest
|
|
|
|
|
// asserts the provided vaults/LP against them — see amm_remove_liquidity_plan).
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"tokenInId", token_a},
|
|
|
|
|
{"tokenOutId", token_b},
|
|
|
|
|
});
|
|
|
|
|
if (!poolId.ok)
|
|
|
|
|
return error(poolId.error.empty() ? "backend_error" : poolId.error);
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
// amm_remove_liquidity_plan resolves the pool accounts, encodes RemoveLiquidity (with the
|
|
|
|
|
// per-side slippage floors + caller deadline), and returns a ready-to-submit plan.
|
|
|
|
|
const FfiResult planResult = call(amm_remove_liquidity_plan, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
2026-08-17 13:24:17 +02:00
|
|
|
{"lpAmount", lp_amount_decimal},
|
|
|
|
|
{"minAmountA", min_a_decimal},
|
|
|
|
|
{"minAmountB", min_b_decimal},
|
2026-08-10 17:58:45 +02:00
|
|
|
{"deadlineMs", deadline_decimal},
|
|
|
|
|
{"userHoldingAId", holding_a},
|
|
|
|
|
{"userHoldingBId", holding_b},
|
|
|
|
|
{"userHoldingLpId", user_lp},
|
|
|
|
|
{"poolData", pool_data},
|
|
|
|
|
});
|
|
|
|
|
if (!planResult.ok)
|
|
|
|
|
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("removeLiquidity: SUBMIT programId=" << program_id
|
|
|
|
|
<< " instrBytes=" << instruction.size() << " accounts=" << accounts.size());
|
|
|
|
|
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("removeLiquidity: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false))
|
|
|
|
|
return error("wallet_submission_failed");
|
|
|
|
|
|
|
|
|
|
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 22:36:45 +02:00
|
|
|
LogosMap AmmModuleImpl::syncReserves(const LogosMap& request) {
|
|
|
|
|
auto error = [](const std::string& err) {
|
|
|
|
|
return LogosMap{{"status", "error"}, {"error", err}};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// config_missing == no program id from AMM_PROGRAM_BIN (same as the other submits).
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return error("config_missing");
|
|
|
|
|
|
|
|
|
|
// amm_sync_reserves_plan needs the config account for the twap program id the current-tick
|
|
|
|
|
// PDA derives from; a bad/absent config surfaces from the plan.
|
|
|
|
|
const FfiResult configResult =
|
|
|
|
|
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
|
|
|
|
if (!configResult.ok)
|
|
|
|
|
return error("backend_error");
|
|
|
|
|
const json config = readPublicAccount(jStr(configResult.value, "configId"));
|
|
|
|
|
|
|
|
|
|
// Normalize the pair to hex (transitional). Sync has no user inputs beyond the pair — no
|
|
|
|
|
// holdings, amounts, or deadline, and nothing signs.
|
|
|
|
|
const std::string token_a = normalizeAccountId(jStr(request, "tokenAId"));
|
|
|
|
|
const std::string token_b = normalizeAccountId(jStr(request, "tokenBId"));
|
|
|
|
|
if (token_a.empty() || token_b.empty())
|
|
|
|
|
return error("invalid_token_id");
|
|
|
|
|
|
|
|
|
|
// Read the pool so the plan can use its stored vault ids (the guest asserts the provided
|
|
|
|
|
// vaults against them — see amm_sync_reserves_plan).
|
|
|
|
|
const FfiResult poolId = call(amm_pool_id, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"tokenInId", token_a},
|
|
|
|
|
{"tokenOutId", token_b},
|
|
|
|
|
});
|
|
|
|
|
if (!poolId.ok)
|
|
|
|
|
return error(poolId.error.empty() ? "backend_error" : poolId.error);
|
|
|
|
|
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
|
|
|
|
|
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
|
|
|
|
|
|
|
|
|
|
const FfiResult planResult = call(amm_sync_reserves_plan, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"tokenAId", token_a},
|
|
|
|
|
{"tokenBId", token_b},
|
|
|
|
|
{"poolData", pool_data},
|
|
|
|
|
});
|
|
|
|
|
if (!planResult.ok)
|
|
|
|
|
return error(planResult.error.empty() ? "backend_error" : planResult.error);
|
|
|
|
|
const json plan = planResult.value;
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
|
|
|
|
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
|
|
|
|
const std::vector<uint8_t> instruction = jsonWordsToLeBytes(plan.value("instruction", json::array()));
|
|
|
|
|
const std::string program_id = jStr(plan, "programId");
|
|
|
|
|
|
|
|
|
|
AMM_TRACE("syncReserves: SUBMIT programId=" << program_id
|
|
|
|
|
<< " instrBytes=" << instruction.size() << " accounts=" << accounts.size());
|
|
|
|
|
|
|
|
|
|
const std::string reply = modules().logos_execution_zone.send_generic_public_transaction(
|
|
|
|
|
accounts, signers, instruction, program_id);
|
|
|
|
|
AMM_TRACE("syncReserves: tx reply=" << reply);
|
|
|
|
|
|
|
|
|
|
const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
if (!obj.is_object() || !obj.value("success", false))
|
|
|
|
|
return error("wallet_submission_failed");
|
|
|
|
|
|
|
|
|
|
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 14:44:41 +02:00
|
|
|
LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) {
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return LogosList::array();
|
|
|
|
|
|
|
|
|
|
// The config gives the token_program_id that identifies which wallet accounts
|
|
|
|
|
// are token holdings (decoded by the FFI op).
|
|
|
|
|
const FfiResult configResult =
|
|
|
|
|
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
|
|
|
|
if (!configResult.ok)
|
|
|
|
|
return LogosList::array();
|
|
|
|
|
const json config = readPublicAccount(jStr(configResult.value, "configId"));
|
|
|
|
|
|
|
|
|
|
// Fresh wallet read each call — the selector wants current holdings/balances.
|
2026-08-12 16:19:40 +02:00
|
|
|
const json wallet_accounts = walletAccountReads(wallet_open);
|
2026-08-08 14:44:41 +02:00
|
|
|
|
|
|
|
|
const FfiResult result = call(amm_token_holdings, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"walletAccounts", wallet_accounts},
|
|
|
|
|
});
|
|
|
|
|
if (!result.ok)
|
|
|
|
|
return LogosList::array();
|
|
|
|
|
|
|
|
|
|
LogosList out = LogosList::array();
|
|
|
|
|
const auto it = result.value.find("holdings");
|
|
|
|
|
if (it != result.value.end() && it->is_array())
|
|
|
|
|
for (const auto& holding : *it) out.push_back(holding);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 10:25:48 +02:00
|
|
|
LogosList AmmModuleImpl::feeTiers() {
|
|
|
|
|
// Pure enumeration of amm_core::SUPPORTED_FEE_TIERS — no program id, config,
|
|
|
|
|
// or wallet read needed. The FFI wraps the list as { feeTiers: [...] }.
|
|
|
|
|
const FfiResult result = call(amm_fee_tiers, json::object());
|
|
|
|
|
if (!result.ok)
|
|
|
|
|
return LogosList::array();
|
|
|
|
|
|
|
|
|
|
LogosList out = LogosList::array();
|
|
|
|
|
const auto it = result.value.find("feeTiers");
|
|
|
|
|
if (it != result.value.end() && it->is_array())
|
|
|
|
|
for (const auto& tier : *it) out.push_back(tier);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 15:23:28 +02:00
|
|
|
LogosList AmmModuleImpl::resolveTokens(const LogosMap& request, bool wallet_open) {
|
|
|
|
|
const std::string amm_program_id = ammProgramId();
|
|
|
|
|
if (amm_program_id.empty())
|
|
|
|
|
return LogosList::array();
|
|
|
|
|
|
|
|
|
|
// The config gives the token_program_id the FFI needs to decode definitions/holdings.
|
|
|
|
|
const FfiResult configResult =
|
|
|
|
|
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
|
|
|
|
if (!configResult.ok)
|
|
|
|
|
return LogosList::array();
|
|
|
|
|
const json config = readPublicAccount(jStr(configResult.value, "configId"));
|
|
|
|
|
|
|
|
|
|
// Normalize the app-provided ids (base58 or hex) → hex, de-dup, and read each
|
|
|
|
|
// definition account. The FFI is stateless, so it gets the reads pre-fetched.
|
|
|
|
|
const auto token_ids_it = request.find("tokenIds");
|
|
|
|
|
const json token_ids = (token_ids_it != request.end() && token_ids_it->is_array())
|
|
|
|
|
? *token_ids_it
|
|
|
|
|
: json::array();
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> ids_vec;
|
|
|
|
|
ids_vec.reserve(token_ids.size());
|
|
|
|
|
for (const auto& raw : token_ids) {
|
|
|
|
|
if (!raw.is_string()) continue;
|
|
|
|
|
const std::string hex = normalizeAccountId(raw.get<std::string>());
|
|
|
|
|
if (!hex.empty()) ids_vec.push_back(hex);
|
|
|
|
|
}
|
|
|
|
|
std::sort(ids_vec.begin(), ids_vec.end());
|
|
|
|
|
ids_vec.erase(std::unique(ids_vec.begin(), ids_vec.end()), ids_vec.end());
|
|
|
|
|
|
|
|
|
|
json ids = json::array();
|
|
|
|
|
json definitions = json::array();
|
|
|
|
|
for (const auto& hex : ids_vec) {
|
|
|
|
|
ids.push_back(hex);
|
|
|
|
|
definitions.push_back(readPublicAccount(hex));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fresh wallet read — the selector wants current holdings/balances.
|
2026-08-12 16:19:40 +02:00
|
|
|
const json wallet_accounts = walletAccountReads(wallet_open);
|
2026-08-12 15:23:28 +02:00
|
|
|
|
|
|
|
|
const FfiResult result = call(amm_resolve_tokens, json{
|
|
|
|
|
{"ammProgramId", amm_program_id},
|
|
|
|
|
{"config", config},
|
|
|
|
|
{"tokenIds", ids},
|
|
|
|
|
{"walletAccounts", wallet_accounts},
|
|
|
|
|
{"tokenDefinitions", definitions},
|
|
|
|
|
});
|
|
|
|
|
if (!result.ok)
|
|
|
|
|
return LogosList::array();
|
|
|
|
|
|
|
|
|
|
LogosList out = LogosList::array();
|
|
|
|
|
const auto it = result.value.find("tokens");
|
|
|
|
|
if (it != result.value.end() && it->is_array())
|
|
|
|
|
for (const auto& row : *it) out.push_back(row);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|