feat(amm): source liquidity tokens app-side + add custom tokens by id

Move the liquidity token selector off the module's stateful newPositionContext
onto a lean, app-owned surface, and let users add unlisted tokens by id.

FFI: new stateless `resolve_tokens` op — the app passes an explicit id set and
gets uniform selector rows `{ definitionId (base58), name, totalSupply, holdingId,
balance }`, held tokens first, unresolvable/non-fungible ids omitted. Reuses the
per-token definition/holding logic from `context`, without the network/status
envelope. Unit-tested.

Module: `resolveTokens(request, wallet_open)` reads the definitions + wallet and
calls the op (ids wrapped in a map — the universal-module glue only marshals
map/scalar inputs, not bare lists).

Backend: the app owns the id set — configured tokens (TOKENS_CONFIG) plus the
user's persisted custom ids. Held-but-unlisted tokens are NOT auto-listed (the
list mirrors the swap side); a token you hold still shows its balance once listed.
`addCustomToken` validates a pasted id by resolving its on-chain definition, then
persists it to CUSTOM_TOKEN_CONFIG (defaulting to the per-user app-data store, with
a HOME fallback so persistence never silently no-ops on an empty path).

QML: NewPositionForm/LiquidityPage take tokens/walletReady/loadingTokens as inputs
and drive selection + custom-token resolution through the backend; dropped all
newPositionContext reads and the selectable/status/code row fields.

Tests: custom-token.mjs creates token D on-chain (left out of the token config)
and verifies pasting its id resolves, selects, and persists it across a reload.
The setup script mints token D and initializes/prints the isolated
CUSTOM_TOKEN_CONFIG store
This commit is contained in:
r4bbit
2026-08-13 15:33:31 +02:00
parent cb1b457ad3
commit 7a7ebfdbaf
18 changed files with 737 additions and 150 deletions
+56
View File
@@ -1328,6 +1328,62 @@ LogosList AmmModuleImpl::feeTiers() {
return out;
}
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.
const json wallet_accounts = walletAccountReads(wallet_open, /*refresh=*/true);
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;
}
LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request,
bool wallet_open,
bool refresh_wallet_accounts) {