refactor(amm): remove the dead Network/context machinery

The liquidity token surface moved app-side (resolveTokens + custom tokens),
leaving the whole newPositionContext path dormant. Delete it end to end and
point the swap methods at the same lean program-id helper everything else uses.
This commit is contained in:
r4bbit
2026-08-13 16:17:58 +02:00
parent 10b52ea2f6
commit cbb75c38fd
17 changed files with 88 additions and 923 deletions
+2 -98
View File
@@ -9,14 +9,8 @@ QtObject {
readonly property bool walletStateReady: root.backend !== null
&& root.backend.walletStateReady === true
readonly property var newPositionContext: root.walletStateReady
&& root.backend.newPositionContext
? root.backend.newPositionContext
: root.loadingContext()
readonly property var viewState: ({
"quote": root.newPositionQuote,
"contextLoading": root.contextLoading || !root.walletStateReady
|| root.newPositionContext.status === "loading",
"quoteLoading": root.quoteLoading,
"quoteStale": root.quoteStale,
"submitting": root.submitting,
@@ -24,29 +18,22 @@ QtObject {
// Create-vs-add routing signal, from the resolvePool read: true = add (pool exists),
// false = create, undefined = not resolved yet (a new pair, still resolving).
"poolExists": root.poolExists,
"errorCode": root.flowErrorCode || root.contextErrorCode
|| root.quoteErrorCode
"errorCode": root.flowErrorCode || root.quoteErrorCode
})
property var newPositionQuote: ({})
// Whether the selected pair's pool exists (from resolvePool); drives create-vs-add.
// undefined until the first resolve for the current pair lands.
property var poolExists: undefined
property var resolvedTokenIds: []
property int contextSerial: 0
property int quoteSerial: 0
property bool contextLoading: false
property bool quoteLoading: false
property bool quoteStale: true
property bool submitting: false
property string transactionId: ""
property string flowErrorCode: ""
property string contextErrorCode: ""
property string quoteErrorCode: ""
property var pendingQuoteRequest: ({ "ok": false, "request": ({}) })
signal tokenResolutionFinished(bool finalResponse)
signal tokenResolutionFailed(string code)
signal quoteRefreshRequested(bool immediate)
signal submitSucceeded
signal submitFailed
@@ -59,14 +46,7 @@ QtObject {
onTriggered: root.requestQuoteNow(root.quoteSerial)
}
onNewPositionContextChanged: root.invalidateQuote()
onWalletStateReadyChanged: {
++root.contextSerial
if (!root.walletStateReady)
root.contextLoading = false
root.invalidateQuote()
}
onWalletStateReadyChanged: root.invalidateQuote()
onActiveChanged: {
if (!root.active)
@@ -77,72 +57,6 @@ QtObject {
})
}
function contextHints(refreshWalletAccounts) {
const request = root.pendingQuoteRequest.request || {}
const recent = []
if (request.tokenAId)
recent.push(request.tokenAId)
if (request.tokenBId && request.tokenBId !== request.tokenAId)
recent.push(request.tokenBId)
return {
"recentTokenIds": recent,
"resolvedTokenIds": root.resolvedTokenIds,
"refreshWalletAccounts": refreshWalletAccounts === true
}
}
function refreshContext(refreshWalletAccounts, completed) {
const serial = ++root.contextSerial
root.contextLoading = true
if (!root.walletStateReady || root.runtime === null) {
root.contextLoading = false
return
}
root.runtime.watch(root.backend.refreshNewPositionContext(
root.contextHints(refreshWalletAccounts)),
function() {
root.finishContextRefresh(serial, completed)
},
function(error) {
root.failContextRefresh(serial)
})
}
function finishContextRefresh(serial, completed) {
if (serial !== root.contextSerial)
return
root.contextLoading = false
root.contextErrorCode = ""
Qt.callLater(function() {
if (serial !== root.contextSerial)
return
root.tokenResolutionFinished(true)
if (completed)
completed()
})
}
function failContextRefresh(serial) {
if (serial !== root.contextSerial)
return
root.contextLoading = false
root.contextErrorCode = "backend_error"
root.tokenResolutionFailed("backend_error")
}
function resolveToken(tokenId) {
const value = String(tokenId || "").trim()
if (value.length === 0)
return
if (root.resolvedTokenIds.indexOf(value) < 0) {
const next = root.resolvedTokenIds.slice(0)
next.push(value)
root.resolvedTokenIds = next
}
root.refreshContext(false)
}
function scheduleQuote(immediate, quoteRequest) {
++root.quoteSerial
root.pendingQuoteRequest = quoteRequest
@@ -361,7 +275,6 @@ QtObject {
root.submitting = false
root.transactionId = String(result.transactionId)
root.flowErrorCode = ""
root.contextErrorCode = ""
root.quoteErrorCode = ""
root.invalidateQuote()
root.submitSucceeded()
@@ -414,7 +327,6 @@ QtObject {
root.submitting = false
root.transactionId = String(result.transactionId)
root.flowErrorCode = ""
root.contextErrorCode = ""
root.quoteErrorCode = ""
root.invalidateQuote()
root.submitSucceeded()
@@ -443,7 +355,6 @@ QtObject {
root.invalidateQuote()
root.transactionId = ""
root.flowErrorCode = ""
root.contextErrorCode = ""
root.quoteErrorCode = ""
}
@@ -463,13 +374,6 @@ QtObject {
root.quoteStale = true
}
function loadingContext() {
return {
"status": "loading",
"tokens": []
}
}
function quoteError(code) {
return {
"status": "error",
+1 -49
View File
@@ -71,22 +71,9 @@ namespace {
}
return out;
}
// The new-position context placeholder published before the module
// connection is up (matches the module's "loading" contextState).
QVariantMap loadingContext()
{
return QVariantMap {
{ QStringLiteral("status"), QStringLiteral("loading") },
{ QStringLiteral("networkId"), QStringLiteral("lez") },
{ QStringLiteral("networkFingerprint"), QString() },
{ QStringLiteral("tokens"), QVariantList() },
{ QStringLiteral("feeTiers"), QVariantList() },
{ QStringLiteral("warnings"), QVariantList() },
};
}
}
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
: AmmUiBackendSimpleSource(parent),
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
@@ -148,7 +135,6 @@ void AmmUiBackend::disconnectWallet()
{
m_walletController->disconnect();
setWalletStateReady(true);
refreshNewPositionContext(QVariantMap());
}
QString AmmUiBackend::createAccountPublic()
@@ -176,28 +162,6 @@ QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
return m_walletController->balance(accountIdHex, isPublic);
}
QVariantMap AmmUiBackend::refreshNewPositionContext(QVariantMap request)
{
const bool refreshWalletAccounts =
request.take(QStringLiteral("refreshWalletAccounts")).toBool();
if (request.contains(QStringLiteral("recentTokenIds"))
|| request.contains(QStringLiteral("resolvedTokenIds"))) {
m_newPositionHints = request;
}
else {
request = m_newPositionHints;
}
if (!walletStateReady()) {
const QVariantMap context = loadingContext();
setNewPositionContext(context);
return context;
}
const QVariantMap context = m_logos->amm_module.newPositionContext(
request, isWalletOpen(), refreshWalletAccounts);
setNewPositionContext(context);
return context;
}
void AmmUiBackend::syncWalletState()
{
const WalletUiState& state = m_walletController->state();
@@ -211,18 +175,6 @@ void AmmUiBackend::syncWalletState()
setCurrentBlockHeight(state.currentBlockHeight);
setSequencerAddr(state.sequencerAddress);
setSequencerReachable(state.sequencerReachable);
publishNetworkContext();
}
void AmmUiBackend::publishNetworkContext()
{
if (!walletStateReady()) {
setNewPositionContext(loadingContext());
return;
}
setNewPositionContext(m_logos->amm_module.newPositionContext(
m_newPositionHints, isWalletOpen(), false));
}
QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
+1 -10
View File
@@ -46,7 +46,6 @@ public slots:
void refreshAccounts() override;
void refreshBalances() override;
QString getBalance(QString accountIdHex, bool isPublic) override;
QVariantMap refreshNewPositionContext(QVariantMap request) override;
// Return the new wallet's BIP39 mnemonic (empty string on failure) so the
// UI can force a one-time seed-phrase backup step.
QString createNewDefault(QString password) override;
@@ -101,24 +100,16 @@ private:
QStringList loadCustomTokenIds() const;
bool saveCustomTokenIds(const QStringList& ids) const;
QString customTokenStorePath() const;
// Publishes the new-position context PROP: a local "loading" placeholder
// until wallet state (and thus the module connection) is ready, then the
// module's newPositionContext for the current hints.
void publishNetworkContext();
LogosAPI* m_logosAPI;
// Handle for the amm_module core module (resolvePool / swapExactInput /
// tokenList / new-position). The module wraps the amm_ffi brain and
// tokenList / resolveTokens). The module wraps the amm_ffi brain and
// reaches the shared wallet through its own logos_execution_zone dependency;
// this backend keeps a thin LogosModules over the same LogosAPI as the
// wallet provider so both resolve that one shared wallet instance.
std::unique_ptr<LogosModules> m_logos;
std::unique_ptr<LogosWalletProvider> m_wallet;
std::unique_ptr<WalletController> m_walletController;
// Sticky new-position hints (recent/resolved token ids) so a bare
// republish (wallet-state change) keeps the user's last selection.
QVariantMap m_newPositionHints;
};
#endif // AMM_UI_BACKEND_H
-7
View File
@@ -26,13 +26,6 @@ class AmmUiBackend
SLOT(void refreshBalances())
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
// New Position backend surface. QML calls these through logos.watch(...).
// The QVariant payloads are stable maps/lists so the UI never assembles AMM
// transactions or duplicates quote state.
PROP(QVariantMap newPositionContext READONLY)
// Return the published context so the QML refresh watcher always settles.
SLOT(QVariantMap refreshNewPositionContext(QVariantMap request))
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
// fresh wallet at the canonical walletHome with no path picking. createNew()
// keeps explicit paths for an "advanced" flow. Both return the new wallet's
+5 -116
View File
@@ -11,21 +11,12 @@ TestCase {
name: "LiquidityPage"
Component {
id: backendComponent
id: pageComponent
QtObject {
property bool walletStateReady: false
property int contextRefreshCalls: 0
property var newPositionContext: ({
"status": "ready",
"tokens": [],
"feeTiers": []
})
function refreshNewPositionContext(request) {
++contextRefreshCalls
return newPositionContext
}
Pages.LiquidityPage {
visible: false
width: 800
height: 600
}
}
@@ -54,106 +45,4 @@ TestCase {
verify(form.width > 0)
verify(form.width <= page.width - 32)
}
Component {
id: runtimeComponent
QtObject {
function watch(value, succeeded, failed) {
if (value === undefined)
return
succeeded(value)
}
}
}
Component {
id: pageComponent
Pages.LiquidityPage {
visible: false
width: 800
height: 600
}
}
function test_contextWaitsForWalletState() {
var backend = createTemporaryObject(backendComponent, testCase)
var page = createTemporaryObject(pageComponent, testCase, {
"backend": backend
})
verify(backend)
verify(page)
compare(page.flow.newPositionContext.status, "loading")
backend.walletStateReady = true
tryCompare(page.flow.newPositionContext, "status", "ready")
}
function test_contextRefreshControlsWalletScan() {
var backend = createTemporaryObject(backendComponent, testCase, {
"walletStateReady": true
})
var page = createTemporaryObject(pageComponent, testCase, {
"backend": backend
})
verify(backend)
verify(page)
compare(page.flow.contextHints(false).refreshWalletAccounts, false)
compare(page.flow.contextHints(true).refreshWalletAccounts, true)
}
function test_refreshPositionCompletesAndReenablesTokenSelection() {
var backend = createTemporaryObject(backendComponent, testCase, {
"walletStateReady": true
})
var runtime = createTemporaryObject(runtimeComponent, testCase)
var page = createTemporaryObject(pageComponent, testCase, {
"backend": backend,
"runtime": runtime
})
verify(backend)
verify(runtime)
verify(page)
var refreshButton = findChild(page, "refreshPositionButton")
var tokenAInput = findChild(page, "tokenAAmountInput")
verify(refreshButton)
verify(tokenAInput)
refreshButton.clicked()
tryCompare(backend, "contextRefreshCalls", 1)
tryCompare(page.flow, "contextLoading", false)
compare(refreshButton.enabled, true)
compare(tokenAInput.tokenSelectionEnabled, true)
}
function test_staleContextCompletionCannotFinishNewerRefresh() {
var backend = createTemporaryObject(backendComponent, testCase, {
"walletStateReady": true
})
var page = createTemporaryObject(pageComponent, testCase, {
"backend": backend
})
verify(backend)
verify(page)
page.flow.contextSerial = 2
page.flow.contextLoading = true
page.flow.contextErrorCode = "newer_request_pending"
page.flow.finishContextRefresh(1, null)
page.flow.failContextRefresh(1)
compare(page.flow.contextLoading, true)
compare(page.flow.contextErrorCode, "newer_request_pending")
page.flow.finishContextRefresh(2, null)
compare(page.flow.contextLoading, false)
compare(page.flow.contextErrorCode, "")
}
}
+10 -11
View File
@@ -26,17 +26,16 @@ methods (the module API is generated from the header) are:
**Amount / id conventions** below.
- `tokenList()` — reads the `TOKENS_CONFIG` JSON array and returns it with
`definitionId`/`holding` normalized to hex.
- `newPositionContext(request, walletOpen, refreshWalletAccounts)` — the
add-liquidity view state (available tokens, fee tiers, warnings) as a
context map.
- `quoteNewPosition(request, walletOpen)` prices an add-liquidity request
against current on-chain state (read-only).
- `submitNewPosition(request, quoteHash, walletOpen, freshLpId)` — submits an
add-liquidity transaction. When the quote needs a fresh LP holding and
`freshLpId` is empty, returns `{ status: "requires_fresh_lp" }` **without**
submitting: the caller (the app backend, which owns the wallet keyset) creates
the account and calls again with its id. Headless callers pre-create an LP
holding and pass it.
- `resolveTokens(request, walletOpen)` — resolves an app-provided set of token
ids into selector rows (definition + wallet holding per id). The lean,
stateless successor to the removed `newPositionContext` path: the app owns the
id set, so there is no network envelope or process-cached wallet state here.
- `feeTiers()` — the AMM's supported fee tiers as raw basis points.
- `createPoolQuote(request)` / `createPool(request)` and
`addLiquidityQuote(request)` / `addLiquidity(request)` — the add-liquidity
preview (read-only) and submit paths. The submit forwards the app-supplied
fresh LP holding id; the app backend, which owns the wallet keyset, creates
that account.
## How it fits together
-4
View File
@@ -16,12 +16,8 @@ extern "C" {
char *amm_config_id(const char *request_json);
char *amm_token_ids(const char *request_json);
char *amm_pair_ids(const char *request_json);
char *amm_context(const char *request_json);
char *amm_resolve_tokens(const char *request_json);
char *amm_swap_pair(const char *request_json);
-4
View File
@@ -60,10 +60,6 @@ pub(crate) fn program_id_hex(program_id: ProgramId) -> String {
hex::encode(bytes)
}
pub(crate) fn program_id_base58(program_id: ProgramId) -> String {
AccountId::new(program_id_bytes(program_id)).to_string()
}
pub(crate) fn program_id_bytes(program_id: ProgramId) -> [u8; 32] {
let mut bytes = [0_u8; 32];
for (chunk, word) in bytes.chunks_exact_mut(4).zip(program_id) {
+20 -242
View File
@@ -1,143 +1,20 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use amm_core::{
FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5, MINIMUM_LIQUIDITY,
};
use nssa_core::{account::AccountId, program::ProgramId};
use serde_json::{json, Value};
use token_core::TokenDefinition;
use super::{
config::load_config,
holding::{select_holding, wallet_holdings, SelectedHolding},
quote_error::issue,
ContextRequest, ResolveTokensRequest, TokenIdsRequest,
};
use crate::account::{
account_id_from_hex, account_id_hex, decode_account, parse_base58_id, parse_program_id,
program_id_base58, AccountRead,
holding::{select_holding, wallet_holdings},
ResolveTokensRequest,
};
use crate::account::{account_id_from_hex, decode_account, parse_program_id, AccountRead};
pub(super) fn token_ids(request: TokenIdsRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let Ok(config) = load_config(amm_program, &request.config) else {
return Ok(manifest_error("config_unavailable"));
};
let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
let mut token_ids = BTreeSet::new();
for id in &request.configured_token_ids {
if let Ok(id) = account_id_from_hex(id, "configured token id") {
token_ids.insert(id);
}
}
for id in request
.recent_token_ids
.iter()
.chain(&request.resolved_token_ids)
{
if let Ok(id) = parse_base58_id(id, "token id") {
token_ids.insert(id);
}
}
token_ids.extend(holdings.into_iter().map(|holding| holding.definition_id));
Ok(json!({
"status": "ok",
"tokenIds": token_ids.into_iter().map(account_id_hex).collect::<Vec<_>>(),
}))
}
fn manifest_error(code: &str) -> Value {
json!({ "status": "error", "code": code, "tokenIds": [] })
}
pub(super) fn context(request: ContextRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let Ok(config) = load_config(amm_program, &request.config) else {
return Ok(context_error(&request, "config_unavailable"));
};
let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
let source_map = token_sources(&request, &holdings);
let mut rows = Vec::new();
let mut warnings = Vec::new();
for (token_id, sources) in source_map {
let read = request
.token_definitions
.iter()
.find(|read| account_id_from_hex(&read.id, "token definition id") == Ok(token_id));
let (name, total_supply, metadata_id) =
match fungible_definition(read, token_id, config.token_program_id) {
Ok(definition) => definition,
Err(error) => {
rows.push(unavailable_token_row(token_id, sources, error.code));
if error.warn {
warnings.push(issue(
error.code,
"Token definition could not be read.",
&[],
json!({ "tokenId": token_id.to_string() }),
));
}
continue;
}
};
let selected = select_holding(&holdings, token_id);
let mut row = json!({
"definitionId": token_id.to_string(),
"name": name,
"metadataId": metadata_id.map(|id| id.to_string()),
"totalSupplyRaw": total_supply.to_string(),
"ownerProgramId": program_id_base58(config.token_program_id),
"public": true,
"fungible": true,
"selectable": true,
"status": "available",
"code": "available",
"sources": sources,
});
if let Some(selected) = selected {
row["holdingId"] = json!(selected.id.to_string());
row["balanceRaw"] = json!(selected.balance.to_string());
}
rows.push(row);
}
rows.sort_by(|left, right| {
let left_holding = left.get("holdingId").is_some();
let right_holding = right.get("holdingId").is_some();
right_holding.cmp(&left_holding).then_with(|| {
left["definitionId"]
.as_str()
.cmp(&right["definitionId"].as_str())
})
});
Ok(json!({
"status": if request.wallet_available { "ready" } else { "no_wallet" },
"networkId": request.network_id,
"networkFingerprint": request.network_fingerprint,
"walletAvailable": request.wallet_available,
"minimumLiquidityRaw": MINIMUM_LIQUIDITY.to_string(),
"programIds": {
"amm": program_id_base58(amm_program),
"token": program_id_base58(config.token_program_id),
"twapOracle": program_id_base58(config.twap_oracle_program_id),
},
"tokens": rows,
"feeTiers": fee_tiers(),
"warnings": warnings,
}))
}
/// Resolves an explicit, app-provided set of token ids into selector rows — the lean,
/// stateless successor to `context`. The app owns the id set (its configured tokens plus any
/// custom/pasted ids it remembers), so there is no network envelope and no process-cached wallet
/// state here: the module reads the definitions + wallet fresh and passes them in, exactly like
/// `context` did per token.
/// Resolves an explicit, app-provided set of token ids into selector rows. The app owns the
/// id set (its configured tokens plus any custom/pasted ids it remembers), so there is no
/// network envelope, no status, and no process-cached wallet state here: the module reads the
/// definitions + wallet fresh and passes them in.
///
/// `token_ids` are hex (the module normalizes base58→hex at the boundary); `token_definitions`
/// are the corresponding read accounts, keyed by hex id. Every returned row has the same shape —
@@ -154,7 +31,7 @@ pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, Str
let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
// De-duplicate the requested ids, dropping any malformed ones. Order is irrelevant —
// the rows are sorted below (held first, then by id) like `context`.
// the rows are sorted below (held first, then by id).
let mut token_ids = BTreeSet::new();
for id in &request.token_ids {
if let Ok(id) = account_id_from_hex(id, "token id") {
@@ -169,7 +46,7 @@ pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, Str
.iter()
.find(|read| account_id_from_hex(&read.id, "token definition id") == Ok(token_id));
// Only readable, fungible definitions become rows; anything else is omitted.
let Ok((name, total_supply, _metadata_id)) =
let Some((name, total_supply)) =
fungible_definition(read, token_id, config.token_program_id)
else {
continue;
@@ -201,120 +78,21 @@ pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, Str
Ok(json!({ "status": "ok", "tokens": rows }))
}
fn context_error(request: &ContextRequest, code: &str) -> Value {
json!({
"status": "error",
"code": code,
"networkId": request.network_id,
"networkFingerprint": request.network_fingerprint,
"walletAvailable": request.wallet_available,
"tokens": [],
"feeTiers": fee_tiers(),
"warnings": [],
})
}
fn fee_tiers() -> Value {
json!([
{ "feeBps": FEE_TIER_BPS_1, "label": "0.01%", "enabled": true },
{ "feeBps": FEE_TIER_BPS_5, "label": "0.05%", "enabled": true },
{ "feeBps": FEE_TIER_BPS_30, "label": "0.30%", "enabled": true },
{ "feeBps": FEE_TIER_BPS_100, "label": "1.00%", "enabled": true },
])
}
fn token_sources(
request: &ContextRequest,
holdings: &[SelectedHolding],
) -> BTreeMap<AccountId, Vec<String>> {
let mut sources: BTreeMap<AccountId, BTreeSet<String>> = BTreeMap::new();
for id in &request.configured_token_ids {
if let Ok(id) = account_id_from_hex(id, "configured token id") {
sources
.entry(id)
.or_default()
.insert(String::from("config"));
}
}
for (ids, source) in [
(&request.recent_token_ids, "recent"),
(&request.resolved_token_ids, "resolved"),
] {
for id in ids {
if let Ok(id) = parse_base58_id(id, "token id") {
sources.entry(id).or_default().insert(String::from(source));
}
}
}
for holding in holdings {
sources
.entry(holding.definition_id)
.or_default()
.insert(String::from("holding"));
}
sources
.into_iter()
.map(|(id, values)| (id, values.into_iter().collect()))
.collect()
}
fn unavailable_token_row(token_id: AccountId, sources: Vec<String>, code: &str) -> Value {
json!({
"definitionId": token_id.to_string(),
"name": "",
"metadataId": Value::Null,
"totalSupplyRaw": "0",
"selectable": false,
"status": code,
"code": code,
"sources": sources,
})
}
pub(super) struct DefinitionError {
pub(super) code: &'static str,
pub(super) warn: bool,
}
pub(super) fn fungible_definition(
/// Decodes a token definition read as a fungible `(name, total_supply)`, or `None` when it is
/// unreadable, owned by a different program, its id mismatches, or it isn't fungible.
fn fungible_definition(
read: Option<&AccountRead>,
token_id: AccountId,
token_program: ProgramId,
) -> Result<(String, u128, Option<AccountId>), DefinitionError> {
let Some(read) = read else {
return Err(DefinitionError {
code: "token_definition_unreadable",
warn: true,
});
};
let Ok((id, account)) = decode_account(read) else {
return Err(DefinitionError {
code: "token_definition_unreadable",
warn: true,
});
};
if id != token_id {
return Err(DefinitionError {
code: "token_definition_unreadable",
warn: false,
});
}
if account.program_owner != token_program {
return Err(DefinitionError {
code: "token_program_mismatch",
warn: false,
});
) -> Option<(String, u128)> {
let (id, account) = decode_account(read?).ok()?;
if id != token_id || account.program_owner != token_program {
return None;
}
match TokenDefinition::try_from(&account.data) {
Ok(TokenDefinition::Fungible {
name,
total_supply,
metadata_id,
..
}) => Ok((name, total_supply, metadata_id)),
_ => Err(DefinitionError {
code: "token_not_fungible",
warn: false,
}),
name, total_supply, ..
}) => Some((name, total_supply)),
_ => None,
}
}
+4 -16
View File
@@ -7,7 +7,6 @@ mod holding;
mod liquidity;
mod pair;
mod quote;
mod quote_error;
mod request;
mod swap;
mod token_holdings;
@@ -18,12 +17,11 @@ mod tests;
use std::{error::Error, fmt};
pub use request::{
AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, ContextRequest,
CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, CreatePoolPlanRequest,
CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest,
RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest,
TokenIdsRequest,
};
use serde_json::Value;
@@ -68,22 +66,12 @@ pub fn config_id(request: ConfigIdRequest) -> AmmResult {
config::config_id(request).map_err(Into::into)
}
/// Discovers token definition IDs available to the active wallet and app.
pub fn token_ids(request: TokenIdsRequest) -> AmmResult {
context::token_ids(request).map_err(Into::into)
}
/// Derives canonical accounts for one token pair.
pub fn pair_ids(request: PairIdsRequest) -> AmmResult {
pair::pair_ids(request).map_err(Into::into)
}
/// Builds network, token, holding, and fee-tier context.
pub fn context(request: ContextRequest) -> AmmResult {
context::context(request).map_err(Into::into)
}
/// Resolves an app-provided set of token ids into selector rows (lean successor to `context`).
/// Resolves an app-provided set of token ids into liquidity selector rows.
pub fn resolve_tokens(request: ResolveTokensRequest) -> AmmResult {
context::resolve_tokens(request).map_err(Into::into)
}
-11
View File
@@ -1,11 +0,0 @@
use serde_json::{json, Value};
pub(super) fn issue(code: &str, message: &str, fields: &[&str], details: Value) -> Value {
json!({
"code": code,
"message": message,
"details": details,
"recoverable": true,
"blockingFields": fields,
})
}
+3 -39
View File
@@ -8,45 +8,9 @@ pub struct ConfigIdRequest {
pub amm_program_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TokenIdsRequest {
pub amm_program_id: String,
pub config: AccountRead,
#[serde(default)]
pub wallet_accounts: Vec<AccountRead>,
#[serde(default)]
pub configured_token_ids: Vec<String>,
#[serde(default)]
pub recent_token_ids: Vec<String>,
#[serde(default)]
pub resolved_token_ids: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ContextRequest {
pub network_id: String,
pub network_fingerprint: String,
pub amm_program_id: String,
pub wallet_available: bool,
pub config: AccountRead,
#[serde(default)]
pub wallet_accounts: Vec<AccountRead>,
#[serde(default)]
pub token_definitions: Vec<AccountRead>,
#[serde(default)]
pub configured_token_ids: Vec<String>,
#[serde(default)]
pub recent_token_ids: Vec<String>,
#[serde(default)]
pub resolved_token_ids: Vec<String>,
}
/// Resolves an app-provided set of token ids into selector rows (the lean successor to
/// `ContextRequest`). `token_ids` are hex — the module normalizes base58→hex and reads each
/// definition into `token_definitions` (keyed by hex id) plus the wallet accounts; the FFI is
/// stateless and reads nothing itself.
/// Resolves an app-provided set of token ids into selector rows. `token_ids` are hex — the
/// module normalizes base58→hex and reads each definition into `token_definitions` (keyed by
/// hex id) plus the wallet accounts; the FFI is stateless and reads nothing itself.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResolveTokensRequest {
+2 -104
View File
@@ -14,13 +14,12 @@ use token_core::{TokenDefinition, TokenHolding};
use twap_oracle_core::compute_current_tick_account_pda;
use super::{
context::{context, resolve_tokens, token_ids},
context::resolve_tokens,
holding::{select_holding, SelectedHolding},
pair::{is_canonical_pair, pair_ids, PairIds},
quote::{div_ceil_u256, minimum_opening_pair, Q64},
swap::{swap_exact_in_plan, swap_exact_out_plan},
ContextRequest, PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest,
SwapExactOutPlanRequest, TokenIdsRequest,
PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactOutPlanRequest,
};
use crate::{
account::{account_id_hex, account_read, decode_account, program_id_bytes},
@@ -190,107 +189,6 @@ fn pair_manifest_reports_unavailable_config_as_domain_error() {
assert_eq!(result["code"], "config_unavailable");
}
#[test]
fn token_manifest_includes_compatible_wallet_holdings() {
let config_id = compute_config_pda(AMM_PROGRAM);
let configured = AccountId::new([1; 32]);
let held = AccountId::new([2; 32]);
let recent = AccountId::new([3; 32]);
let resolved = AccountId::new([4; 32]);
let value = token_ids(TokenIdsRequest {
amm_program_id: amm_program_id(),
config: account_read(config_id, &config_account()),
wallet_accounts: vec![account_read(
AccountId::new([5; 32]),
&token_holding(held, 9),
)],
configured_token_ids: vec![account_id_hex(configured)],
recent_token_ids: vec![recent.to_string()],
resolved_token_ids: vec![resolved.to_string()],
})
.unwrap();
assert_eq!(
value["tokenIds"],
json!([
account_id_hex(configured),
account_id_hex(held),
account_id_hex(recent),
account_id_hex(resolved),
])
);
}
#[test]
fn wrong_program_holdings_do_not_contribute_token_candidates() {
let config_id = compute_config_pda(AMM_PROGRAM);
let config = config_account();
let definition = AccountId::new([2; 32]);
let wrong_owner_holding = account(
[99; 8],
Data::from(&TokenHolding::Fungible {
definition_id: definition,
balance: 9,
}),
);
let wallet_accounts = vec![account_read(AccountId::new([3; 32]), &wrong_owner_holding)];
let manifest = token_ids(TokenIdsRequest {
amm_program_id: amm_program_id(),
config: account_read(config_id, &config),
wallet_accounts: wallet_accounts.clone(),
configured_token_ids: Vec::new(),
recent_token_ids: Vec::new(),
resolved_token_ids: Vec::new(),
})
.unwrap();
assert_eq!(manifest["tokenIds"], json!([]));
let value = context(ContextRequest {
network_id: String::from("testnet"),
network_fingerprint: String::from("block10:abc"),
amm_program_id: amm_program_id(),
wallet_available: true,
config: account_read(config_id, &config),
wallet_accounts,
token_definitions: vec![account_read(
definition,
&token_definition("Token", 1_000_000),
)],
configured_token_ids: Vec::new(),
recent_token_ids: Vec::new(),
resolved_token_ids: Vec::new(),
})
.unwrap();
assert_eq!(value["tokens"], json!([]));
}
#[test]
fn context_selects_tokens_without_holdings() {
let token_id = AccountId::new([3; 32]);
let config_id = compute_config_pda(AMM_PROGRAM);
let value = context(ContextRequest {
network_id: String::from("testnet"),
network_fingerprint: String::from("block10:abc"),
amm_program_id: amm_program_id(),
wallet_available: true,
config: account_read(config_id, &config_account()),
wallet_accounts: Vec::new(),
token_definitions: vec![account_read(
token_id,
&token_definition("Token", 1_000_000),
)],
configured_token_ids: vec![account_id_hex(token_id)],
recent_token_ids: Vec::new(),
resolved_token_ids: Vec::new(),
})
.unwrap();
assert_eq!(value["tokens"][0]["selectable"], true);
assert_eq!(value["tokens"][0]["sources"], json!(["config"]));
assert!(value["tokens"][0].get("holdingId").is_none());
}
#[test]
fn resolve_tokens_returns_lean_rows_held_first_and_omits_unresolvable() {
let held = AccountId::new([2; 32]);
+3 -13
View File
@@ -7,11 +7,11 @@ use serde::{de::DeserializeOwned, Serialize};
use crate::api::{
self, AddLiquidityPlanRequest, AddLiquidityQuoteRequest, AmmApiError, AmmResult,
ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest,
FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest,
ConfigIdRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest,
PairIdsRequest, PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest,
RemoveLiquidityQuoteRequest, ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest,
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
SyncReservesPlanRequest, TokenHoldingsRequest, TokenIdsRequest,
SyncReservesPlanRequest, TokenHoldingsRequest,
};
#[derive(Serialize)]
@@ -85,21 +85,11 @@ pub extern "C" fn amm_config_id(request_json: *const c_char) -> *mut c_char {
call::<ConfigIdRequest>(request_json, api::config_id)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_token_ids(request_json: *const c_char) -> *mut c_char {
call::<TokenIdsRequest>(request_json, api::token_ids)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_pair_ids(request_json: *const c_char) -> *mut c_char {
call::<PairIdsRequest>(request_json, api::pair_ids)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_context(request_json: *const c_char) -> *mut c_char {
call::<ContextRequest>(request_json, api::context)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_resolve_tokens(request_json: *const c_char) -> *mut c_char {
call::<ResolveTokensRequest>(request_json, api::resolve_tokens)
+7 -8
View File
@@ -6,12 +6,11 @@ mod ffi;
pub mod api;
pub use api::{
config_id, context, create_pool_plan, create_pool_quote, fee_tiers, pair_ids, pool_id,
program_id, resolve_pool, resolve_tokens, swap_exact_in_plan, swap_exact_in_quote,
swap_exact_out_plan, swap_exact_out_quote, swap_pair, token_ids, AccountRead, AmmApiError,
AmmResponse, AmmResult, ConfigIdRequest, ContextRequest, CreatePoolPlanRequest,
CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest,
ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest,
SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, TokenIdsRequest,
WalletAccount,
config_id, create_pool_plan, create_pool_quote, fee_tiers, pair_ids, pool_id, program_id,
resolve_pool, resolve_tokens, swap_exact_in_plan, swap_exact_in_quote, swap_exact_out_plan,
swap_exact_out_quote, swap_pair, AccountRead, AmmApiError, AmmResponse, AmmResult,
ConfigIdRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest,
PairIdsRequest, PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, ResolveTokensRequest,
SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
SwapExactOutQuoteRequest, SwapPairRequest, WalletAccount,
};
+26 -147
View File
@@ -237,32 +237,6 @@ json publicError(const std::string& code,
};
}
json contextState(const std::string& status,
const std::string& network_id,
const std::string& network_fingerprint,
const std::string& code = {}) {
json state = {
{"status", status},
{"networkId", network_id},
{"networkFingerprint", network_fingerprint},
{"tokens", json::array()},
{"feeTiers", json::array()},
{"warnings", json::array()},
};
if (!code.empty()) state["code"] = code;
return state;
}
// A json array of the strings at `obj[key]` (empty array when absent/wrong type).
json stringArray(const json& obj, const char* key) {
const auto it = obj.find(key);
if (it == obj.end() || !it->is_array()) return json::array();
json out = json::array();
for (const auto& v : *it)
if (v.is_string()) out.push_back(v);
return out;
}
} // namespace
std::vector<uint8_t> AmmModuleImpl::loadAmmElf() {
@@ -288,37 +262,6 @@ std::string AmmModuleImpl::ammProgramId() {
return jStr(r.value, "programId");
}
AmmModuleImpl::Network AmmModuleImpl::network() {
// AMM_PROGRAM_BIN / TOKENS_CONFIG are fixed for the process lifetime and this
// runs on the hot reply path, so resolve the program id + token ids once.
if (!networkResolved) {
const std::string id = ammProgramId();
if (id.empty()) {
// Not resolvable yet (AMM_PROGRAM_BIN unset/unreadable). Don't cache a
// transient miss — a later call retries.
Network net;
net.status = "config_missing";
return net;
}
programId = id;
tokenIds.clear();
for (const auto& token : tokenList()) {
const std::string token_id = jStr(token, "definitionId");
if (!token_id.empty()) tokenIds.push_back(token_id);
}
networkResolved = true;
}
Network net;
net.amm_program_id = programId;
// The program id changes per deployment, so it doubles as the network
// fingerprint (a quote can't be replayed against a different program).
net.fingerprint = programId;
net.token_ids = tokenIds;
net.status = "ready";
return net;
}
std::string AmmModuleImpl::normalizeAccountId(const std::string& id) {
size_t start = 0;
size_t end = id.size();
@@ -369,21 +312,15 @@ nlohmann::json AmmModuleImpl::readPublicAccount(const std::string& account_id) {
return result;
}
nlohmann::json AmmModuleImpl::walletAccountReads(bool wallet_open, bool refresh) {
if (!wallet_open) {
walletAccounts = json(); // invalidate — nothing to read while closed
return json::array();
}
// Each readPublicAccount is a live sequencer round-trip, so serve the cached
// set unless the caller forces a reload (submit / explicit UI refresh).
if (!refresh && !walletAccounts.is_null())
return walletAccounts;
nlohmann::json AmmModuleImpl::walletAccountReads(bool wallet_open) {
if (!wallet_open)
return json::array(); // nothing to read while closed
// 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())
return json::array(); // transient — don't cache
return json::array();
json out = json::array();
for (const auto& entry : accounts) {
@@ -394,13 +331,12 @@ nlohmann::json AmmModuleImpl::walletAccountReads(bool wallet_open, bool refresh)
if (id.empty()) continue;
out.push_back(readPublicAccount(id));
}
walletAccounts = out;
return out;
}
nlohmann::json AmmModuleImpl::readConfig(const Network& net) {
nlohmann::json AmmModuleImpl::readConfig(const std::string& amm_program_id) {
const FfiResult configResult =
call(amm_config_id, json{{"ammProgramId", net.amm_program_id}});
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
if (!configResult.ok) return json(); // null: config_id op failed
return readPublicAccount(jStr(configResult.value, "configId"));
}
@@ -416,12 +352,12 @@ LogosMap AmmModuleImpl::resolvePool(const std::string& def_a_hex,
return LogosMap{{"exists", false}, {"error", error}};
};
const Network net = network();
if (net.status != "ready")
// config_missing == no program id from AMM_PROGRAM_BIN (unset/unreadable/bad).
const std::string amm_program_id = ammProgramId();
if (amm_program_id.empty())
// no program id from AMM_PROGRAM_BIN (unset/unreadable/bad).
return failed("no_program_bin");
const json config = readConfig(net);
const json config = readConfig(amm_program_id);
if (config.is_null())
return failed("bad_config"); // amm_config_id op failed (malformed program id)
@@ -431,7 +367,7 @@ LogosMap AmmModuleImpl::resolvePool(const std::string& def_a_hex,
const std::string token_b = normalizeAccountId(def_b_hex);
const FfiResult pairResult = call(amm_swap_pair, json{
{"ammProgramId", net.amm_program_id},
{"ammProgramId", amm_program_id},
{"tokenInId", token_a},
{"tokenOutId", token_b},
{"config", config},
@@ -584,13 +520,13 @@ std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
return {};
}
const Network net = network();
if (net.status != "ready") {
AMM_TRACE("swapExactInput: FAIL network not ready (" << net.status << ")");
const std::string amm_program_id = ammProgramId();
if (amm_program_id.empty()) {
AMM_TRACE("swapExactInput: FAIL no program id (AMM_PROGRAM_BIN unset/unreadable)");
return {};
}
const json config = readConfig(net);
const json config = readConfig(amm_program_id);
if (config.is_null()) {
AMM_TRACE("swapExactInput: FAIL config_id op failed");
return {};
@@ -599,7 +535,7 @@ std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
// 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{
{"ammProgramId", net.amm_program_id},
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
});
@@ -613,7 +549,7 @@ std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
// amm_swap_exact_in_plan resolves the pool accounts, encodes SwapExactInput,
// and returns a ready-to-submit plan.
const FfiResult planResult = call(amm_swap_exact_in_plan, json{
{"ammProgramId", net.amm_program_id},
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
{"config", config},
@@ -667,13 +603,13 @@ std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
return {};
}
const Network net = network();
if (net.status != "ready") {
AMM_TRACE("swapExactOutput: FAIL network not ready (" << net.status << ")");
const std::string amm_program_id = ammProgramId();
if (amm_program_id.empty()) {
AMM_TRACE("swapExactOutput: FAIL no program id (AMM_PROGRAM_BIN unset/unreadable)");
return {};
}
const json config = readConfig(net);
const json config = readConfig(amm_program_id);
if (config.is_null()) {
AMM_TRACE("swapExactOutput: FAIL config_id op failed");
return {};
@@ -682,7 +618,7 @@ std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
// 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{
{"ammProgramId", net.amm_program_id},
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
});
@@ -696,7 +632,7 @@ std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
// 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{
{"ammProgramId", net.amm_program_id},
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
{"config", config},
@@ -739,7 +675,7 @@ LogosMap AmmModuleImpl::createPoolQuote(const LogosMap& request) {
};
// Pure preview — no program id / chain reads / fee. Normalize the pair to hex (the
// liquidity UI still sources base58 ids from newPositionContext; transitional).
// liquidity UI sources base58 ids from resolveTokens).
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())
@@ -1297,7 +1233,7 @@ LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) {
const json config = readPublicAccount(jStr(configResult.value, "configId"));
// Fresh wallet read each call — the selector wants current holdings/balances.
const json wallet_accounts = walletAccountReads(wallet_open, /*refresh=*/true);
const json wallet_accounts = walletAccountReads(wallet_open);
const FfiResult result = call(amm_token_holdings, json{
{"ammProgramId", amm_program_id},
@@ -1365,7 +1301,7 @@ LogosList AmmModuleImpl::resolveTokens(const LogosMap& request, bool wallet_open
}
// Fresh wallet read — the selector wants current holdings/balances.
const json wallet_accounts = walletAccountReads(wallet_open, /*refresh=*/true);
const json wallet_accounts = walletAccountReads(wallet_open);
const FfiResult result = call(amm_resolve_tokens, json{
{"ammProgramId", amm_program_id},
@@ -1384,60 +1320,3 @@ LogosList AmmModuleImpl::resolveTokens(const LogosMap& request, bool wallet_open
return out;
}
LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request,
bool wallet_open,
bool refresh_wallet_accounts) {
const Network net = network();
if (net.status != "ready")
return contextState(net.status, net.id, net.fingerprint);
const json walletAccounts = walletAccountReads(wallet_open, refresh_wallet_accounts);
const FfiResult configResult =
call(amm_config_id, json{{"ammProgramId", net.amm_program_id}});
if (!configResult.ok)
return contextState("error", net.id, net.fingerprint, "backend_error");
const json config = readPublicAccount(jStr(configResult.value, "configId"));
json configured = json::array();
for (const auto& id : net.token_ids) configured.push_back(id);
const json recent = stringArray(request, "recentTokenIds");
const json resolved = stringArray(request, "resolvedTokenIds");
const FfiResult tokenResult = call(amm_token_ids, json{
{"ammProgramId", net.amm_program_id},
{"config", config},
{"walletAccounts", walletAccounts},
{"configuredTokenIds", configured},
{"recentTokenIds", recent},
{"resolvedTokenIds", resolved},
});
const json tokenManifest = tokenResult.value;
if (!tokenResult.ok || jStr(tokenManifest, "status") != "ok") {
const std::string code =
tokenResult.ok ? jStr(tokenManifest, "code") : std::string("backend_error");
return contextState("error", net.id, net.fingerprint,
code.empty() ? "backend_error" : code);
}
json definitions = json::array();
for (const auto& id : tokenManifest.value("tokenIds", json::array()))
if (id.is_string()) definitions.push_back(readPublicAccount(id.get<std::string>()));
const FfiResult contextResult = call(amm_context, json{
{"networkId", net.id},
{"networkFingerprint", net.fingerprint},
{"ammProgramId", net.amm_program_id},
{"walletAvailable", wallet_open},
{"config", config},
{"walletAccounts", walletAccounts},
{"tokenDefinitions", definitions},
{"configuredTokenIds", configured},
{"recentTokenIds", recent},
{"resolvedTokenIds", resolved},
});
return contextResult.ok
? contextResult.value
: contextState("error", net.id, net.fingerprint, "backend_error");
}
+4 -44
View File
@@ -231,33 +231,7 @@ public:
/// universal-module glue only supports map/scalar inputs.)
LogosList resolveTokens(const LogosMap& request, bool wallet_open);
/// New-position (add-liquidity) view state: reads the AMM config + the
/// user's wallet accounts and returns the new-position context map the
/// UI renders (available tokens, fee tiers, warnings). `wallet_open` gates
/// whether wallet accounts are included; `refresh_wallet_accounts` forces a
/// fresh read rather than a cached one.
LogosMap newPositionContext(const LogosMap& request,
bool wallet_open,
bool refresh_wallet_accounts);
private:
// Off-chain "network" context, derived from the process env (the same
// sources the app backend used): AMM deployment id from AMM_PROGRAM_BIN,
// configured token set from TOKENS_CONFIG. `status` is "ready" once the
// program id resolves, else "config_missing".
struct Network {
std::string id = "lez";
std::string status;
std::string fingerprint; // == amm_program_id (binds a quote to the deploy)
std::string amm_program_id; // 64-char lowercase hex
std::vector<std::string> token_ids;
};
// AMM_PROGRAM_BIN / TOKENS_CONFIG are fixed for the process lifetime, and
// this runs on the hot reply path (every op), so it resolves the program id
// + token ids ONCE and caches them (networkResolved). Cached only on
// success, so a startup miss (bin not readable yet) retries.
Network network();
// 64-char lowercase-hex AMM program id via the amm_ffi `program_id` op
// over the AMM_PROGRAM_BIN bytes (empty if unset/unreadable/bad).
std::string ammProgramId();
@@ -272,7 +246,7 @@ private:
// Derives the config account id (amm_config_id) and reads it, returning the
// account-read shape the amm_ffi ops embed. Null json when the config_id
// op itself fails (readPublicAccount always yields at least {id,status}).
nlohmann::json readConfig(const Network& net);
nlohmann::json readConfig(const std::string& amm_program_id);
// Reads a public account through the wallet module and returns the
// { id, status, account:{ program_owner, balance, nonce, data } } shape the
@@ -280,21 +254,7 @@ private:
// omitted when the read has no data (uninitialized/nonexistent).
nlohmann::json readPublicAccount(const std::string& account_id);
// The user's own public account reads (empty when the wallet is closed).
// Cached across calls (walletAccounts); `refresh` reloads instead of serving
// the cache — quote reuses it, submit forces fresh — since each read is a
// live sequencer round-trip.
nlohmann::json walletAccountReads(bool wallet_open, bool refresh);
// Process-lifetime network config, resolved once (see network()). Serialized
// module dispatch means no locking is needed; there is no invalidation, as
// runtime env reload is not supported.
bool networkResolved = false;
std::string programId;
std::vector<std::string> tokenIds;
// Cache of the user's public account reads for the context/quote path (each
// read is a live sequencer round-trip). Null until first read; `refresh`
// reloads it, and it's dropped when the wallet closes. See walletAccountReads.
nlohmann::json walletAccounts;
// The user's own public account reads, fresh each call (empty when the wallet
// is closed). Each read is a live sequencer round-trip.
nlohmann::json walletAccountReads(bool wallet_open);
};