mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
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.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<SequencerAccount>,
|
||||
#[serde(default)]
|
||||
error: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SequencerAccount {
|
||||
program_owner: ProgramId,
|
||||
balance: u128,
|
||||
data: Vec<u8>,
|
||||
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<AccountSnapshot, SequencerAccountError> {
|
||||
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),
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -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<u128>,
|
||||
@@ -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<T>(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(
|
||||
|
||||
@@ -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<TransactionError> for WireError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SequencerAccountError> 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<Value, WireError> {
|
||||
.map(u128::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
})),
|
||||
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::<String>(),
|
||||
})
|
||||
}
|
||||
|
||||
fn amm_context_json(context: &AmmContext) -> Value {
|
||||
json!({
|
||||
"ammProgramId": program_id_words(context.amm_program_id),
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user