From d8e2ae3d8592c46a4e12eee6e22e38cca8464c43 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Thu, 23 Jul 2026 02:30:45 -0300 Subject: [PATCH] refactor(amm-client)!: keep transport parsing in hosts Remove the generic sequencer-response adapter and reuse the transaction order mapper. BREAKING CHANGE: account_snapshot_from_sequencer_response and its quote JSON operation are removed. Hosts must normalize RPC responses into canonical account snapshots before calling amm_client. --- programs/amm/client/README.md | 7 +- programs/amm/client/docs/wire-api.md | 14 +-- programs/amm/client/include/amm_client.h | 4 +- programs/amm/client/src/lib.rs | 2 - programs/amm/client/src/sequencer.rs | 99 ------------------- programs/amm/client/src/transaction.rs | 69 ++++--------- programs/amm/client/src/wire.rs | 46 +-------- .../client/tests/consumer_adapter_contract.rs | 47 +-------- 8 files changed, 36 insertions(+), 252 deletions(-) delete mode 100644 programs/amm/client/src/sequencer.rs diff --git a/programs/amm/client/README.md b/programs/amm/client/README.md index cda8d08..183b451 100644 --- a/programs/amm/client/README.md +++ b/programs/amm/client/README.md @@ -17,8 +17,6 @@ adapter responsibilities. - `intent` prepares canonical opening amounts and caller/stored order mappings with integer-only protocol math. It also converts exact human price ratios and token decimals into stored-order Q64.64 prices without floating point. -- `sequencer` decodes original `getAccount` response text directly into lossless - `AccountSnapshot` values, preserving integer fields above `2^53`. - `slippage` converts validated quotes into integer-only instruction guards. Minimum guards round down, maximum guards round up, and checked overflow returns a typed error. - `plan` covers all ten guest instructions and returns the canonical instruction plus ordered @@ -80,5 +78,6 @@ Account data uses hexadecimal, and encoded instruction words remain JSON `u32` n JavaScript `Number` conversion is required for chain amounts or deadlines. Plan JSON also includes typed `instructionArgs`, derived directly from the same `amm_core::Instruction` encoded in `instructionWords`. Only `amm_client_plan` accepts the five -snapshot-bound `prepare_*_transaction` operations. The quote entrypoint also exposes -`account_snapshot_from_sequencer_response` and `human_price_ratio_to_q64_64` host adapters. +snapshot-bound `prepare_*_transaction` operations. The quote entrypoint exposes +`human_price_ratio_to_q64_64`; hosts normalize RPC responses into canonical snapshots before +calling the client. diff --git a/programs/amm/client/docs/wire-api.md b/programs/amm/client/docs/wire-api.md index 399bd93..e54090c 100644 --- a/programs/amm/client/docs/wire-api.md +++ b/programs/amm/client/docs/wire-api.md @@ -160,7 +160,6 @@ shown in this table and the sections below. | `operation` | Additional fields | |---|---| | `protocol_constants` | none; returns decimal-string `minimumLiquidity`, `feeBpsDenominator`, `slippageBpsDenominator`, and `supportedFeeTiers` | -| `account_snapshot_from_sequencer_response` | canonical base58 `accountId`, original `getAccount` response text in `response` | | `human_price_ratio_to_q64_64` | caller-ordered token IDs, `firstAmount`, `secondAmount`, and decimal-string `firstTokenDecimals`/`secondTokenDecimals` | | `derive_config_id` | `ammProgramId` | | `inspect_config` | `ammProgramId`, raw `config` snapshot | @@ -209,11 +208,9 @@ snapshot to the returned quote. It is not execution-price impact. ## Host adapters -`account_snapshot_from_sequencer_response` accepts the original JSON-RPC response as a JSON string, -not a host-parsed object. It decodes sequencer numeric literals directly as Rust `u128` values and -returns the standard snapshot fields: `id`, `programOwner`, `balance`, `nonce`, and `data`. This -preserves balances and nonces above `2^53`. Do not route the response through a JavaScript or QML -numeric value first. +Hosts normalize sequencer or wallet responses into the canonical snapshot fields before calling the +client: base58 `id`, eight-word `programOwner`, decimal-string `balance` and `nonce`, and +hexadecimal `data`. Keep raw RPC parsing and wallet-specific representations outside AMM. `human_price_ratio_to_q64_64` declares that `firstAmount` human units of the first token equal `secondAmount` human units of the second token. Amounts are unsigned decimal text and may contain @@ -337,9 +334,8 @@ Every failure uses `{ "code": "...", "message": "..." }`. `code` is the stable machine-readable contract; `message` is diagnostic text. JSON adapter failures return `invalid_request` or `unsupported_schema`. The C envelope additionally returns `null_request`, `invalid_utf8`, `invalid_json`, `response_serialization_failed`, or `response_contains_nul` for -boundary failures. Sequencer adapters return `invalid_sequencer_response`, -`sequencer_account_error`, `sequencer_account_missing`, or `account_data_too_large`. Human-price -conversion uses the stable `IntentError` codes documented by the Rust API. +boundary failures. Human-price conversion uses the stable `IntentError` codes documented by the +Rust API. No request performs network I/O or checks an ImageID, release version, compatibility manifest, or program allowlist. Deployment configuration is expected to select the corresponding AMM build. diff --git a/programs/amm/client/include/amm_client.h b/programs/amm/client/include/amm_client.h index c09a9f4..e0c8a56 100644 --- a/programs/amm/client/include/amm_client.h +++ b/programs/amm/client/include/amm_client.h @@ -19,8 +19,8 @@ char *amm_client_plan(const char *request_json); * Accepts a tagged UTF-8 JSON request and returns an owned UTF-8 JSON envelope. * Supported operation tags include protocol constants; config and pair discovery; * pair inspection; caller-order opening preparation; economic quote/preparation - * operations; reserve synchronization; oracle-price initialization; raw sequencer - * account normalization; and human-price Q64.64 conversion. + * operations; reserve synchronization; oracle-price initialization; and human-price Q64.64 + * conversion. Hosts normalize RPC and wallet responses before calling this API. * Snapshot-bound prepare_*_transaction operations belong to amm_client_plan. * See docs/wire-api.md for fields. * Release the result with amm_client_free. diff --git a/programs/amm/client/src/lib.rs b/programs/amm/client/src/lib.rs index 49ec411..0aad0f7 100644 --- a/programs/amm/client/src/lib.rs +++ b/programs/amm/client/src/lib.rs @@ -6,7 +6,6 @@ mod ffi; pub mod intent; pub mod plan; pub mod quote; -pub mod sequencer; pub mod slippage; pub mod transaction; pub mod wire; @@ -36,7 +35,6 @@ pub use plan::{ SyncReservesPlanInput, TransactionPlan, UpdateConfigPlanInput, }; pub use quote::AccountSnapshot; -pub use sequencer::{account_snapshot_from_sequencer_response, SequencerAccountError}; pub use slippage::{ maximum_guard_amount, minimum_guard_amount, prepare_add_liquidity, prepare_create_pool, prepare_remove_liquidity, prepare_swap_exact_input, prepare_swap_exact_output, diff --git a/programs/amm/client/src/sequencer.rs b/programs/amm/client/src/sequencer.rs deleted file mode 100644 index 511ef5f..0000000 --- a/programs/amm/client/src/sequencer.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Lossless adapters for raw sequencer account responses. - -use std::{error::Error, fmt}; - -use nssa_core::{ - account::{Account, AccountId, Data, Nonce}, - program::ProgramId, -}; -use serde::Deserialize; -use serde_json::Value; - -use crate::quote::AccountSnapshot; - -/// Failure while decoding a sequencer account response. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub enum SequencerAccountError { - /// Response was not valid sequencer account JSON. - InvalidResponse, - /// Sequencer returned an RPC error. - RpcError, - /// Sequencer returned no account result. - MissingAccount, - /// Account data exceeds the NSSA account-data limit. - AccountDataTooLarge, -} - -impl SequencerAccountError { - /// Stable machine-readable error code. - #[must_use] - pub const fn code(self) -> &'static str { - match self { - Self::InvalidResponse => "invalid_sequencer_response", - Self::RpcError => "sequencer_account_error", - Self::MissingAccount => "sequencer_account_missing", - Self::AccountDataTooLarge => "account_data_too_large", - } - } -} - -impl fmt::Display for SequencerAccountError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::InvalidResponse => "sequencer account response is invalid", - Self::RpcError => "sequencer returned an account error", - Self::MissingAccount => "sequencer returned no account", - Self::AccountDataTooLarge => "sequencer account data is too large", - }) - } -} - -impl Error for SequencerAccountError {} - -#[derive(Deserialize)] -struct SequencerEnvelope { - #[serde(default)] - result: Option, - #[serde(default)] - error: Option, -} - -#[derive(Deserialize)] -struct SequencerAccount { - program_owner: ProgramId, - balance: u128, - data: Vec, - nonce: u128, -} - -/// Decodes a raw `getAccount` JSON-RPC response without routing integer fields through a -/// JavaScript numeric value. -/// -/// `response` must be the original response text. Passing a JSON value already parsed by a host -/// with IEEE-754 numbers can lose balances or nonces above `2^53` before this function sees them. -pub fn account_snapshot_from_sequencer_response( - account_id: AccountId, - response: &str, -) -> Result { - let envelope: SequencerEnvelope = - serde_json::from_str(response).map_err(|_| SequencerAccountError::InvalidResponse)?; - if envelope.error.is_some() { - return Err(SequencerAccountError::RpcError); - } - let account = envelope - .result - .ok_or(SequencerAccountError::MissingAccount)?; - let data = - Data::try_from(account.data).map_err(|_| SequencerAccountError::AccountDataTooLarge)?; - - Ok(AccountSnapshot::new( - account_id, - Account { - program_owner: account.program_owner, - balance: account.balance, - data, - nonce: Nonce(account.nonce), - }, - )) -} diff --git a/programs/amm/client/src/transaction.rs b/programs/amm/client/src/transaction.rs index 94d248b..6ccb04c 100644 --- a/programs/amm/client/src/transaction.rs +++ b/programs/amm/client/src/transaction.rs @@ -464,21 +464,9 @@ pub fn prepare_create_pool_transaction( } else { PairOrder::Reversed }; - let (stored_a_definition, stored_b_definition, stored_a_holding, stored_b_holding) = match order - { - PairOrder::Stored => ( - &first_definition, - &second_definition, - &first_holding, - &second_holding, - ), - PairOrder::Reversed => ( - &second_definition, - &first_definition, - &second_holding, - &first_holding, - ), - }; + let (stored_a_definition, stored_b_definition) = + order_pair(order, first_definition, second_definition); + let (stored_a_holding, stored_b_holding) = order_pair(order, &first_holding, &second_holding); let (stored_a_amount, stored_b_amount) = order.amounts_to_stored(input.first_amount, input.second_amount); let prepared = prepare_create_pool( @@ -558,7 +546,11 @@ pub fn prepare_add_liquidity_transaction( let snapshot = active.pool(); let order = active.caller_order(); validate_expected_fee(snapshot, input.expected_fee_bps)?; - let (first_definition, second_definition) = caller_definitions(snapshot, order); + let (first_definition, second_definition) = order_pair( + order, + snapshot.token_a_definition(), + snapshot.token_b_definition(), + ); let first_holding = ValidatedFungibleHolding::new(&context, input.first_token_holding, first_definition)?; let second_holding = @@ -586,8 +578,7 @@ pub fn prepare_add_liquidity_transaction( snapshot.liquidity_definition().account_id(), "liquidity holding", )?; - let (stored_holding_a, stored_holding_b) = - stored_holdings(order, &first_holding, &second_holding); + let (stored_holding_a, stored_holding_b) = order_pair(order, &first_holding, &second_holding); let pool = PoolContext::new(&context, snapshot.pool_id(), snapshot.pool())?; let plan = plan_add_liquidity(AddLiquidityPlanInput { context: &context, @@ -648,7 +639,11 @@ pub fn prepare_remove_liquidity_transaction( let snapshot = active.pool(); let order = active.caller_order(); validate_expected_fee(snapshot, input.expected_fee_bps)?; - let (first_definition, second_definition) = caller_definitions(snapshot, order); + let (first_definition, second_definition) = order_pair( + order, + snapshot.token_a_definition(), + snapshot.token_b_definition(), + ); let first_fresh = validate_holding_destination( &context, input.first_token_holding, @@ -1206,16 +1201,6 @@ fn pair_sources( sources } -fn caller_definitions( - snapshot: &ValidatedPoolSnapshot, - order: PairOrder, -) -> (&ValidatedFungibleDefinition, &ValidatedFungibleDefinition) { - match order { - PairOrder::Stored => (snapshot.token_a_definition(), snapshot.token_b_definition()), - PairOrder::Reversed => (snapshot.token_b_definition(), snapshot.token_a_definition()), - } -} - fn validate_expected_fee( snapshot: &ValidatedPoolSnapshot, expected_fee_bps: Option, @@ -1229,17 +1214,6 @@ fn validate_expected_fee( Ok(()) } -fn stored_holdings<'a>( - order: PairOrder, - first: &'a ValidatedFungibleHolding, - second: &'a ValidatedFungibleHolding, -) -> (&'a ValidatedFungibleHolding, &'a ValidatedFungibleHolding) { - match order { - PairOrder::Stored => (first, second), - PairOrder::Reversed => (second, first), - } -} - fn order_pair(order: PairOrder, first: T, second: T) -> (T, T) { match order { PairOrder::Stored => (first, second), @@ -1252,14 +1226,13 @@ fn swap_definitions( input_definition_id: AccountId, output_definition_id: AccountId, ) -> Result<(&ValidatedFungibleDefinition, &ValidatedFungibleDefinition), ClientError> { - match amm_program::quote::pair_order( - snapshot.pool(), - input_definition_id, - output_definition_id, - )? { - PairOrder::Stored => Ok((snapshot.token_a_definition(), snapshot.token_b_definition())), - PairOrder::Reversed => Ok((snapshot.token_b_definition(), snapshot.token_a_definition())), - } + let order = + amm_program::quote::pair_order(snapshot.pool(), input_definition_id, output_definition_id)?; + Ok(order_pair( + order, + snapshot.token_a_definition(), + snapshot.token_b_definition(), + )) } fn validate_holding_destination( diff --git a/programs/amm/client/src/wire.rs b/programs/amm/client/src/wire.rs index be6e525..5b55a3e 100644 --- a/programs/amm/client/src/wire.rs +++ b/programs/amm/client/src/wire.rs @@ -18,7 +18,6 @@ use serde::Deserialize; use serde_json::{json, Value}; use crate::{ - account_snapshot_from_sequencer_response, discovery::{self, CanonicalPair, PairReadManifest}, human_price_ratio_to_q64_64, plan_add_liquidity, plan_create_oracle_price_account, plan_create_pool, plan_create_price_observations, plan_initialize, plan_remove_liquidity, @@ -31,10 +30,10 @@ use crate::{ CreatePoolPlanInput, CreatePriceObservationsPlanInput, InitializePlanInput, IntentError, OpeningLiquidityIntent, PoolContext, PreparedAddLiquidity, PreparedCallerOpeningPair, PreparedCreatePool, PreparedOpeningPair, PreparedRemoveLiquidity, PreparedSwapExactInput, - PreparedSwapExactOutput, PreparedTransaction, RemoveLiquidityPlanInput, SequencerAccountError, - SlippageTolerance, SwapExactInputPlanInput, SwapExactOutputPlanInput, SyncReservesPlanInput, - TransactionError, TransactionOperation, TransactionPlan, UpdateConfigPlanInput, - WalletPrerequisites, SLIPPAGE_BPS_DENOMINATOR, + PreparedSwapExactOutput, PreparedTransaction, RemoveLiquidityPlanInput, SlippageTolerance, + SwapExactInputPlanInput, SwapExactOutputPlanInput, SyncReservesPlanInput, TransactionError, + TransactionOperation, TransactionPlan, UpdateConfigPlanInput, WalletPrerequisites, + SLIPPAGE_BPS_DENOMINATOR, }; /// Version of the reusable AMM client JSON contract. @@ -91,12 +90,6 @@ impl From for WireError { } } -impl From for WireError { - fn from(error: SequencerAccountError) -> Self { - Self::new(error.code(), error.to_string()) - } -} - #[derive(Clone, Copy, Deserialize)] #[serde(transparent)] struct ProgramIdInput(ProgramId); @@ -409,11 +402,6 @@ impl PoolInput { #[serde(tag = "operation", rename_all = "snake_case")] enum QuoteRequest { ProtocolConstants, - AccountSnapshotFromSequencerResponse { - #[serde(rename = "accountId")] - account_id: String, - response: String, - }, HumanPriceRatioToQ64_64 { #[serde(rename = "firstTokenDefinitionId")] first_token_definition_id: String, @@ -1308,16 +1296,6 @@ pub fn quote_json(value: Value) -> Result { .map(u128::to_string) .collect::>(), })), - QuoteRequest::AccountSnapshotFromSequencerResponse { - account_id: requested_account_id, - response, - } => { - let snapshot = account_snapshot_from_sequencer_response( - account_id(&requested_account_id, "accountId")?, - &response, - )?; - Ok(account_snapshot_json(&snapshot)) - } QuoteRequest::HumanPriceRatioToQ64_64 { first_token_definition_id, second_token_definition_id, @@ -1989,22 +1967,6 @@ fn pool_update_json(pool: PoolUpdate) -> Value { }) } -fn account_snapshot_json(snapshot: &AccountSnapshot) -> Value { - let account = snapshot.account(); - json!({ - "id": snapshot.account_id().to_string(), - "programOwner": program_id_words(account.program_owner), - "balance": account.balance.to_string(), - "nonce": account.nonce.0.to_string(), - "data": account - .data - .as_ref() - .iter() - .map(|byte| format!("{byte:02x}")) - .collect::(), - }) -} - fn amm_context_json(context: &AmmContext) -> Value { json!({ "ammProgramId": program_id_words(context.amm_program_id), diff --git a/programs/amm/client/tests/consumer_adapter_contract.rs b/programs/amm/client/tests/consumer_adapter_contract.rs index 8dd5a8c..63b957c 100644 --- a/programs/amm/client/tests/consumer_adapter_contract.rs +++ b/programs/amm/client/tests/consumer_adapter_contract.rs @@ -1,53 +1,8 @@ -mod common; - -use amm_client::{ - account_snapshot_from_sequencer_response, human_price_ratio_to_q64_64, wire::quote_json, - Q64_64_ONE, -}; +use amm_client::{human_price_ratio_to_q64_64, wire::quote_json, Q64_64_ONE}; use amm_core::canonical_token_pair; -use common::program_id_words; use nssa_core::account::AccountId; use serde_json::json; -const RAW_SEQUENCER_RESPONSE: &str = r#"{ - "jsonrpc":"2.0", - "id":1, - "result":{ - "program_owner":[1,2,3,4,5,6,7,8], - "balance":340282366920938463463374607431768211455, - "data":[0,255], - "nonce":9007199254740993 - } -}"#; - -#[test] -fn raw_sequencer_response_becomes_lossless_snapshot() { - let account_id = AccountId::new([7; 32]); - let snapshot = account_snapshot_from_sequencer_response(account_id, RAW_SEQUENCER_RESPONSE) - .expect("raw sequencer account must decode"); - - assert_eq!(snapshot.account_id(), account_id); - assert_eq!(snapshot.account().program_owner, [1, 2, 3, 4, 5, 6, 7, 8]); - assert_eq!(snapshot.account().balance, u128::MAX); - assert_eq!(snapshot.account().nonce.0, 9_007_199_254_740_993); - assert_eq!(snapshot.account().data.as_ref(), &[0, 255]); - - let wire = quote_json(json!({ - "operation": "account_snapshot_from_sequencer_response", - "accountId": account_id.to_string(), - "response": RAW_SEQUENCER_RESPONSE, - })) - .expect("wire adapter must decode raw response text"); - assert_eq!(wire["id"], account_id.to_string()); - assert_eq!( - wire["programOwner"], - json!(program_id_words([1, 2, 3, 4, 5, 6, 7, 8])) - ); - assert_eq!(wire["balance"], u128::MAX.to_string()); - assert_eq!(wire["nonce"], "9007199254740993"); - assert_eq!(wire["data"], "00ff"); -} - #[test] fn human_price_conversion_handles_large_values_order_and_decimals() { let first_id = AccountId::new([1; 32]);