From b1b6ec851746731b856a3e4b053469b6c49da59d Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:09:36 +0200 Subject: [PATCH] refactor(amm): remove the dead submitNewPosition path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that add-liquidity and pool creation submit via addLiquidity / createPool, the legacy submitNewPosition path is dead. Remove it end to end. App backend (AmmUiBackend): - Drop the submitNewPosition slot/method and its newPositionError helper. Module (AmmModuleImpl): - Drop submitNewPosition, the m_requestPending guard, and the now-orphaned nowMs/parseU64 helpers (+ the include). FFI (amm_ffi) — the plan op only submitNewPosition called: - Delete plan.rs; remove amm_plan (extern + regenerated header), api::plan, and PlanRequest. - Remove the plan-only machinery it fed: QuoteBranch, NewPositionPlan, EvaluatedQuote.plan/quote_hash, and AccountPlan's wallet_args / requires_fresh_lp / contains / validate_ready, plus the plan construction in quote.rs. - Tests: drop the plan-only tests/helpers; keep quote coverage by trimming the mixed tests to their quote assertions. QML tests (tst_LiquidityPage): - Remove the two legacy-submit tests (base58-only success) + the submitNewPosition mock and its now-unused fixtures. Keep the finishSubmitFailure test (unchanged behaviour). quoteNewPosition and its machinery (amm_quote, buildQuoteInput, PairSnapshot, AccountPlan preview/sources, commitment) stay — they retire with the legacy quoting in the quote-migration vertical. --- apps/amm/src/AmmUiBackend.cpp | 42 ------- apps/amm/src/AmmUiBackend.h | 1 - apps/amm/src/AmmUiBackend.rep | 1 - apps/amm/tests/qml/tst_LiquidityPage.qml | 73 ------------ modules/amm/ffi/include/amm_ffi.h | 2 - modules/amm/ffi/src/api/mod.rs | 14 +-- modules/amm/ffi/src/api/plan.rs | 130 --------------------- modules/amm/ffi/src/api/position.rs | 80 ------------- modules/amm/ffi/src/api/quote.rs | 38 +----- modules/amm/ffi/src/api/request.rs | 14 --- modules/amm/ffi/src/api/tests.rs | 140 ++++------------------- modules/amm/ffi/src/ffi.rs | 11 +- modules/amm/ffi/src/lib.rs | 6 +- modules/amm/src/amm_module_impl.cpp | 112 ------------------ modules/amm/src/amm_module_impl.h | 23 +--- 15 files changed, 41 insertions(+), 646 deletions(-) delete mode 100644 modules/amm/ffi/src/api/plan.rs diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 3b89b48..5cacdbf 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -22,25 +22,6 @@ namespace { { QStringLiteral("warnings"), QVariantList() }, }; } - - // A new-position error envelope (matches the module's publicError), for - // the backend-side failure paths (e.g. LP-account creation failing). - QVariantMap newPositionError(const QString& code) - { - return QVariantMap { - { QStringLiteral("status"), QStringLiteral("error") }, - { QStringLiteral("canSubmit"), false }, - { QStringLiteral("code"), code }, - { QStringLiteral("errors"), QVariantList { QVariantMap { - { QStringLiteral("code"), code }, - { QStringLiteral("recoverable"), true }, - { QStringLiteral("blockingFields"), QVariantList() }, - { QStringLiteral("details"), QVariantMap() }, - } } }, - { QStringLiteral("warnings"), QVariantList() }, - { QStringLiteral("accountPreview"), QVariantList() }, - }; - } } AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) @@ -156,29 +137,6 @@ QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request) return m_logos->amm_module.quoteNewPosition(request, isWalletOpen()); } -QVariantMap AmmUiBackend::submitNewPosition(QVariantMap request, QString quoteHash) -{ - // First attempt with no LP account. If the module needs a fresh LP holding - // it returns "requires_fresh_lp" without submitting; we own wallet-keyset - // mutation, so create the account here (keeping the account model + on-disk - // storage coherent) and resubmit with its id. - QVariantMap result = m_logos->amm_module.submitNewPosition( - request, quoteHash, isWalletOpen(), QString()); - - if (result.value(QStringLiteral("status")).toString() - == QStringLiteral("requires_fresh_lp")) { - const QString lpId = m_walletController->createAccount(true); - if (lpId.isEmpty()) - return newPositionError(QStringLiteral("wallet_submission_failed")); - result = m_logos->amm_module.submitNewPosition( - request, quoteHash, isWalletOpen(), lpId); - } - - if (result.value(QStringLiteral("status")).toString() == QStringLiteral("submitted")) - refreshBalances(); - return result; -} - void AmmUiBackend::syncWalletState() { const WalletUiState& state = m_walletController->state(); diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 7825c92..238badf 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -48,7 +48,6 @@ public slots: QString getBalance(QString accountIdHex, bool isPublic) override; void refreshNewPositionContext(QVariantMap request) override; QVariantMap quoteNewPosition(QVariantMap request) override; - QVariantMap submitNewPosition(QVariantMap request, QString quoteHash) 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; diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep index f68399f..f6eaf77 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -32,7 +32,6 @@ class AmmUiBackend PROP(QVariantMap newPositionContext READONLY) SLOT(void refreshNewPositionContext(QVariantMap request)) SLOT(QVariantMap quoteNewPosition(QVariantMap request)) - SLOT(QVariantMap submitNewPosition(QVariantMap request, QString quoteHash)) // Wallet lifecycle. createNewDefault() is the happy path: it creates a // fresh wallet at the canonical walletHome with no path picking. createNew() diff --git a/apps/amm/tests/qml/tst_LiquidityPage.qml b/apps/amm/tests/qml/tst_LiquidityPage.qml index adc723a..4edc34e 100644 --- a/apps/amm/tests/qml/tst_LiquidityPage.qml +++ b/apps/amm/tests/qml/tst_LiquidityPage.qml @@ -9,15 +9,12 @@ TestCase { id: testCase name: "LiquidityPage" - readonly property string submittedTransactionId: - "1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE" Component { id: backendComponent QtObject { property bool walletStateReady: false - property var submitResult: ({}) property var quoteResult: ({ "status": "ok", "poolStatus": "missing_pool" @@ -28,10 +25,6 @@ TestCase { "feeTiers": [] }) - function submitNewPosition(request, quoteHash) { - return submitResult - } - function quoteNewPosition(request) { return quoteResult } @@ -64,16 +57,6 @@ TestCase { verify(form.width <= page.width - 32) } - Component { - id: runtimeComponent - - QtObject { - function watch(value, succeeded, failed) { - succeeded(value) - } - } - } - Component { id: pageComponent @@ -163,60 +146,4 @@ TestCase { compare(page.flow.quoteStale, false) } - function test_base58SubmittedResultEntersSuccessState() { - var backend = createTemporaryObject(backendComponent, testCase, { - "walletStateReady": true, - "submitResult": { - "status": "submitted", - "transactionId": submittedTransactionId, - "deadlineMs": String(Date.now() + 60000) - } - }) - var runtime = createTemporaryObject(runtimeComponent, testCase) - var page = createTemporaryObject(pageComponent, testCase, { - "backend": backend, - "runtime": runtime - }) - verify(backend) - verify(runtime) - verify(page) - - page.flow.confirm({ - "request": ({}), - "quoteHash": "sha256:expected" - }) - - compare(page.flow.transactionId, submittedTransactionId) - compare(page.flow.flowErrorCode, "") - compare(page.flow.submitting, false) - } - - function test_nativeHexSubmittedResultDoesNotEnterSuccessState() { - var backend = createTemporaryObject(backendComponent, testCase, { - "walletStateReady": true, - "submitResult": { - "status": "submitted", - "transactionId": "000102030405060708090a0b0c0d0e0f" - + "101112131415161718191a1b1c1d1e1f" - } - }) - var runtime = createTemporaryObject(runtimeComponent, testCase) - var page = createTemporaryObject(pageComponent, testCase, { - "backend": backend, - "runtime": runtime - }) - verify(backend) - verify(runtime) - verify(page) - - page.flow.confirm({ - "request": ({}), - "quoteHash": "sha256:expected" - }) - - compare(page.flow.transactionId, "") - compare(page.flow.flowErrorCode, "wallet_submission_failed") - compare(page.flow.submitting, false) - } - } diff --git a/modules/amm/ffi/include/amm_ffi.h b/modules/amm/ffi/include/amm_ffi.h index 1b3abfd..5477821 100644 --- a/modules/amm/ffi/include/amm_ffi.h +++ b/modules/amm/ffi/include/amm_ffi.h @@ -24,8 +24,6 @@ char *amm_context(const char *request_json); char *amm_quote(const char *request_json); -char *amm_plan(const char *request_json); - char *amm_swap_pair(const char *request_json); char *amm_resolve_pool(const char *request_json); diff --git a/modules/amm/ffi/src/api/mod.rs b/modules/amm/ffi/src/api/mod.rs index 40bb4b9..6110927 100644 --- a/modules/amm/ffi/src/api/mod.rs +++ b/modules/amm/ffi/src/api/mod.rs @@ -9,7 +9,6 @@ mod funding; mod holding; mod liquidity; mod pair; -mod plan; mod position; mod quote; mod quote_error; @@ -24,10 +23,10 @@ use std::{error::Error, fmt}; pub use request::{ AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, ContextRequest, - CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, PairSnapshot, PlanRequest, - PoolIdRequest, PositionRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, - SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, - SwapExactOutQuoteRequest, SwapPairRequest, TokenHoldingsRequest, TokenIdsRequest, + CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, PairSnapshot, PoolIdRequest, + PositionRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest, + SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, + TokenHoldingsRequest, TokenIdsRequest, }; use serde_json::Value; @@ -92,11 +91,6 @@ pub fn quote(request: QuoteRequest) -> AmmResult { quote::quote(request).map_err(Into::into) } -/// Materializes a previously quoted request into wallet submission arguments. -pub fn plan(request: PlanRequest) -> AmmResult { - plan::plan(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) diff --git a/modules/amm/ffi/src/api/plan.rs b/modules/amm/ffi/src/api/plan.rs deleted file mode 100644 index a0f9d2b..0000000 --- a/modules/amm/ffi/src/api/plan.rs +++ /dev/null @@ -1,130 +0,0 @@ -use nssa_core::account::Account; -use serde_json::{json, Value}; - -use super::{ - clock::decode_clock, - position::{NewPositionPlan, QuoteBranch, QuoteComputation}, - quote::compute_quote, - PlanRequest, QuoteRequest, -}; -use crate::account::{account_id_hex, decode_account, AccountRead}; - -const DEADLINE_WINDOW_MS: u64 = 1_200_000; - -pub(super) fn plan(input: PlanRequest) -> Result { - let quote_input = QuoteRequest { - network_id: input.network_id, - network_fingerprint: input.network_fingerprint, - amm_program_id: input.amm_program_id.clone(), - request: input.request, - snapshot: input.snapshot, - }; - let quote = compute_quote("e_input)?; - if quote.quote_hash() != Some(input.quote_hash.as_str()) { - return Ok(json!({ - "status": "error", - "code": "quote_changed", - "recoverable": true, - "quote": quote.into_value("e_input.request), - })); - } - let evaluated = match quote { - QuoteComputation::Evaluated(evaluated) => evaluated, - QuoteComputation::Failed(failure) => { - return Ok(json!({ - "status": "error", - "code": "quote_not_submittable", - "recoverable": true, - "quote": failure.into_value("e_input.request), - })) - } - }; - let Some(plan) = evaluated.plan else { - return Ok(json!({ - "status": "error", - "code": "quote_not_submittable", - "recoverable": true, - "quote": evaluated.value, - })); - }; - let fresh_lp = if plan.requires_fresh_lp() { - let Some(read) = input.fresh_lp.as_ref() else { - return Ok(json!({ - "status": "needs_fresh_lp", - "code": "fresh_lp_required", - })); - }; - let Ok((id, account)) = decode_account(read) else { - return Ok(plan_error("wallet_submission_failed")); - }; - if account != Account::default() || plan.accounts.contains(id) { - return Ok(plan_error("wallet_submission_failed")); - } - Some(id) - } else { - None - }; - let deadline = input - .now_ms - .checked_add(DEADLINE_WINDOW_MS) - .ok_or_else(|| String::from("transaction deadline overflow"))?; - let clock_timestamp = clock_timestamp("e_input.snapshot.clock)?; - if clock_timestamp >= deadline { - return Ok(plan_error("transaction_deadline_expired")); - } - let NewPositionPlan { accounts, branch } = plan; - let (account_ids, signing_requirements) = accounts.wallet_args(fresh_lp)?; - let instruction = match branch { - QuoteBranch::Missing { amount_a, amount_b } => { - let instruction = amm_core::Instruction::NewDefinition { - token_a_amount: amount_a, - token_b_amount: amount_b, - fees: u128::from(quote_input.request.fee_bps), - deadline, - }; - risc0_zkvm::serde::to_vec(&instruction) - .map_err(|error| format!("instruction serialization failed: {error}"))? - } - QuoteBranch::Active { - max_a, - max_b, - minimum_lp, - stored_reversed, - } => { - let (stored_max_a, stored_max_b) = if stored_reversed { - (max_b, max_a) - } else { - (max_a, max_b) - }; - let instruction = amm_core::Instruction::AddLiquidity { - min_amount_liquidity: minimum_lp, - max_amount_to_add_token_a: stored_max_a, - max_amount_to_add_token_b: stored_max_b, - deadline, - }; - risc0_zkvm::serde::to_vec(&instruction) - .map_err(|error| format!("instruction serialization failed: {error}"))? - } - }; - - Ok(json!({ - "status": "ready", - "programId": input.amm_program_id, - "accountIds": account_ids.into_iter().map(account_id_hex).collect::>(), - "signingRequirements": signing_requirements, - "instruction": instruction, - "deadlineMs": deadline.to_string(), - })) -} - -fn plan_error(code: &str) -> Value { - json!({ - "status": "error", - "code": code, - "recoverable": true, - }) -} - -fn clock_timestamp(read: &AccountRead) -> Result { - decode_clock(read).map(|(_, clock)| clock.timestamp) -} diff --git a/modules/amm/ffi/src/api/position.rs b/modules/amm/ffi/src/api/position.rs index 85389b9..79de99f 100644 --- a/modules/amm/ffi/src/api/position.rs +++ b/modules/amm/ffi/src/api/position.rs @@ -7,24 +7,8 @@ use super::{ }; use crate::account::{account_id_from_hex, program_id_base58}; -#[derive(Clone)] -pub(super) enum QuoteBranch { - Missing { - amount_a: u128, - amount_b: u128, - }, - Active { - max_a: u128, - max_b: u128, - minimum_lp: u128, - stored_reversed: bool, - }, -} - pub(super) struct EvaluatedQuote { pub(super) value: Value, - pub(super) quote_hash: String, - pub(super) plan: Option, } pub(super) enum QuoteComputation { @@ -38,11 +22,6 @@ pub(super) struct QuoteFailure { pub(super) details: Value, } -pub(super) struct NewPositionPlan { - pub(super) accounts: AccountPlan, - pub(super) branch: QuoteBranch, -} - pub(super) struct AccountPlan { pub(super) rows: Vec, pub(super) sources: Vec, @@ -70,13 +49,6 @@ impl QuoteComputation { Self::Evaluated(EvaluatedQuote { value, .. }) => value, } } - - pub(super) fn quote_hash(&self) -> Option<&str> { - match self { - Self::Failed(_) => None, - Self::Evaluated(quote) => Some("e.quote_hash), - } - } } impl QuoteFailure { @@ -100,17 +72,6 @@ impl QuoteFailure { } } -impl NewPositionPlan { - pub(super) fn new(accounts: AccountPlan, branch: QuoteBranch) -> Result { - accounts.validate_ready()?; - Ok(Self { accounts, branch }) - } - - pub(super) fn requires_fresh_lp(&self) -> bool { - self.accounts.requires_fresh_lp() - } -} - impl AccountPlan { pub(super) fn validate_snapshot_ids( pair: &PairIds, @@ -151,47 +112,6 @@ impl AccountPlan { pub(super) fn take_sources(&mut self) -> Vec { std::mem::take(&mut self.sources) } - - pub(super) fn requires_fresh_lp(&self) -> bool { - self.rows - .iter() - .any(|row| row.role == "user_holding_lp" && row.account_id.is_none()) - } - - pub(super) fn contains(&self, account_id: AccountId) -> bool { - self.rows - .iter() - .any(|row| row.account_id == Some(account_id)) - } - - pub(super) fn validate_ready(&self) -> Result<(), String> { - if self.rows.iter().any(|row| { - row.account_id.is_none() && !(row.role == "user_holding_lp" && row.signer && row.init) - }) { - return Err(String::from("submittable quote has an unresolved account")); - } - Ok(()) - } - - pub(super) fn wallet_args( - &self, - fresh_lp: Option, - ) -> Result<(Vec, Vec), String> { - let mut account_ids = Vec::with_capacity(self.rows.len()); - let mut signing_requirements = Vec::with_capacity(self.rows.len()); - for row in &self.rows { - let account_id = match row.account_id { - Some(account_id) => account_id, - None if row.role == "user_holding_lp" => { - fresh_lp.ok_or_else(|| String::from("transaction plan has no LP holding"))? - } - None => return Err(String::from("transaction plan has an unresolved account")), - }; - account_ids.push(account_id); - signing_requirements.push(row.signer); - } - Ok((account_ids, signing_requirements)) - } } impl AccountPlanRow { diff --git a/modules/amm/ffi/src/api/quote.rs b/modules/amm/ffi/src/api/quote.rs index 4d335ea..70b2ad3 100644 --- a/modules/amm/ffi/src/api/quote.rs +++ b/modules/amm/ffi/src/api/quote.rs @@ -19,10 +19,7 @@ use super::{ 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, NewPositionPlan, QuoteBranch, - QuoteComputation, - }, + position::{AccountPlan, AccountPlanHoldings, EvaluatedQuote, QuoteComputation}, quote_error::{fatal_quote, issue}, QuoteRequest, }; @@ -277,19 +274,7 @@ fn compute_missing_quote( "errors": funding, "warnings": [], }); - let plan = if can_submit { - Some(NewPositionPlan::new( - account_plan, - QuoteBranch::Missing { amount_a, amount_b }, - )?) - } else { - None - }; - Ok(QuoteComputation::Evaluated(EvaluatedQuote { - value, - quote_hash, - plan, - })) + Ok(QuoteComputation::Evaluated(EvaluatedQuote { value })) } fn compute_active_quote( @@ -525,24 +510,7 @@ fn compute_active_quote( "errors": funding, "warnings": warnings, }); - let plan = if can_submit { - Some(NewPositionPlan::new( - account_plan, - QuoteBranch::Active { - max_a, - max_b, - minimum_lp, - stored_reversed, - }, - )?) - } else { - None - }; - Ok(QuoteComputation::Evaluated(EvaluatedQuote { - value, - quote_hash, - plan, - })) + Ok(QuoteComputation::Evaluated(EvaluatedQuote { value })) } fn validate_active_accounts( diff --git a/modules/amm/ffi/src/api/request.rs b/modules/amm/ffi/src/api/request.rs index 46b1343..a6e9aca 100644 --- a/modules/amm/ffi/src/api/request.rs +++ b/modules/amm/ffi/src/api/request.rs @@ -269,17 +269,3 @@ pub struct QuoteRequest { pub request: PositionRequest, pub snapshot: PairSnapshot, } - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct PlanRequest { - pub network_id: String, - pub network_fingerprint: String, - pub amm_program_id: String, - pub request: PositionRequest, - pub snapshot: PairSnapshot, - pub quote_hash: String, - pub now_ms: u64, - #[serde(default)] - pub fresh_lp: Option, -} diff --git a/modules/amm/ffi/src/api/tests.rs b/modules/amm/ffi/src/api/tests.rs index 81f53f2..0789486 100644 --- a/modules/amm/ffi/src/api/tests.rs +++ b/modules/amm/ffi/src/api/tests.rs @@ -19,15 +19,14 @@ use super::{ context::{context, token_ids}, holding::{select_holding, wallet_holdings, SelectedHolding}, pair::{is_canonical_pair, pair_ids, PairIds}, - plan::plan, position::AccountPlanHoldings, quote::{div_ceil_u256, minimum_opening_pair, quote, Q64}, swap::{swap_exact_in_plan, swap_exact_out_plan}, - ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest, QuoteRequest, + ContextRequest, PairIdsRequest, PairSnapshot, PositionRequest, QuoteRequest, SwapExactInPlanRequest, SwapExactOutPlanRequest, TokenIdsRequest, }; use crate::{ - account::{account_id_hex, account_read, decode_account, parse_base58_id, program_id_bytes}, + account::{account_id_hex, account_read, decode_account, program_id_bytes}, AccountRead, }; @@ -198,44 +197,6 @@ impl Scenario { fn quote(&self) -> Value { quote(self.quote_request()).unwrap() } - - fn plan(self, quote_hash: impl Into, fresh_lp: Option) -> Value { - plan(PlanRequest { - network_id: String::from(self.network_id), - network_fingerprint: String::from(self.network_fingerprint), - amm_program_id: amm_program_id(), - request: self.request, - snapshot: self.snapshot, - quote_hash: quote_hash.into(), - now_ms: 2_000, - fresh_lp, - }) - .unwrap() - } -} - -fn assert_preview_matches_plan( - quote_value: &Value, - plan_value: &Value, - fresh_lp: Option, -) { - let preview = quote_value["accountPreview"].as_array().unwrap(); - let account_ids = plan_value["accountIds"].as_array().unwrap(); - let signing_requirements = plan_value["signingRequirements"].as_array().unwrap(); - assert_eq!(preview.len(), account_ids.len()); - assert_eq!(preview.len(), signing_requirements.len()); - - for (order, row) in preview.iter().enumerate() { - assert_eq!(row["order"], order); - assert_eq!(row["signer"], signing_requirements[order]); - if let Some(account_id) = row["accountId"].as_str() { - let account_id = parse_base58_id(account_id, "preview account id").unwrap(); - assert_eq!(account_ids[order], account_id_hex(account_id)); - } else { - assert_eq!(row["role"], "user_holding_lp"); - assert_eq!(account_ids[order], account_id_hex(fresh_lp.unwrap())); - } - } } #[test] @@ -521,37 +482,21 @@ fn missing_pool_snapshot_defaults_remain_real_accounts() { } #[test] -fn missing_pool_quote_and_plan_use_current_account_order() { +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); - assert_eq!(quote_value["accountPreview"].as_array().unwrap().len(), 11); - let quote_hash = quote_value["quoteHash"].as_str().unwrap().to_owned(); - - let fresh_lp = AccountId::new([63; 32]); - let plan_value = scenario.plan(quote_hash, Some(default_read(fresh_lp))); - assert_eq!(plan_value["status"], "ready"); - assert_eq!(plan_value["accountIds"].as_array().unwrap().len(), 11); - assert_eq!(plan_value["accountIds"][8], account_id_hex(fresh_lp)); - assert_eq!(plan_value["signingRequirements"][6], true); - assert_eq!(plan_value["signingRequirements"][7], true); - assert_eq!(plan_value["signingRequirements"][8], true); - assert_preview_matches_plan("e_value, &plan_value, Some(fresh_lp)); -} - -#[test] -fn missing_pool_plan_rejects_fresh_lp_account_collision() { - let scenario = Scenario::devnet(); - let pool = scenario.pair.pool; - let quote_value = scenario.quote(); - let quote_hash = quote_value["quoteHash"].as_str().unwrap().to_owned(); - - let plan_value = scenario.plan(quote_hash, Some(default_read(pool))); - - assert_eq!(plan_value["status"], "error"); - assert_eq!(plan_value["code"], "wallet_submission_failed"); + // 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] @@ -572,33 +517,6 @@ fn missing_pool_quote_accepts_large_direct_raw_amounts() { assert!(quote_value.get("depositScaleBps").is_none()); } -#[test] -fn advancing_clock_does_not_stale_quote() { - let mut scenario = Scenario::testnet(); - let quote_value = scenario.quote(); - - scenario.snapshot.clock = account_read( - scenario.pair.clock, - &account( - [44; 8], - Data::try_from( - ClockAccountData { - block_id: 11, - timestamp: 1_500, - } - .to_bytes(), - ) - .unwrap(), - ), - ); - let plan_value = scenario.plan( - quote_value["quoteHash"].as_str().unwrap(), - Some(default_read(AccountId::new([63; 32]))), - ); - - assert_eq!(plan_value["status"], "ready"); -} - #[test] fn active_pool_quote_uses_ratio_and_existing_lp_holding() { let mut scenario = Scenario::testnet(); @@ -671,35 +589,27 @@ fn active_pool_quote_uses_ratio_and_existing_lp_holding() { assert_eq!(quote_value["canSubmit"], true); assert_eq!(quote_value["errors"], json!([])); - let plan_value = scenario.plan(quote_value["quoteHash"].as_str().unwrap(), None); - assert_eq!(plan_value["status"], "ready"); - assert_eq!(plan_value["accountIds"].as_array().unwrap().len(), 10); - assert_eq!(plan_value["accountIds"][7], account_id_hex(lp_holding)); - assert_eq!(plan_value["signingRequirements"][7], false); - assert_preview_matches_plan("e_value, &plan_value, None); + // 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 matching_unfunded_quote_has_no_transaction_plan() { +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); - - let plan_value = scenario.plan(quote_value["quoteHash"].as_str().unwrap(), None); - - assert_eq!(plan_value["status"], "error"); - assert_eq!(plan_value["code"], "quote_not_submittable"); - assert_eq!(plan_value["quote"], quote_value); -} - -#[test] -fn stale_hash_returns_recomputed_quote_without_plan() { - let value = Scenario::devnet().plan("sha256:deadbeef", None); - assert_eq!(value["status"], "error"); - assert_eq!(value["code"], "quote_changed"); - assert_eq!(value["quote"]["status"], "ok"); } #[test] diff --git a/modules/amm/ffi/src/ffi.rs b/modules/amm/ffi/src/ffi.rs index af6b194..e841656 100644 --- a/modules/amm/ffi/src/ffi.rs +++ b/modules/amm/ffi/src/ffi.rs @@ -8,9 +8,9 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::api::{ self, AddLiquidityPlanRequest, AddLiquidityQuoteRequest, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, - PlanRequest, PoolIdRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, - SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, - SwapExactOutQuoteRequest, SwapPairRequest, TokenHoldingsRequest, TokenIdsRequest, + PoolIdRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest, + SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, + TokenHoldingsRequest, TokenIdsRequest, }; #[derive(Serialize)] @@ -104,11 +104,6 @@ pub extern "C" fn amm_quote(request_json: *const c_char) -> *mut c_char { call::(request_json, api::quote) } -#[unsafe(no_mangle)] -pub extern "C" fn amm_plan(request_json: *const c_char) -> *mut c_char { - call::(request_json, api::plan) -} - #[unsafe(no_mangle)] pub extern "C" fn amm_swap_pair(request_json: *const c_char) -> *mut c_char { call::(request_json, api::swap_pair) diff --git a/modules/amm/ffi/src/lib.rs b/modules/amm/ffi/src/lib.rs index e86c42c..3b6f8d5 100644 --- a/modules/amm/ffi/src/lib.rs +++ b/modules/amm/ffi/src/lib.rs @@ -6,11 +6,11 @@ mod ffi; pub mod api; pub use api::{ - config_id, context, create_pool_plan, liquidity_quote, pair_ids, plan, pool_id, program_id, - quote, resolve_pool, swap_exact_in_plan, swap_exact_in_quote, swap_exact_out_plan, + config_id, context, create_pool_plan, liquidity_quote, pair_ids, pool_id, program_id, quote, + 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, PlanRequest, PoolIdRequest, PositionRequest, ProgramIdRequest, QuoteRequest, + PairSnapshot, PoolIdRequest, PositionRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, TokenIdsRequest, WalletAccount, }; diff --git a/modules/amm/src/amm_module_impl.cpp b/modules/amm/src/amm_module_impl.cpp index 96db944..0e42052 100644 --- a/modules/amm/src/amm_module_impl.cpp +++ b/modules/amm/src/amm_module_impl.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -88,31 +87,6 @@ std::string jStr(const json& obj, const char* key) { return (it != obj.end() && it->is_string()) ? it->get() : std::string(); } -// Milliseconds since the unix epoch (u64). Used for the plan's `nowMs` and the -// client deadline check — the module runs on the host, not in the zkVM, so wall -// clock is available (unlike a guest). -uint64_t nowMs() { - return static_cast( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count()); -} - -// Decimal string -> u64. False (leaving `out` unset) on empty, non-digit, or -// overflow. -bool parseU64(const std::string& s, uint64_t& out) { - if (s.empty()) return false; - uint64_t value = 0; - for (const char c : s) { - if (c < '0' || c > '9') return false; - const uint64_t d = static_cast(c - '0'); - if (value > (~static_cast(0) - d) / 10) return false; - value = value * 10 + d; - } - out = value; - return true; -} - // Coerce a swap amount arg — arriving as EITHER a JSON number (bare `1000` on // the CLI) or a decimal string ("1000", a big u128 the UI passes, or a // quote-wrapped big value on the CLI) — to its canonical decimal-string form. @@ -1214,89 +1188,3 @@ LogosMap AmmModuleImpl::quoteNewPosition(const LogosMap& request, bool wallet_op return result.ok ? result.value : publicError("backend_error"); } -LogosMap AmmModuleImpl::submitNewPosition(const LogosMap& request, - const std::string& quote_hash, - bool wallet_open, - const std::string& fresh_lp_id) { - if (m_requestPending) return publicError("submit_in_progress"); - if (!wallet_open) return publicError("wallet_unavailable"); - m_requestPending = true; - struct Guard { - bool* flag; - ~Guard() { *flag = false; } - } guard{&m_requestPending}; - - const Network net = network(); - json error; - const json input = buildQuoteInput(request, net, wallet_open, /*fresh=*/true, &error); - if (!error.is_null()) return error; - - const FfiResult quoteResult = call(amm_quote, input); - if (!quoteResult.ok) return publicError("backend_error"); - const json quote = quoteResult.value; - if (jStr(quote, "quoteHash") != quote_hash) { - json result = publicError("quote_changed"); - result["quote"] = quote; - return result; - } - if (!quote.value("canSubmit", false)) { - json result = publicError("quote_not_submittable"); - result["quote"] = quote; - return result; - } - - // Fresh LP holding: the app owns wallet-keyset mutation. If the quote needs - // one and the caller hasn't supplied it, ask for it (no submit) so the - // backend can create it through its own wallet provider and call again. - json freshLp; // null - if (quote.value("requiresFreshLp", false)) { - if (fresh_lp_id.empty()) { - return json{ - {"status", "requires_fresh_lp"}, - {"quote", quote}, - }; - } - freshLp = readPublicAccount(fresh_lp_id); - } - - json planInput = input; - planInput["quoteHash"] = quote_hash; - planInput["nowMs"] = nowMs(); - if (!freshLp.is_null()) planInput["freshLp"] = freshLp; - - const FfiResult planResult = call(amm_plan, planInput); - if (!planResult.ok) return publicError("backend_error"); - const json plan = planResult.value; - if (jStr(plan, "status") != "ready") { - const std::string code = jStr(plan, "code"); - return publicError(code.empty() ? "wallet_submission_failed" : code); - } - - uint64_t deadline = 0; - if (!parseU64(jStr(plan, "deadlineMs"), deadline) || nowMs() >= deadline) - return publicError("transaction_deadline_expired"); - - const std::vector accounts = jsonStrVec(plan.value("accountIds", json::array())); - const std::vector signers = jsonBoolVec(plan.value("signingRequirements", json::array())); - const std::vector instruction = jsonWordsToLeBytes(plan.value("instruction", json::array())); - const std::string program_id = jStr(plan, "programId"); - - const std::string reply = modules().logos_execution_zone.send_generic_public_transaction( - accounts, signers, instruction, program_id); - const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false); - if (!obj.is_object() || !obj.value("success", false)) - return publicError("wallet_submission_failed"); - - // Native tx hash (64-char hex) -> base58 transaction id via the wallet - // module (avoids linking libbase58 just for this encode). - const std::string tx_hash = jStr(obj, "tx_hash"); - const std::string transaction_id = - modules().logos_execution_zone.account_id_to_base58(tx_hash); - if (transaction_id.empty()) return publicError("wallet_submission_failed"); - - return { - {"status", "submitted"}, - {"transactionId", transaction_id}, - {"deadlineMs", plan.value("deadlineMs", json())}, - }; -} diff --git a/modules/amm/src/amm_module_impl.h b/modules/amm/src/amm_module_impl.h index fef6f13..e4a9be9 100644 --- a/modules/amm/src/amm_module_impl.h +++ b/modules/amm/src/amm_module_impl.h @@ -189,18 +189,6 @@ public: /// requiresFreshLp, amounts, warnings). Read-only — no submission. LogosMap quoteNewPosition(const LogosMap& request, bool wallet_open); - /// Submits an add-liquidity transaction. Re-quotes and validates `quote_hash` - /// against the current quote. If the quote requires a fresh LP holding and - /// `fresh_lp_id` 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. Otherwise builds - /// the plan (injecting the fresh LP account when given) and submits, then - /// returns the new-position submitted/error map. - LogosMap submitNewPosition(const LogosMap& request, - const std::string& quote_hash, - bool wallet_open, - const std::string& fresh_lp_id); - private: // Off-chain "network" context, derived from the process env (the same // sources the app backend used): AMM deployment id from AMM_PROGRAM_BIN, @@ -248,20 +236,15 @@ private: nlohmann::json walletAccountReads(bool wallet_open, bool refresh); // Builds the { networkId, networkFingerprint, ammProgramId, request, - // snapshot } input shared by quoteNewPosition / submitNewPosition. On a - // recoverable precondition failure, sets *error to a new-position error - // map and returns a null json. + // 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); - // Guards against a re-entrant in-flight request (e.g. a double-submit) on the - // shared module instance. Released per call, so the app's fresh-LP resubmit - // still proceeds. - bool m_requestPending = false; - // 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.