mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
The add-liquidity flow stamped a `new-position.v1` schema tag on every request and response and validated it across all three layers — the QML plugin, the amm_module core module, and the amm_client Rust crate. It was a cross-version compatibility guard, but these artifacts always ship together, so the contract is honored implicitly, and the swap view already works fine without one. Dropping it makes the liquidity view consistent with swap and removes a layer of ceremony. - amm_client: remove PositionRequest.schema and the unsupported_schema check in compute_quote; drop QuoteCommitment.schema (changes quoteHash, which is internal-only) and every "schema" response stamp; delete the SCHEMA / NEW_POSITION_SCHEMA constants and the public export. - amm_module: remove the SCHEMA constant and its four response stamps. - AmmUiBackend: remove its local NEW_POSITION_SCHEMA and the stamps in loadingContext() / newPositionError(). - QML: drop the "schema" fields from the request/envelope builders and relax the validity gates to check status / canSubmit instead (the sole check in NewPositionFlow now guards on a missing `status`); strip the now-dead schema fields from the liquidity QML test fixtures. - Also removes the last stale comment references to the deleted *Runtime classes.
253 lines
8.1 KiB
Rust
253 lines
8.1 KiB
Rust
use std::collections::{BTreeMap, 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, TokenIdsRequest,
|
|
};
|
|
use crate::account::{
|
|
account_id_from_hex, account_id_hex, decode_account, parse_base58_id, parse_program_id,
|
|
program_id_base58, 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,
|
|
}))
|
|
}
|
|
|
|
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(
|
|
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,
|
|
});
|
|
}
|
|
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,
|
|
}),
|
|
}
|
|
}
|