mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
refactor(amm): remove the dead submitNewPosition path
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 <chrono> 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.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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::<Vec<_>>(),
|
||||
"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<u64, String> {
|
||||
decode_clock(read).map(|(_, clock)| clock.timestamp)
|
||||
}
|
||||
@@ -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<NewPositionPlan>,
|
||||
}
|
||||
|
||||
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<AccountPlanRow>,
|
||||
pub(super) sources: Vec<SourceCommitment>,
|
||||
@@ -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<Self, String> {
|
||||
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<SourceCommitment> {
|
||||
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<AccountId>,
|
||||
) -> Result<(Vec<AccountId>, Vec<bool>), 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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<AccountRead>,
|
||||
}
|
||||
|
||||
@@ -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<String>, fresh_lp: Option<AccountRead>) -> 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<AccountId>,
|
||||
) {
|
||||
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]
|
||||
|
||||
@@ -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::<QuoteRequest>(request_json, api::quote)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_plan(request_json: *const c_char) -> *mut c_char {
|
||||
call::<PlanRequest>(request_json, api::plan)
|
||||
}
|
||||
|
||||
#[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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
@@ -88,31 +87,6 @@ std::string jStr(const json& obj, const char* key) {
|
||||
return (it != obj.end() && it->is_string()) ? it->get<std::string>() : 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<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
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<uint64_t>(c - '0');
|
||||
if (value > (~static_cast<uint64_t>(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<std::string> accounts = jsonStrVec(plan.value("accountIds", json::array()));
|
||||
const std::vector<bool> signers = jsonBoolVec(plan.value("signingRequirements", json::array()));
|
||||
const std::vector<uint8_t> 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())},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user