refactor(amm): remove the dead newPosition quote path

Both liquidity branches now quote through the lean ops (liquidityQuote /
addLiquidityQuote), so quoteNewPosition and the heavy amm_quote machinery it
drove are unreachable. Remove them end to end.

FFI (modules/amm/ffi):
- Drop the amm_quote entry point and the whole quote-evaluation graph:
  api/{accounts,commitment,funding,position}.rs, the QuoteRequest /
  PositionRequest / PairSnapshot request types, quote_error::fatal_quote, and
  api/clock.rs (its decode_clock was quote-only). quote.rs keeps only the shared
  opening-deposit math (minimum_opening_pair + helpers) that liquidity_quote
  reuses.
- Trim the fields the quote path was the sole reader of: SelectedHolding.account
  and PairIds.{token_program,twap_program}.
- Drop the quote-path unit tests; keep the math / pair / context / holding /
  swap ones (37 pass, clippy clean).

Module (modules/amm/src):
- Remove AmmModuleImpl::quoteNewPosition and its buildQuoteInput snapshot helper.

App (apps/amm):
- Remove the AmmUiBackend quoteNewPosition slot (.rep/.h/.cpp) and the dead QML
  backend mock + obsolete fresh-quote test.
- finishSubmitFailure no longer keeps a submit-returned re-quote (the lean submit
  ops never return one); it always re-quotes on failure.
- submissionSnapshot drops the always-empty quoteHash and derives the confirm
  dialog's action from the resolved pool state instead of the dead
  quotePayload.instruction (restores the "Create pool" / "Add liquidity" label).
This commit is contained in:
r4bbit
2026-08-11 17:51:51 +02:00
parent eb98aac31f
commit 64e7614e74
24 changed files with 34 additions and 1737 deletions
@@ -8,12 +8,8 @@ ColumnLayout {
spacing: 8
function actionText(instruction) {
if (instruction === "NewDefinition")
return qsTr("Create pool")
if (instruction === "AddLiquidity")
return qsTr("Add liquidity")
return instruction || "-"
function actionText() {
return root.snapshot.poolExists ? qsTr("Add liquidity") : qsTr("Create pool")
}
SummaryRow {
@@ -25,7 +21,7 @@ ColumnLayout {
SummaryRow {
Layout.fillWidth: true
label: qsTr("Action")
value: root.actionText(root.snapshot.instruction)
value: root.actionText()
}
SummaryRow {
@@ -1440,7 +1440,6 @@ AmmActionCard {
var built = root.buildQuoteRequest()
return {
"request": built.request,
"quoteHash": String(root.quotePayload.quoteHash || ""),
// Canonical-order holdings for the createPool / addLiquidity calls: the
// request's tokenAId/amountARaw are canonical, so holdingAId must be the
// canonical token A's holding too (the module re-canonicalizes as a no-op).
@@ -1456,7 +1455,8 @@ AmmActionCard {
"depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"),
"depositBText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B"),
"expectedLpText": root.rawLpText(root.quotePayload.expectedLpRaw),
"instruction": String(root.quotePayload.instruction || "")
// Confirm-dialog action derives from the resolved pool state (add vs create).
"poolExists": root.activePool
}
}
+2 -9
View File
@@ -431,18 +431,11 @@ QtObject {
function finishSubmitFailure(result) {
root.submitting = false
const hasFreshQuote = result && result.quote
&& result.quote.status
if (hasFreshQuote) {
root.newPositionQuote = result.quote
root.quoteLoading = false
root.quoteStale = false
}
const code = result && result.code ? result.code : "wallet_submission_failed"
root.flowErrorCode = code
root.submitFailed()
if (hasFreshQuote)
return
// The lean submit ops report only a status/error (never a re-quote), so a failure
// always re-quotes to refresh state against the current pool.
root.scheduleQuote(true, root.pendingQuoteRequest)
}
-5
View File
@@ -132,11 +132,6 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request)
request, isWalletOpen(), refreshWalletAccounts));
}
QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request)
{
return m_logos->amm_module.quoteNewPosition(request, isWalletOpen());
}
void AmmUiBackend::syncWalletState()
{
const WalletUiState& state = m_walletController->state();
-1
View File
@@ -47,7 +47,6 @@ public slots:
void refreshBalances() override;
QString getBalance(QString accountIdHex, bool isPublic) override;
void refreshNewPositionContext(QVariantMap request) override;
QVariantMap quoteNewPosition(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;
-1
View File
@@ -31,7 +31,6 @@ class AmmUiBackend
// transactions or duplicates quote state.
PROP(QVariantMap newPositionContext READONLY)
SLOT(void refreshNewPositionContext(QVariantMap request))
SLOT(QVariantMap quoteNewPosition(QVariantMap request))
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
// fresh wallet at the canonical walletHome with no path picking. createNew()
-34
View File
@@ -15,19 +15,11 @@ TestCase {
QtObject {
property bool walletStateReady: false
property var quoteResult: ({
"status": "ok",
"poolStatus": "missing_pool"
})
property var newPositionContext: ({
"status": "ready",
"tokens": [],
"feeTiers": []
})
function quoteNewPosition(request) {
return quoteResult
}
}
}
@@ -120,30 +112,4 @@ TestCase {
compare(page.flow.contextErrorCode, "")
}
function test_submitFailureKeepsReturnedFreshQuoteWithoutRequery() {
var backend = createTemporaryObject(backendComponent, testCase, {
"walletStateReady": true
})
var page = createTemporaryObject(pageComponent, testCase, {
"backend": backend
})
verify(backend)
verify(page)
page.flow.quoteSerial = 7
page.flow.finishSubmitFailure({
"status": "error",
"code": "quote_not_submittable",
"quote": {
"status": "ok",
"canSubmit": false,
"quoteHash": "sha256:fresh"
}
})
compare(page.flow.quoteSerial, 7)
compare(page.flow.newPositionQuote.quoteHash, "sha256:fresh")
compare(page.flow.quoteStale, false)
}
}
-2
View File
@@ -22,8 +22,6 @@ char *amm_pair_ids(const char *request_json);
char *amm_context(const char *request_json);
char *amm_quote(const char *request_json);
char *amm_swap_pair(const char *request_json);
char *amm_resolve_pool(const char *request_json);
-226
View File
@@ -1,226 +0,0 @@
use amm_core::PoolDefinition;
use nssa_core::program::ProgramId;
use super::{
funding::{append_holding_source, sources_from_reads},
pair::PairIds,
position::{AccountPlan, AccountPlanHoldings, AccountPlanRow},
QuoteRequest,
};
pub(super) fn missing_account_plan(
input: &QuoteRequest,
pair: PairIds,
amm_program: ProgramId,
holdings: AccountPlanHoldings<'_>,
) -> Result<AccountPlan, String> {
let mut sources = sources_from_reads(&[
("config", &input.snapshot.config),
("token_a", &input.snapshot.token_a),
("token_b", &input.snapshot.token_b),
("pool", &input.snapshot.pool),
("vault_a", &input.snapshot.vault_a),
("vault_b", &input.snapshot.vault_b),
("lp_definition", &input.snapshot.lp_definition),
("lp_lock_holding", &input.snapshot.lp_lock_holding),
("current_tick", &input.snapshot.current_tick),
])?;
append_holding_source(&mut sources, "holding_a", holdings.token_a);
append_holding_source(&mut sources, "holding_b", holdings.token_b);
Ok(AccountPlan {
rows: vec![
AccountPlanRow::new(
"config",
Some(pair.config),
Some(amm_program),
"read",
false,
false,
),
AccountPlanRow::new(
"pool",
Some(pair.pool),
Some(amm_program),
"create",
false,
true,
),
AccountPlanRow::new(
"vault_a",
Some(pair.vault_a),
Some(pair.token_program),
"create",
false,
true,
),
AccountPlanRow::new(
"vault_b",
Some(pair.vault_b),
Some(pair.token_program),
"create",
false,
true,
),
AccountPlanRow::new(
"lp_definition",
Some(pair.lp_definition),
Some(pair.token_program),
"create",
false,
true,
),
AccountPlanRow::new(
"lp_lock_holding",
Some(pair.lp_lock_holding),
Some(pair.token_program),
"create",
false,
true,
),
AccountPlanRow::new(
"user_holding_a",
holdings.token_a.map(|value| value.id),
Some(pair.token_program),
"update",
true,
false,
),
AccountPlanRow::new(
"user_holding_b",
holdings.token_b.map(|value| value.id),
Some(pair.token_program),
"update",
true,
false,
),
AccountPlanRow::new(
"user_holding_lp",
None,
Some(pair.token_program),
"create",
true,
true,
),
AccountPlanRow::new(
"current_tick",
Some(pair.current_tick),
Some(pair.twap_program),
"create",
false,
true,
),
AccountPlanRow::new("clock", Some(pair.clock), None, "read", false, false),
],
sources,
})
}
pub(super) fn active_account_plan(
input: &QuoteRequest,
pair: PairIds,
amm_program: ProgramId,
pool: &PoolDefinition,
stored_reversed: bool,
holdings: AccountPlanHoldings<'_>,
) -> Result<AccountPlan, String> {
let (stored_holding_a, stored_holding_b) = if stored_reversed {
(holdings.token_b, holdings.token_a)
} else {
(holdings.token_a, holdings.token_b)
};
let mut sources = sources_from_reads(&[
("config", &input.snapshot.config),
("token_a", &input.snapshot.token_a),
("token_b", &input.snapshot.token_b),
("pool", &input.snapshot.pool),
("vault_a", &input.snapshot.vault_a),
("vault_b", &input.snapshot.vault_b),
("lp_definition", &input.snapshot.lp_definition),
("current_tick", &input.snapshot.current_tick),
])?;
append_holding_source(&mut sources, "holding_a", holdings.token_a);
append_holding_source(&mut sources, "holding_b", holdings.token_b);
append_holding_source(&mut sources, "holding_lp", holdings.lp);
Ok(AccountPlan {
rows: vec![
AccountPlanRow::new(
"config",
Some(pair.config),
Some(amm_program),
"read",
false,
false,
),
AccountPlanRow::new(
"pool",
Some(pair.pool),
Some(amm_program),
"update",
false,
false,
),
AccountPlanRow::new(
"vault_a",
Some(pool.vault_a_id),
Some(pair.token_program),
"update",
false,
false,
),
AccountPlanRow::new(
"vault_b",
Some(pool.vault_b_id),
Some(pair.token_program),
"update",
false,
false,
),
AccountPlanRow::new(
"lp_definition",
Some(pair.lp_definition),
Some(pair.token_program),
"update",
false,
false,
),
AccountPlanRow::new(
"user_holding_a",
stored_holding_a.map(|value| value.id),
Some(pair.token_program),
"update",
true,
false,
),
AccountPlanRow::new(
"user_holding_b",
stored_holding_b.map(|value| value.id),
Some(pair.token_program),
"update",
true,
false,
),
AccountPlanRow::new(
"user_holding_lp",
holdings.lp.map(|value| value.id),
Some(pair.token_program),
if holdings.lp.is_some() {
"update"
} else {
"create"
},
holdings.lp.is_none(),
holdings.lp.is_none(),
),
AccountPlanRow::new(
"current_tick",
Some(pair.current_tick),
Some(pair.twap_program),
"update",
false,
false,
),
AccountPlanRow::new("clock", Some(pair.clock), None, "read", false, false),
],
sources,
})
}
-12
View File
@@ -1,12 +0,0 @@
use borsh::from_slice;
use clock_core::ClockAccountData;
use nssa_core::account::AccountId;
use crate::account::{decode_account, AccountRead};
pub(super) fn decode_clock(read: &AccountRead) -> Result<(AccountId, ClockAccountData), String> {
let (id, account) = decode_account(read)?;
let clock = from_slice(account.data.as_ref())
.map_err(|error| format!("invalid clock account: {error}"))?;
Ok((id, clock))
}
-50
View File
@@ -1,50 +0,0 @@
use borsh::BorshSerialize;
#[derive(BorshSerialize)]
pub(super) enum RequestCommitment {
Missing {
amount_a: u128,
amount_b: u128,
},
Active {
max_a: u128,
max_b: u128,
slippage_bps: u32,
},
}
#[derive(BorshSerialize)]
pub(super) struct SourceCommitment {
pub(super) role: String,
pub(super) commitment: [u8; 32],
}
#[derive(BorshSerialize)]
pub(super) struct FundingCommitment {
pub(super) token_id: [u8; 32],
pub(super) holding_id: Option<[u8; 32]>,
pub(super) available: u128,
pub(super) requested: u128,
}
#[derive(BorshSerialize)]
pub(super) struct QuoteCommitment {
pub(super) network_id: String,
pub(super) network_fingerprint: String,
pub(super) amm_program_id: [u8; 32],
pub(super) token_a_id: [u8; 32],
pub(super) token_b_id: [u8; 32],
pub(super) fee_bps: u32,
pub(super) pool_status: u8,
pub(super) request: RequestCommitment,
pub(super) max_a: u128,
pub(super) max_b: u128,
pub(super) actual_a: u128,
pub(super) actual_b: u128,
pub(super) expected_lp: u128,
pub(super) lp_guard: u128,
pub(super) requires_fresh_lp: bool,
pub(super) sources: Vec<SourceCommitment>,
pub(super) funding: Vec<FundingCommitment>,
pub(super) warnings: Vec<String>,
}
-106
View File
@@ -1,106 +0,0 @@
use nssa_core::Commitment;
use serde_json::{json, Value};
use sha2::{Digest as _, Sha256};
use super::{
commitment::{FundingCommitment, QuoteCommitment, SourceCommitment},
holding::SelectedHolding,
pair::PairIds,
quote_error::issue,
};
use crate::account::{decode_account, AccountRead};
pub(super) fn funding_issues(
wallet_available: bool,
pair: PairIds,
holding_a: &Option<SelectedHolding>,
requested_a: u128,
holding_b: &Option<SelectedHolding>,
requested_b: u128,
fields: [&str; 2],
) -> Vec<Value> {
if !wallet_available {
return vec![issue(
"no_wallet",
"Connect a wallet to submit.",
&[],
json!({}),
)];
}
let mut errors = Vec::new();
for (token_id, holding, requested, field) in [
(pair.token_a, holding_a, requested_a, fields[0]),
(pair.token_b, holding_b, requested_b, fields[1]),
] {
let available = holding.as_ref().map_or(0, |value| value.balance);
if available < requested {
errors.push(issue(
"amount_exceeds_balance",
"Amount exceeds the selected wallet holding balance.",
&[field],
json!({
"requestedRaw": requested.to_string(),
"availableRaw": available.to_string(),
"holdingFound": holding.is_some(),
"tokenId": token_id.to_string(),
}),
));
}
}
errors
}
pub(super) fn funding_commitments(
pair: PairIds,
holding_a: &Option<SelectedHolding>,
requested_a: u128,
holding_b: &Option<SelectedHolding>,
requested_b: u128,
) -> Vec<FundingCommitment> {
[
(pair.token_a, holding_a, requested_a),
(pair.token_b, holding_b, requested_b),
]
.into_iter()
.map(|(token_id, holding, requested)| FundingCommitment {
token_id: token_id.into_value(),
holding_id: holding.as_ref().map(|value| value.id.into_value()),
available: holding.as_ref().map_or(0, |value| value.balance),
requested,
})
.collect()
}
pub(super) fn sources_from_reads(
reads: &[(&str, &AccountRead)],
) -> Result<Vec<SourceCommitment>, String> {
reads
.iter()
.map(|(role, read)| {
let (id, account) = decode_account(read)?;
Ok(SourceCommitment {
role: String::from(*role),
commitment: Commitment::new(&id, &account).to_byte_array(),
})
})
.collect()
}
pub(super) fn append_holding_source(
sources: &mut Vec<SourceCommitment>,
role: &str,
holding: Option<&SelectedHolding>,
) {
if let Some(holding) = holding {
sources.push(SourceCommitment {
role: String::from(role),
commitment: Commitment::new(&holding.id, &holding.account).to_byte_array(),
});
}
}
pub(super) fn hash_quote(commitment: &QuoteCommitment) -> Result<String, String> {
let bytes = borsh::to_vec(commitment)
.map_err(|error| format!("quote commitment serialization failed: {error}"))?;
Ok(format!("sha256:{}", hex::encode(Sha256::digest(bytes))))
}
+1 -6
View File
@@ -1,7 +1,4 @@
use nssa_core::{
account::{Account, AccountId},
program::ProgramId,
};
use nssa_core::{account::AccountId, program::ProgramId};
use token_core::TokenHolding;
use crate::account::{decode_account, AccountRead};
@@ -11,7 +8,6 @@ pub(super) struct SelectedHolding {
pub(super) id: AccountId,
pub(super) definition_id: AccountId,
pub(super) balance: u128,
pub(super) account: Account,
}
pub(super) fn wallet_holdings(
@@ -44,7 +40,6 @@ pub(super) fn decode_fungible_holding(
id,
definition_id,
balance,
account,
})
}
+3 -14
View File
@@ -1,15 +1,10 @@
//! Transport-independent AMM client operations.
mod accounts;
mod clock;
mod commitment;
mod config;
mod context;
mod funding;
mod holding;
mod liquidity;
mod pair;
mod position;
mod quote;
mod quote_error;
mod request;
@@ -23,10 +18,9 @@ use std::{error::Error, fmt};
pub use request::{
AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, ContextRequest,
CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, PairSnapshot, PoolIdRequest,
PositionRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest,
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
TokenHoldingsRequest, TokenIdsRequest,
CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest,
ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
SwapExactOutQuoteRequest, SwapPairRequest, TokenHoldingsRequest, TokenIdsRequest,
};
use serde_json::Value;
@@ -86,11 +80,6 @@ pub fn context(request: ContextRequest) -> AmmResult {
context::context(request).map_err(Into::into)
}
/// Evaluates a pool-creation or add-liquidity request.
pub fn quote(request: QuoteRequest) -> AmmResult {
quote::quote(request).map_err(Into::into)
}
/// Derives the canonical account ids for a swap pair (tokens in either order).
pub fn swap_pair(request: SwapPairRequest) -> AmmResult {
swap::swap_pair(request).map_err(Into::into)
-4
View File
@@ -22,8 +22,6 @@ pub(super) struct PairIds {
pub(super) lp_lock_holding: AccountId,
pub(super) current_tick: AccountId,
pub(super) clock: AccountId,
pub(super) token_program: ProgramId,
pub(super) twap_program: ProgramId,
}
pub(super) fn pair_ids(request: PairIdsRequest) -> Result<Value, String> {
@@ -71,8 +69,6 @@ pub(super) fn derive_pair(
lp_lock_holding: compute_lp_lock_holding_pda(amm_program, pool),
current_tick: compute_current_tick_account_pda(config.twap_oracle_program_id, pool),
clock: CLOCK_01_PROGRAM_ACCOUNT_ID,
token_program: config.token_program_id,
twap_program: config.twap_oracle_program_id,
})
}
-148
View File
@@ -1,148 +0,0 @@
use nssa_core::{account::AccountId, program::ProgramId};
use serde_json::{json, Value};
use super::{
commitment::SourceCommitment, holding::SelectedHolding, pair::PairIds, quote_error::issue,
PairSnapshot, PositionRequest,
};
use crate::account::{account_id_from_hex, program_id_base58};
pub(super) struct EvaluatedQuote {
pub(super) value: Value,
}
pub(super) enum QuoteComputation {
Failed(QuoteFailure),
Evaluated(EvaluatedQuote),
}
pub(super) struct QuoteFailure {
pub(super) code: &'static str,
pub(super) fields: Vec<&'static str>,
pub(super) details: Value,
}
pub(super) struct AccountPlan {
pub(super) rows: Vec<AccountPlanRow>,
pub(super) sources: Vec<SourceCommitment>,
}
pub(super) struct AccountPlanRow {
pub(super) role: &'static str,
pub(super) account_id: Option<AccountId>,
pub(super) expected_program: Option<ProgramId>,
pub(super) action: &'static str,
pub(super) signer: bool,
pub(super) init: bool,
}
pub(super) struct AccountPlanHoldings<'a> {
pub(super) token_a: Option<&'a SelectedHolding>,
pub(super) token_b: Option<&'a SelectedHolding>,
pub(super) lp: Option<&'a SelectedHolding>,
}
impl QuoteComputation {
pub(super) fn into_value(self, request: &PositionRequest) -> Value {
match self {
Self::Failed(failure) => failure.into_value(request),
Self::Evaluated(EvaluatedQuote { value, .. }) => value,
}
}
}
impl QuoteFailure {
pub(super) fn into_value(self, request: &PositionRequest) -> Value {
json!({
"status": "error",
"canSubmit": false,
"code": self.code,
"poolStatus": "unavailable_pool",
"tokenAId": request.token_a_id,
"tokenBId": request.token_b_id,
"accountPreview": [],
"errors": [issue(
self.code,
"Position quote is unavailable.",
&self.fields,
self.details,
)],
"warnings": [],
})
}
}
impl AccountPlan {
pub(super) fn validate_snapshot_ids(
pair: &PairIds,
snapshot: &PairSnapshot,
) -> Option<&'static str> {
let expected = [
(&snapshot.config, pair.config, "config"),
(&snapshot.token_a, pair.token_a, "token_a"),
(&snapshot.token_b, pair.token_b, "token_b"),
(&snapshot.pool, pair.pool, "pool"),
(&snapshot.vault_a, pair.vault_a, "vault_a"),
(&snapshot.vault_b, pair.vault_b, "vault_b"),
(&snapshot.lp_definition, pair.lp_definition, "lp_definition"),
(
&snapshot.lp_lock_holding,
pair.lp_lock_holding,
"lp_lock_holding",
),
(&snapshot.current_tick, pair.current_tick, "current_tick"),
(&snapshot.clock, pair.clock, "clock"),
];
expected.into_iter().find_map(|(read, id, role)| {
match account_id_from_hex(&read.id, "account id") {
Ok(actual) if actual == id => None,
_ => Some(role),
}
})
}
pub(super) fn preview(&self) -> Vec<Value> {
self.rows
.iter()
.enumerate()
.map(|(order, row)| row.preview(order))
.collect()
}
pub(super) fn take_sources(&mut self) -> Vec<SourceCommitment> {
std::mem::take(&mut self.sources)
}
}
impl AccountPlanRow {
pub(super) fn new(
role: &'static str,
account_id: Option<AccountId>,
expected_program: Option<ProgramId>,
action: &'static str,
signer: bool,
init: bool,
) -> Self {
Self {
role,
account_id,
expected_program,
action,
signer,
init,
}
}
fn preview(&self, order: usize) -> Value {
json!({
"order": order,
"role": self.role,
"accountId": self.account_id.map(|id| id.to_string()),
"expectedProgramId": self.expected_program.map(program_id_base58),
"action": self.action,
"writable": self.action != "read",
"signer": self.signer,
"init": self.init,
})
}
}
+10 -636
View File
@@ -1,643 +1,17 @@
//! Shared opening-deposit math for pool creation.
//!
//! Reused by `liquidity::liquidity_quote` to size the smallest deposit that clears
//! `MINIMUM_LIQUIDITY` for a given opening price.
use alloy_primitives::U256;
use amm_core::{
is_supported_fee_tier, isqrt_product, mul_div_floor, spot_price_q64_64, PoolDefinition,
FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY,
};
use nssa_core::{
account::{Account, AccountId},
program::ProgramId,
};
use serde_json::{json, Value};
use token_core::TokenDefinition;
use twap_oracle_core::CurrentTickAccount;
use amm_core::MINIMUM_LIQUIDITY;
use super::{
accounts::{active_account_plan, missing_account_plan},
clock::decode_clock,
commitment::{QuoteCommitment, RequestCommitment},
context::fungible_definition,
funding::{funding_commitments, funding_issues, hash_quote},
holding::{decode_fungible_holding, select_holding, wallet_holdings},
pair::{derive_pair, is_canonical_pair, PairIds},
position::{AccountPlan, AccountPlanHoldings, EvaluatedQuote, QuoteComputation},
quote_error::{fatal_quote, issue},
QuoteRequest,
};
use crate::account::{
decode_account, parse_base58_id, parse_program_id, program_id_bytes, AccountRead,
};
const DEFAULT_SLIPPAGE_BPS: u32 = 50;
const MAX_SLIPPAGE_BPS: u32 = 5_000;
const HIGH_SLIPPAGE_BPS: u32 = 2_000;
/// `Q64.64` scaling factor (`2^64`).
pub(super) const Q64: u128 = 1_u128 << 64;
pub(super) fn quote(request: QuoteRequest) -> Result<Value, String> {
Ok(compute_quote(&request)?.into_value(&request.request))
}
pub(super) fn compute_quote(input: &QuoteRequest) -> Result<QuoteComputation, String> {
let amm_program = parse_program_id(&input.amm_program_id)?;
let token_a = match parse_base58_id(&input.request.token_a_id, "token A id") {
Ok(id) => id,
Err(_) => return Ok(fatal_quote("invalid_token_id", &["tokenAId"], json!({}))),
};
let token_b = match parse_base58_id(&input.request.token_b_id, "token B id") {
Ok(id) => id,
Err(_) => return Ok(fatal_quote("invalid_token_id", &["tokenBId"], json!({}))),
};
if token_a == token_b {
return Ok(fatal_quote(
"same_token_pair",
&["tokenAId", "tokenBId"],
json!({}),
));
}
if !is_canonical_pair(token_a, token_b) {
return Ok(fatal_quote(
"non_canonical_pair",
&["tokenAId", "tokenBId"],
json!({}),
));
}
if !is_supported_fee_tier(u128::from(input.request.fee_bps)) {
return Ok(fatal_quote(
"invalid_fee_tier",
&["feeBps"],
json!({ "feeBps": input.request.fee_bps }),
));
}
let pair = match derive_pair(amm_program, token_a, token_b, &input.snapshot.config) {
Ok(pair) => pair,
Err(_) => return Ok(fatal_quote("config_unavailable", &[], json!({}))),
};
if let Some(error) = AccountPlan::validate_snapshot_ids(&pair, &input.snapshot) {
return Ok(fatal_quote(
"account_read_failed",
&[],
json!({ "role": error }),
));
}
for (read, token_id, field) in [
(&input.snapshot.token_a, token_a, "tokenAId"),
(&input.snapshot.token_b, token_b, "tokenBId"),
] {
if let Err(error) = fungible_definition(Some(read), token_id, pair.token_program) {
return Ok(fatal_quote(
error.code,
&[field],
json!({ "tokenId": token_id.to_string() }),
));
}
}
let (_, pool_account) = match decode_account(&input.snapshot.pool) {
Ok(value) => value,
Err(_) => {
return Ok(fatal_quote(
"account_read_failed",
&[],
json!({ "role": "pool", "accountId": pair.pool.to_string() }),
))
}
};
if pool_account == Account::default() {
compute_missing_quote(input, amm_program, pair)
} else {
compute_active_quote(input, amm_program, pair, pool_account)
}
}
fn compute_missing_quote(
input: &QuoteRequest,
amm_program: ProgramId,
pair: PairIds,
) -> Result<QuoteComputation, String> {
for (read, role) in [
(&input.snapshot.vault_a, "vault_a"),
(&input.snapshot.vault_b, "vault_b"),
(&input.snapshot.lp_definition, "lp_definition"),
(&input.snapshot.lp_lock_holding, "lp_lock_holding"),
(&input.snapshot.current_tick, "current_tick"),
] {
let Ok((_, account)) = decode_account(read) else {
return Ok(fatal_quote(
"account_read_failed",
&[],
json!({ "role": role }),
));
};
if account != Account::default() {
return Ok(fatal_quote(
"pool_unavailable",
&[],
json!({ "role": role }),
));
}
}
if !valid_clock(&input.snapshot.clock, pair.clock) {
return Ok(fatal_quote(
"account_read_failed",
&[],
json!({ "role": "clock" }),
));
}
let requested_price = match raw_value(input.request.initial_price_real_raw.as_deref()) {
Ok(value) if value > 0 => value,
Ok(_) => {
return Ok(fatal_quote(
"amount_must_be_positive",
&["initialPriceRealRaw"],
json!({}),
))
}
Err(code) => return Ok(fatal_quote(code, &["initialPriceRealRaw"], json!({}))),
};
let (minimum_a, minimum_b) = minimum_opening_pair(requested_price)?;
let direct_amounts =
input.request.amount_a_raw.is_some() || input.request.amount_b_raw.is_some();
let (amount_a, amount_b) = if direct_amounts {
let amount_a = match raw_value(input.request.amount_a_raw.as_deref()) {
Ok(value) if value > 0 => value,
Ok(_) => {
return Ok(fatal_quote(
"amount_must_be_positive",
&["amountARaw"],
json!({}),
))
}
Err(code) => return Ok(fatal_quote(code, &["amountARaw"], json!({}))),
};
let amount_b = match raw_value(input.request.amount_b_raw.as_deref()) {
Ok(value) if value > 0 => value,
Ok(_) => {
return Ok(fatal_quote(
"amount_must_be_positive",
&["amountBRaw"],
json!({}),
))
}
Err(code) => return Ok(fatal_quote(code, &["amountBRaw"], json!({}))),
};
if spot_price_q64_64(amount_a, amount_b) != requested_price {
return Ok(fatal_quote(
"deposit_ratio_mismatch",
&["amountARaw", "amountBRaw"],
json!({}),
));
}
(amount_a, amount_b)
} else {
(minimum_a, minimum_b)
};
let initial_lp = isqrt_product(amount_a, amount_b);
if initial_lp <= MINIMUM_LIQUIDITY {
return Ok(fatal_quote(
"amount_too_low",
&["amountARaw", "amountBRaw"],
json!({ "minimumLiquidityRaw": MINIMUM_LIQUIDITY.to_string() }),
));
}
let expected_lp = initial_lp - MINIMUM_LIQUIDITY;
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
let holding_a = select_holding(&holdings, pair.token_a);
let holding_b = select_holding(&holdings, pair.token_b);
let funding = funding_issues(
input.snapshot.wallet_available,
pair,
&holding_a,
amount_a,
&holding_b,
amount_b,
["amountARaw", "amountBRaw"],
);
let can_submit = funding.is_empty();
let mut account_plan = missing_account_plan(
input,
pair,
amm_program,
AccountPlanHoldings {
token_a: holding_a.as_ref(),
token_b: holding_b.as_ref(),
lp: None,
},
)?;
let sources = account_plan.take_sources();
let funding_commitment = funding_commitments(pair, &holding_a, amount_a, &holding_b, amount_b);
let commitment = QuoteCommitment {
network_id: input.network_id.clone(),
network_fingerprint: input.network_fingerprint.clone(),
amm_program_id: program_id_bytes(amm_program),
token_a_id: pair.token_a.into_value(),
token_b_id: pair.token_b.into_value(),
fee_bps: input.request.fee_bps,
pool_status: 0,
request: RequestCommitment::Missing { amount_a, amount_b },
max_a: amount_a,
max_b: amount_b,
actual_a: amount_a,
actual_b: amount_b,
expected_lp,
lp_guard: MINIMUM_LIQUIDITY,
requires_fresh_lp: true,
sources,
funding: funding_commitment,
warnings: Vec::new(),
};
let quote_hash = hash_quote(&commitment)?;
let preview = account_plan.preview();
let value = json!({
"status": "ok",
"canSubmit": can_submit,
"code": if can_submit { "ready" } else { "funding_required" },
"poolStatus": "missing_pool",
"instruction": "NewDefinition",
"quoteHash": quote_hash,
"feeBps": input.request.fee_bps,
"poolId": pair.pool.to_string(),
"tokenAId": pair.token_a.to_string(),
"tokenBId": pair.token_b.to_string(),
"maxAmountARaw": amount_a.to_string(),
"maxAmountBRaw": amount_b.to_string(),
"actualAmountARaw": amount_a.to_string(),
"actualAmountBRaw": amount_b.to_string(),
"expectedLpRaw": expected_lp.to_string(),
"lockedLpRaw": MINIMUM_LIQUIDITY.to_string(),
"initialPriceRealRaw": spot_price_q64_64(amount_a, amount_b).to_string(),
"minimumAmountARaw": minimum_a.to_string(),
"minimumAmountBRaw": minimum_b.to_string(),
"requiresFreshLp": true,
"accountPreview": preview,
"errors": funding,
"warnings": [],
});
Ok(QuoteComputation::Evaluated(EvaluatedQuote { value }))
}
fn compute_active_quote(
input: &QuoteRequest,
amm_program: ProgramId,
pair: PairIds,
pool_account: Account,
) -> Result<QuoteComputation, String> {
if pool_account.program_owner != amm_program {
return Ok(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "owner_mismatch" }),
));
}
let Ok(pool) = PoolDefinition::try_from(&pool_account.data) else {
return Ok(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "invalid_pool_data" }),
));
};
let stored_reversed = if pool.definition_token_a_id == pair.token_a
&& pool.definition_token_b_id == pair.token_b
{
false
} else if pool.definition_token_a_id == pair.token_b
&& pool.definition_token_b_id == pair.token_a
{
true
} else {
return Ok(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "pair_mismatch" }),
));
};
if pool.reserve_a == 0 || pool.reserve_b == 0 || pool.liquidity_pool_supply == 0 {
return Ok(fatal_quote("pool_inactive", &[], json!({})));
}
if pool.fees != u128::from(input.request.fee_bps) {
return Ok(fatal_quote(
"fee_tier_mismatch",
&["feeBps"],
json!({ "poolFeeBps": pool.fees.to_string() }),
));
}
if !is_supported_fee_tier(pool.fees) {
return Ok(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "unsupported_pool_fee" }),
));
}
let max_a = match raw_value(input.request.max_amount_a_raw.as_deref()) {
Ok(value) if value > 0 => value,
Ok(_) => {
return Ok(fatal_quote(
"amount_must_be_positive",
&["maxAmountARaw"],
json!({}),
))
}
Err(code) => return Ok(fatal_quote(code, &["maxAmountARaw"], json!({}))),
};
let max_b = match raw_value(input.request.max_amount_b_raw.as_deref()) {
Ok(value) if value > 0 => value,
Ok(_) => {
return Ok(fatal_quote(
"amount_must_be_positive",
&["maxAmountBRaw"],
json!({}),
))
}
Err(code) => return Ok(fatal_quote(code, &["maxAmountBRaw"], json!({}))),
};
let slippage_bps = input.request.slippage_bps.unwrap_or(DEFAULT_SLIPPAGE_BPS);
if slippage_bps > MAX_SLIPPAGE_BPS {
return Ok(fatal_quote(
"invalid_slippage",
&["slippageBps"],
json!({ "maximum": MAX_SLIPPAGE_BPS }),
));
}
let (stored_max_a, stored_max_b) = if stored_reversed {
(max_b, max_a)
} else {
(max_a, max_b)
};
let ideal_a = mul_div_floor(pool.reserve_a, stored_max_b, pool.reserve_b);
let ideal_b = mul_div_floor(pool.reserve_b, stored_max_a, pool.reserve_a);
let stored_actual_a = stored_max_a.min(ideal_a);
let stored_actual_b = stored_max_b.min(ideal_b);
if stored_actual_a == 0 || stored_actual_b == 0 {
return Ok(fatal_quote(
"amount_too_low",
&["maxAmountARaw", "maxAmountBRaw"],
json!({}),
));
}
let expected_lp =
mul_div_floor(pool.liquidity_pool_supply, stored_actual_a, pool.reserve_a).min(
mul_div_floor(pool.liquidity_pool_supply, stored_actual_b, pool.reserve_b),
);
if expected_lp == 0 {
return Ok(fatal_quote(
"amount_too_low",
&["maxAmountARaw", "maxAmountBRaw"],
json!({}),
));
}
let minimum_lp = mul_div_floor(
expected_lp,
FEE_BPS_DENOMINATOR - u128::from(slippage_bps),
FEE_BPS_DENOMINATOR,
);
if minimum_lp == 0 {
return Ok(fatal_quote("minimum_lp_zero", &["slippageBps"], json!({})));
}
let (actual_a, actual_b, reserve_a, reserve_b) = if stored_reversed {
(
stored_actual_b,
stored_actual_a,
pool.reserve_b,
pool.reserve_a,
)
} else {
(
stored_actual_a,
stored_actual_b,
pool.reserve_a,
pool.reserve_b,
)
};
if let Some(error) = validate_active_accounts(input, pair, &pool, stored_reversed) {
return Ok(error);
}
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
let holding_a = select_holding(&holdings, pair.token_a);
let holding_b = select_holding(&holdings, pair.token_b);
let lp_holding = select_holding(&holdings, pair.lp_definition);
let requires_fresh_lp = lp_holding.is_none();
let funding = funding_issues(
input.snapshot.wallet_available,
pair,
&holding_a,
actual_a,
&holding_b,
actual_b,
["maxAmountARaw", "maxAmountBRaw"],
);
let can_submit = funding.is_empty();
let warnings = if slippage_bps >= HIGH_SLIPPAGE_BPS {
vec![issue(
"high_slippage",
"High slippage tolerance.",
&["slippageBps"],
json!({ "slippageBps": slippage_bps }),
)]
} else {
Vec::new()
};
let warning_codes = warnings
.iter()
.filter_map(|warning| warning["code"].as_str().map(String::from))
.collect();
let mut account_plan = active_account_plan(
input,
pair,
amm_program,
&pool,
stored_reversed,
AccountPlanHoldings {
token_a: holding_a.as_ref(),
token_b: holding_b.as_ref(),
lp: lp_holding.as_ref(),
},
)?;
let sources = account_plan.take_sources();
let commitment = QuoteCommitment {
network_id: input.network_id.clone(),
network_fingerprint: input.network_fingerprint.clone(),
amm_program_id: program_id_bytes(amm_program),
token_a_id: pair.token_a.into_value(),
token_b_id: pair.token_b.into_value(),
fee_bps: input.request.fee_bps,
pool_status: 1,
request: RequestCommitment::Active {
max_a,
max_b,
slippage_bps,
},
max_a,
max_b,
actual_a,
actual_b,
expected_lp,
lp_guard: minimum_lp,
requires_fresh_lp,
sources,
funding: funding_commitments(pair, &holding_a, actual_a, &holding_b, actual_b),
warnings: warning_codes,
};
let quote_hash = hash_quote(&commitment)?;
let preview = account_plan.preview();
let value = json!({
"status": "ok",
"canSubmit": can_submit,
"code": if can_submit { "ready" } else { "funding_required" },
"poolStatus": "active_pool",
"instruction": "AddLiquidity",
"quoteHash": quote_hash,
"feeBps": input.request.fee_bps,
"poolFeeBps": pool.fees.to_string(),
"poolId": pair.pool.to_string(),
"tokenAId": pair.token_a.to_string(),
"tokenBId": pair.token_b.to_string(),
"maxAmountARaw": max_a.to_string(),
"maxAmountBRaw": max_b.to_string(),
"actualAmountARaw": actual_a.to_string(),
"actualAmountBRaw": actual_b.to_string(),
"reserveARaw": reserve_a.to_string(),
"reserveBRaw": reserve_b.to_string(),
"liquiditySupplyRaw": pool.liquidity_pool_supply.to_string(),
"expectedLpRaw": expected_lp.to_string(),
"minimumLpRaw": minimum_lp.to_string(),
"initialPriceRealRaw": spot_price_q64_64(reserve_a, reserve_b).to_string(),
"requiresFreshLp": requires_fresh_lp,
"accountPreview": preview,
"errors": funding,
"warnings": warnings,
});
Ok(QuoteComputation::Evaluated(EvaluatedQuote { value }))
}
fn validate_active_accounts(
input: &QuoteRequest,
pair: PairIds,
pool: &PoolDefinition,
stored_reversed: bool,
) -> Option<QuoteComputation> {
let expected_vault_a = if stored_reversed {
pair.vault_b
} else {
pair.vault_a
};
let expected_vault_b = if stored_reversed {
pair.vault_a
} else {
pair.vault_b
};
if pool.vault_a_id != expected_vault_a
|| pool.vault_b_id != expected_vault_b
|| pool.liquidity_pool_id != pair.lp_definition
{
return Some(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "pool_account_mismatch" }),
));
}
let (canonical_vault_a, canonical_vault_b) = (
decode_holding(&input.snapshot.vault_a, pair.token_program, pair.token_a),
decode_holding(&input.snapshot.vault_b, pair.token_program, pair.token_b),
);
let (Ok(vault_a_balance), Ok(vault_b_balance)) = (canonical_vault_a, canonical_vault_b) else {
return Some(fatal_quote(
"account_read_failed",
&[],
json!({ "role": "vault" }),
));
};
let (reserve_a, reserve_b) = if stored_reversed {
(pool.reserve_b, pool.reserve_a)
} else {
(pool.reserve_a, pool.reserve_b)
};
if vault_a_balance < reserve_a || vault_b_balance < reserve_b {
return Some(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "vault_below_reserve" }),
));
}
let Ok((lp_id, lp_account)) = decode_account(&input.snapshot.lp_definition) else {
return Some(fatal_quote(
"account_read_failed",
&[],
json!({ "role": "lp_definition" }),
));
};
if lp_id != pair.lp_definition
|| lp_account.program_owner != pair.token_program
|| !matches!(
TokenDefinition::try_from(&lp_account.data),
Ok(TokenDefinition::Fungible { .. })
)
{
return Some(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "invalid_lp_definition" }),
));
}
let Ok((tick_id, tick_account)) = decode_account(&input.snapshot.current_tick) else {
return Some(fatal_quote(
"account_read_failed",
&[],
json!({ "role": "current_tick" }),
));
};
if tick_id != pair.current_tick
|| tick_account.program_owner != pair.twap_program
|| CurrentTickAccount::try_from(&tick_account.data).is_err()
{
return Some(fatal_quote(
"pool_unavailable",
&[],
json!({ "reason": "invalid_current_tick" }),
));
}
if !valid_clock(&input.snapshot.clock, pair.clock) {
return Some(fatal_quote(
"account_read_failed",
&[],
json!({ "role": "clock" }),
));
}
None
}
fn decode_holding(
read: &AccountRead,
token_program: ProgramId,
definition_id: AccountId,
) -> Result<u128, String> {
let holding = decode_fungible_holding(read, token_program)?;
if holding.definition_id != definition_id {
return Err(String::from("invalid fungible holding"));
}
Ok(holding.balance)
}
fn valid_clock(read: &AccountRead, expected_id: AccountId) -> bool {
matches!(decode_clock(read), Ok((id, _)) if id == expected_id)
}
fn raw_value(value: Option<&str>) -> Result<u128, &'static str> {
let Some(value) = value else {
return Err("amount_required");
};
if value.is_empty() {
return Err("amount_required");
}
if !value.bytes().all(|byte| byte.is_ascii_digit()) {
return Err("invalid_raw_amount");
}
value.parse().map_err(|_| "invalid_raw_amount")
}
/// Smallest `(amount_a, amount_b)` deposit whose geometric-mean LP clears
/// `MINIMUM_LIQUIDITY`, holding the canonical opening `price` (token B per token A,
/// `Q64.64`). Binary-searches the smaller side, then derives the paired amount by ceil.
pub(super) fn minimum_opening_pair(price: u128) -> Result<(u128, u128), String> {
let minimum_initial_lp = U256::from(MINIMUM_LIQUIDITY + 1);
let target_product = minimum_initial_lp
-14
View File
@@ -1,7 +1,5 @@
use serde_json::{json, Value};
use super::position::{QuoteComputation, QuoteFailure};
pub(super) fn issue(code: &str, message: &str, fields: &[&str], details: Value) -> Value {
json!({
"code": code,
@@ -11,15 +9,3 @@ pub(super) fn issue(code: &str, message: &str, fields: &[&str], details: Value)
"blockingFields": fields,
})
}
pub(super) fn fatal_quote(
code: &'static str,
fields: &[&'static str],
details: Value,
) -> QuoteComputation {
QuoteComputation::Failed(QuoteFailure {
code,
fields: fields.to_vec(),
details,
})
}
-48
View File
@@ -230,51 +230,3 @@ pub struct TokenHoldingsRequest {
pub struct ProgramIdRequest {
pub elf: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PositionRequest {
pub token_a_id: String,
pub token_b_id: String,
pub fee_bps: u32,
#[serde(default)]
pub amount_a_raw: Option<String>,
#[serde(default)]
pub amount_b_raw: Option<String>,
#[serde(default)]
pub max_amount_a_raw: Option<String>,
#[serde(default)]
pub max_amount_b_raw: Option<String>,
#[serde(default)]
pub slippage_bps: Option<u32>,
#[serde(default)]
pub initial_price_real_raw: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PairSnapshot {
pub config: AccountRead,
pub token_a: AccountRead,
pub token_b: AccountRead,
pub pool: AccountRead,
pub vault_a: AccountRead,
pub vault_b: AccountRead,
pub lp_definition: AccountRead,
pub lp_lock_holding: AccountRead,
pub current_tick: AccountRead,
pub clock: AccountRead,
pub wallet_available: bool,
#[serde(default)]
pub wallet_accounts: Vec<AccountRead>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct QuoteRequest {
pub network_id: String,
pub network_fingerprint: String,
pub amm_program_id: String,
pub request: PositionRequest,
pub snapshot: PairSnapshot,
}
+8 -320
View File
@@ -1,29 +1,26 @@
use alloy_primitives::U256;
use amm_core::{
compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda,
compute_vault_pda, isqrt_product, spot_price_q64_64, AmmConfig, PoolDefinition,
MINIMUM_LIQUIDITY,
compute_vault_pda, isqrt_product, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
};
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
use nssa_core::{
account::{Account, AccountId, Data, Nonce},
program::ProgramId,
};
use pretty_assertions::assert_eq;
use serde_json::{json, Value};
use serde_json::json;
use token_core::{TokenDefinition, TokenHolding};
use twap_oracle_core::{compute_current_tick_account_pda, CurrentTickAccount};
use twap_oracle_core::compute_current_tick_account_pda;
use super::{
accounts::{active_account_plan, missing_account_plan},
context::{context, token_ids},
holding::{select_holding, wallet_holdings, SelectedHolding},
holding::{select_holding, SelectedHolding},
pair::{is_canonical_pair, pair_ids, PairIds},
position::AccountPlanHoldings,
quote::{div_ceil_u256, minimum_opening_pair, quote, Q64},
quote::{div_ceil_u256, minimum_opening_pair, Q64},
swap::{swap_exact_in_plan, swap_exact_out_plan},
ContextRequest, PairIdsRequest, PairSnapshot, PositionRequest, QuoteRequest,
SwapExactInPlanRequest, SwapExactOutPlanRequest, TokenIdsRequest,
ContextRequest, PairIdsRequest, SwapExactInPlanRequest, SwapExactOutPlanRequest,
TokenIdsRequest,
};
use crate::{
account::{account_id_hex, account_read, decode_account, program_id_bytes},
@@ -80,20 +77,6 @@ fn token_holding(definition_id: AccountId, balance: u128) -> Account {
)
}
fn clock_account() -> Account {
account(
[44; 8],
Data::try_from(
ClockAccountData {
block_id: 10,
timestamp: 1_000,
}
.to_bytes(),
)
.unwrap(),
)
}
fn ids() -> PairIds {
let token_a = AccountId::new([2; 32]);
let token_b = AccountId::new([1; 32]);
@@ -110,45 +93,6 @@ fn ids() -> PairIds {
lp_lock_holding: compute_lp_lock_holding_pda(AMM_PROGRAM, pool),
current_tick: compute_current_tick_account_pda(TWAP_PROGRAM, pool),
clock: CLOCK_01_PROGRAM_ACCOUNT_ID,
token_program: TOKEN_PROGRAM,
twap_program: TWAP_PROGRAM,
}
}
fn base_snapshot(pair: PairIds) -> PairSnapshot {
let holding_a_id = AccountId::new([61; 32]);
let holding_b_id = AccountId::new([62; 32]);
PairSnapshot {
config: account_read(pair.config, &config_account()),
token_a: account_read(pair.token_a, &token_definition("A", 1_000_000)),
token_b: account_read(pair.token_b, &token_definition("B", 2_000_000)),
pool: default_read(pair.pool),
vault_a: default_read(pair.vault_a),
vault_b: default_read(pair.vault_b),
lp_definition: default_read(pair.lp_definition),
lp_lock_holding: default_read(pair.lp_lock_holding),
current_tick: default_read(pair.current_tick),
clock: account_read(pair.clock, &clock_account()),
wallet_available: true,
wallet_accounts: vec![
account_read(holding_a_id, &token_holding(pair.token_a, 1_000_000)),
account_read(holding_b_id, &token_holding(pair.token_b, 1_000_000)),
],
}
}
fn request(pair: PairIds) -> PositionRequest {
assert!(is_canonical_pair(pair.token_a, pair.token_b));
PositionRequest {
token_a_id: pair.token_a.to_string(),
token_b_id: pair.token_b.to_string(),
fee_bps: 30,
amount_a_raw: None,
amount_b_raw: None,
max_amount_a_raw: None,
max_amount_b_raw: None,
slippage_bps: None,
initial_price_real_raw: Some(Q64.to_string()),
}
}
@@ -156,124 +100,6 @@ fn amm_program_id() -> String {
hex::encode(program_id_bytes(AMM_PROGRAM))
}
struct Scenario {
pair: PairIds,
request: PositionRequest,
snapshot: PairSnapshot,
network_id: &'static str,
network_fingerprint: &'static str,
}
impl Scenario {
fn devnet() -> Self {
Self::new("devnet", "channel:test")
}
fn testnet() -> Self {
Self::new("testnet", "block10:test")
}
fn new(network_id: &'static str, network_fingerprint: &'static str) -> Self {
let pair = ids();
Self {
pair,
request: request(pair),
snapshot: base_snapshot(pair),
network_id,
network_fingerprint,
}
}
fn quote_request(&self) -> QuoteRequest {
QuoteRequest {
network_id: String::from(self.network_id),
network_fingerprint: String::from(self.network_fingerprint),
amm_program_id: amm_program_id(),
request: self.request.clone(),
snapshot: self.snapshot.clone(),
}
}
fn quote(&self) -> Value {
quote(self.quote_request()).unwrap()
}
}
#[test]
fn account_plan_sources_follow_pool_branch() {
let scenario = Scenario::devnet();
let pair = scenario.pair;
let input = scenario.quote_request();
let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program);
let holding_a = select_holding(&holdings, pair.token_a);
let holding_b = select_holding(&holdings, pair.token_b);
let missing = missing_account_plan(
&input,
pair,
AMM_PROGRAM,
AccountPlanHoldings {
token_a: holding_a.as_ref(),
token_b: holding_b.as_ref(),
lp: None,
},
)
.unwrap();
assert_eq!(
missing
.sources
.iter()
.map(|source| source.role.as_str())
.collect::<Vec<_>>(),
vec![
"config",
"token_a",
"token_b",
"pool",
"vault_a",
"vault_b",
"lp_definition",
"lp_lock_holding",
"current_tick",
"holding_a",
"holding_b",
]
);
let active = active_account_plan(
&input,
pair,
AMM_PROGRAM,
&PoolDefinition::default(),
false,
AccountPlanHoldings {
token_a: holding_a.as_ref(),
token_b: holding_b.as_ref(),
lp: None,
},
)
.unwrap();
assert_eq!(
active
.sources
.iter()
.map(|source| source.role.as_str())
.collect::<Vec<_>>(),
vec![
"config",
"token_a",
"token_b",
"pool",
"vault_a",
"vault_b",
"lp_definition",
"current_tick",
"holding_a",
"holding_b",
]
);
}
#[test]
fn minimum_pair_exceeds_protocol_lock() {
for price in [1, Q64 / 2_500, Q64 / 10, Q64, Q64 * 2, u128::MAX] {
@@ -304,13 +130,6 @@ fn highest_balance_holding_wins_then_lowest_id() {
id: AccountId::new([id; 32]),
definition_id: definition,
balance,
account: account(
TOKEN_PROGRAM,
Data::from(&TokenHolding::Fungible {
definition_id: definition,
balance,
}),
),
};
let selected = select_holding(
&[holding(4, 10), holding(2, 20), holding(1, 20)],
@@ -481,137 +300,6 @@ fn missing_pool_snapshot_defaults_remain_real_accounts() {
assert_eq!(decoded, Account::default());
}
#[test]
fn missing_pool_quote_uses_current_account_order() {
let scenario = Scenario::devnet();
let quote_value = scenario.quote();
assert_eq!(quote_value["status"], "ok");
assert_eq!(quote_value["poolStatus"], "missing_pool");
assert_eq!(quote_value["canSubmit"], true);
// The 11-account NewDefinition preview order (config, pool, vaults, lp def/lock,
// holdings a/b/lp, current_tick, clock) — the create submit consumes the same shape.
let preview = quote_value["accountPreview"].as_array().unwrap();
assert_eq!(preview.len(), 11);
assert_eq!(preview[6]["role"], "user_holding_a");
assert_eq!(preview[6]["signer"], true);
assert_eq!(preview[7]["signer"], true);
assert_eq!(preview[8]["role"], "user_holding_lp");
assert_eq!(preview[8]["signer"], true);
}
#[test]
fn missing_pool_quote_accepts_large_direct_raw_amounts() {
let mut scenario = Scenario::devnet();
let amount_a = 100_000_000;
let amount_b = 150_000_000;
scenario.request.amount_a_raw = Some(amount_a.to_string());
scenario.request.amount_b_raw = Some(amount_b.to_string());
scenario.request.initial_price_real_raw =
Some(spot_price_q64_64(amount_a, amount_b).to_string());
let quote_value = scenario.quote();
assert_eq!(quote_value["status"], "ok");
assert_eq!(quote_value["actualAmountARaw"], amount_a.to_string());
assert_eq!(quote_value["actualAmountBRaw"], amount_b.to_string());
assert!(quote_value.get("depositScaleBps").is_none());
}
#[test]
fn active_pool_quote_uses_ratio_and_existing_lp_holding() {
let mut scenario = Scenario::testnet();
let pair = scenario.pair;
let pool = PoolDefinition {
definition_token_a_id: pair.token_a,
definition_token_b_id: pair.token_b,
vault_a_id: pair.vault_a,
vault_b_id: pair.vault_b,
liquidity_pool_id: pair.lp_definition,
liquidity_pool_supply: 10_000,
reserve_a: 10_000,
reserve_b: 20_000,
fees: 30,
};
scenario.snapshot.pool = account_read(pair.pool, &account(AMM_PROGRAM, Data::from(&pool)));
scenario.snapshot.vault_a =
account_read(pair.vault_a, &token_holding(pair.token_a, pool.reserve_a));
scenario.snapshot.vault_b =
account_read(pair.vault_b, &token_holding(pair.token_b, pool.reserve_b));
scenario.snapshot.lp_definition = account_read(
pair.lp_definition,
&account(
TOKEN_PROGRAM,
Data::from(&TokenDefinition::Fungible {
name: String::from("LP"),
total_supply: pool.liquidity_pool_supply,
metadata_id: None,
authority: Some(pair.lp_definition),
}),
),
);
scenario.snapshot.current_tick = account_read(
pair.current_tick,
&account(
TWAP_PROGRAM,
Data::from(&CurrentTickAccount {
tick: 0,
last_updated: 1_000,
}),
),
);
scenario.snapshot.wallet_accounts = vec![
account_read(
AccountId::new([61; 32]),
&token_holding(pair.token_a, 1_000),
),
account_read(
AccountId::new([62; 32]),
&token_holding(pair.token_b, 2_000),
),
];
let lp_holding = AccountId::new([64; 32]);
scenario.snapshot.wallet_accounts.push(account_read(
lp_holding,
&token_holding(pair.lp_definition, 500),
));
scenario.request.initial_price_real_raw = None;
scenario.request.max_amount_a_raw = Some(String::from("1000"));
scenario.request.max_amount_b_raw = Some(String::from("3000"));
scenario.request.slippage_bps = Some(50);
let quote_value = scenario.quote();
assert_eq!(quote_value["poolStatus"], "active_pool");
assert_eq!(quote_value["actualAmountARaw"], "1000");
assert_eq!(quote_value["actualAmountBRaw"], "2000");
assert_eq!(quote_value["expectedLpRaw"], "1000");
assert_eq!(quote_value["minimumLpRaw"], "995");
assert_eq!(quote_value["requiresFreshLp"], false);
assert_eq!(quote_value["canSubmit"], true);
assert_eq!(quote_value["errors"], json!([]));
// The existing LP holding (not a fresh one) fills the LP slot of the 10-account
// AddLiquidity preview, and only the two token holdings sign.
let preview = quote_value["accountPreview"].as_array().unwrap();
assert_eq!(preview.len(), 10);
assert_eq!(preview[7]["role"], "user_holding_lp");
assert_eq!(
preview[7]["accountId"].as_str().unwrap(),
lp_holding.to_string()
);
assert_eq!(preview[7]["signer"], false);
assert_eq!(preview[5]["signer"], true);
assert_eq!(preview[6]["signer"], true);
}
#[test]
fn unfunded_quote_cannot_submit() {
let mut scenario = Scenario::devnet();
scenario.snapshot.wallet_available = false;
scenario.snapshot.wallet_accounts.clear();
let quote_value = scenario.quote();
assert_eq!(quote_value["canSubmit"], false);
}
#[test]
fn swap_plan_uses_the_pool_stored_vaults_not_canonical_order() {
// A pool created NON-canonically: its stored def_a is the smaller-valued
+1 -6
View File
@@ -8,7 +8,7 @@ use serde::{de::DeserializeOwned, Serialize};
use crate::api::{
self, AddLiquidityPlanRequest, AddLiquidityQuoteRequest, AmmApiError, AmmResult,
ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest,
PoolIdRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest,
PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, SwapExactInPlanRequest,
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
TokenHoldingsRequest, TokenIdsRequest,
};
@@ -99,11 +99,6 @@ 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_quote(request_json: *const c_char) -> *mut c_char {
call::<QuoteRequest>(request_json, api::quote)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_swap_pair(request_json: *const c_char) -> *mut c_char {
call::<SwapPairRequest>(request_json, api::swap_pair)
+4 -4
View File
@@ -6,11 +6,11 @@ mod ffi;
pub mod api;
pub use api::{
config_id, context, create_pool_plan, liquidity_quote, pair_ids, pool_id, program_id, quote,
config_id, context, create_pool_plan, liquidity_quote, pair_ids, pool_id, program_id,
resolve_pool, 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, LiquidityQuoteRequest, PairIdsRequest,
PairSnapshot, PoolIdRequest, PositionRequest, ProgramIdRequest, QuoteRequest,
ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
SwapExactOutQuoteRequest, SwapPairRequest, TokenIdsRequest, WalletAccount,
PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, SwapExactInPlanRequest,
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
TokenIdsRequest, WalletAccount,
};
-67
View File
@@ -1096,63 +1096,6 @@ LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) {
return out;
}
nlohmann::json AmmModuleImpl::buildQuoteInput(const LogosMap& request,
const Network& net,
bool wallet_open,
bool fresh_wallet_accounts,
nlohmann::json* error) {
if (net.status != "ready") {
*error = publicError(net.status);
return json();
}
const FfiResult configResult =
call(amm_config_id, json{{"ammProgramId", net.amm_program_id}});
if (!configResult.ok) {
*error = publicError("backend_error");
return json();
}
const json config = readPublicAccount(jStr(configResult.value, "configId"));
const FfiResult pairResult = call(amm_pair_ids, json{
{"ammProgramId", net.amm_program_id},
{"config", config},
{"tokenAId", request.value("tokenAId", json())},
{"tokenBId", request.value("tokenBId", json())},
});
if (!pairResult.ok) {
*error = publicError("backend_error");
return json();
}
const json pairManifest = pairResult.value;
if (jStr(pairManifest, "status") != "ok") {
*error = publicError(jStr(pairManifest, "code"));
return json();
}
const json walletAccounts = walletAccountReads(wallet_open, fresh_wallet_accounts);
const json snapshot = {
{"config", config},
{"tokenA", readPublicAccount(jStr(pairManifest, "tokenAId"))},
{"tokenB", readPublicAccount(jStr(pairManifest, "tokenBId"))},
{"pool", readPublicAccount(jStr(pairManifest, "poolId"))},
{"vaultA", readPublicAccount(jStr(pairManifest, "vaultAId"))},
{"vaultB", readPublicAccount(jStr(pairManifest, "vaultBId"))},
{"lpDefinition", readPublicAccount(jStr(pairManifest, "lpDefinitionId"))},
{"lpLockHolding", readPublicAccount(jStr(pairManifest, "lpLockHoldingId"))},
{"currentTick", readPublicAccount(jStr(pairManifest, "currentTickId"))},
{"clock", readPublicAccount(jStr(pairManifest, "clockId"))},
{"walletAvailable", wallet_open},
{"walletAccounts", walletAccounts},
};
return {
{"networkId", net.id},
{"networkFingerprint", net.fingerprint},
{"ammProgramId", net.amm_program_id},
{"request", request},
{"snapshot", snapshot},
};
}
LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request,
bool wallet_open,
bool refresh_wallet_accounts) {
@@ -1210,13 +1153,3 @@ LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request,
: contextState("error", net.id, net.fingerprint, "backend_error");
}
LogosMap AmmModuleImpl::quoteNewPosition(const LogosMap& request, bool wallet_open) {
const Network net = network();
json error;
const json input = buildQuoteInput(request, net, wallet_open, /*fresh=*/false, &error);
if (!error.is_null()) return error;
const FfiResult result = call(amm_quote, input);
return result.ok ? result.value : publicError("backend_error");
}
-15
View File
@@ -184,11 +184,6 @@ public:
bool wallet_open,
bool refresh_wallet_accounts);
/// Prices an add-liquidity request against current on-chain state and
/// returns the new-position quote map (quoteHash, canSubmit,
/// requiresFreshLp, amounts, warnings). Read-only — no submission.
LogosMap quoteNewPosition(const LogosMap& request, bool wallet_open);
private:
// Off-chain "network" context, derived from the process env (the same
// sources the app backend used): AMM deployment id from AMM_PROGRAM_BIN,
@@ -235,16 +230,6 @@ private:
// live sequencer round-trip.
nlohmann::json walletAccountReads(bool wallet_open, bool refresh);
// Builds the { networkId, networkFingerprint, ammProgramId, request,
// snapshot } input for quoteNewPosition's amm_quote call. On a recoverable
// precondition failure, sets *error to a new-position error map and returns
// a null json.
nlohmann::json buildQuoteInput(const LogosMap& request,
const Network& net,
bool wallet_open,
bool fresh_wallet_accounts,
nlohmann::json* error);
// 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.