fix(amm-client)!: align shared quote wire contract

Use canonical ProgramId word arrays, include pool spot movement in standalone swap quotes, and keep quote commitments stable across deadline refreshes.

BREAKING CHANGE: JSON ProgramId values now use eight u32 words. Convert hex or byte representations in host adapters.
This commit is contained in:
Ricardo Guilherme Schmidt
2026-08-10 11:22:44 -03:00
parent ae0bb310b0
commit 43ed4de64f
12 changed files with 411 additions and 130 deletions
+6 -5
View File
@@ -75,9 +75,10 @@ Every call returns an owned JSON envelope. Release it exactly once with `amm_cli
[`docs/wire-api.md`](docs/wire-api.md) for the complete transport contract.
Raw `u128` and `u64` values cross JSON as decimal strings. Account IDs use canonical base58.
Program IDs use 64-character lowercase hexadecimal strings. Account data uses hexadecimal, and
encoded instruction words remain JSON `u32` numbers. No 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
Program IDs use exactly eight JSON `u32` words; hexadecimal and byte layouts are host-adapter-only.
Account data uses hexadecimal, and encoded instruction words remain JSON `u32` numbers. No
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.
+17 -11
View File
@@ -14,9 +14,10 @@ Requests may include `"schema":"amm-client.v1"`. Schema-less requests remain acc
compatibility. Every successful wire value and every C envelope identifies the response schema.
All `u128` amounts, reserves, supplies, fees, nonces, and balances are unsigned decimal strings.
All `u64` windows and deadlines are also decimal strings. Program IDs are exactly 64 lowercase
hexadecimal characters: the 32 bytes formed by concatenating the eight `u32` words in
little-endian byte order. Signed ticks are decimal strings. Account IDs use canonical base58.
All `u64` windows and deadlines are also decimal strings. Program IDs are JSON arrays containing
exactly eight `u32` words in their canonical order. Hexadecimal and byte layouts are host-adapter
concerns and are neither accepted nor emitted by this shared wire API. Signed ticks are decimal
strings. Account IDs use canonical base58.
Account `data` is an even-length hexadecimal string.
## Shared inputs
@@ -25,9 +26,9 @@ Plan context:
```json
{
"ammProgramId": "0000000000000000000000000000000000000000000000000000000000000000",
"tokenProgramId": "0000000000000000000000000000000000000000000000000000000000000000",
"twapOracleProgramId": "0000000000000000000000000000000000000000000000000000000000000000",
"ammProgramId": [0, 0, 0, 0, 0, 0, 0, 0],
"tokenProgramId": [0, 0, 0, 0, 0, 0, 0, 0],
"twapOracleProgramId": [0, 0, 0, 0, 0, 0, 0, 0],
"authority": "base58-account-id"
}
```
@@ -54,7 +55,7 @@ Fetched account snapshot used by quotes:
```json
{
"id": "base58-account-id",
"programOwner": "0000000000000000000000000000000000000000000000000000000000000000",
"programOwner": [0, 0, 0, 0, 0, 0, 0, 0],
"balance": "0",
"nonce": "0",
"data": "00ff"
@@ -65,7 +66,7 @@ Existing-pool quote operations include these top-level state fields:
```json
{
"ammProgramId": "0000000000000000000000000000000000000000000000000000000000000000",
"ammProgramId": [0, 0, 0, 0, 0, 0, 0, 0],
"config": { "...": "account snapshot" },
"snapshot": {
"pool": { "...": "account snapshot" },
@@ -129,7 +130,7 @@ A successful plan value contains the following fields (`instructionWords` is abb
"maxAmountToAddTokenB": "100",
"deadline": "1900000000000"
},
"programId": "0000000000000000000000000000000000000000000000000000000000000000",
"programId": [0, 0, 0, 0, 0, 0, 0, 0],
"accounts": [
{
"id": "base58-account-id",
@@ -194,7 +195,8 @@ Quote values use these result shapes:
- pool creation: `pool`, `lockedLiquidity`, `userLiquidity`;
- add liquidity: `actualAmountA`, `actualAmountB`, `liquidityToMint`, `pool`;
- remove liquidity: `withdrawAmountA`, `withdrawAmountB`, `liquidityToBurn`, `pool`;
- swaps: `direction`, `amountIn`, `effectiveAmountIn`, `feeAmount`, `amountOut`, `pool`;
- swaps: `direction`, `amountIn`, `effectiveAmountIn`, `feeAmount`, `amountOut`, `pool`, and
decimal-string `poolSpotChangeBps`;
- reserve sync: `donatedAmountA`, `donatedAmountB`, `pool`;
- oracle price: `baseAsset`, `quoteAsset`, `initialPriceQ64_64`, `windowDuration`; and
- pair order: `order` (`stored` or `reversed`).
@@ -202,6 +204,9 @@ Quote values use these result shapes:
A `pool` result contains decimal-string `liquidityPoolSupply`, `reserveA`, `reserveB`, and
`spotPriceQ64_64` fields.
`poolSpotChangeBps` is the exact directional movement of the pool's spot price from the pre-swap
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,
@@ -295,7 +300,8 @@ Successful task output contains:
`poolSpotChangeBps` is `null` for non-swap tasks. Add-liquidity funding requirements use the
caller caps. Exact-output swap funding uses the plan's slippage-adjusted `maxAmountIn`. Hosts
should refresh snapshots, prepare again, compare `quoteCommitment`, and submit only the refreshed
plan.
plan. A deadline-only refresh keeps the same `quoteCommitment`; the refreshed plan still carries
the deadline to submit.
## Prepared instruction arguments
+3 -2
View File
@@ -29,8 +29,9 @@ char *amm_client_quote(const char *request_json);
/*
* Raw u128, u64, and signed tick values are decimal JSON strings. Program IDs
* are 64-character lowercase hexadecimal strings. Instruction words are JSON
* u32 arrays. Account IDs use canonical base58 and account data is hexadecimal.
* are JSON arrays of eight u32 words. Hexadecimal and byte layouts are host-adapter-only.
* Instruction words are JSON u32 arrays. Account IDs use canonical base58 and account data is
* hexadecimal.
* Requests may carry schema "amm-client.v1";
* schema-less legacy requests remain accepted. Responses use
* {"schema":"amm-client.v1","ok":true,"value":...} or the same envelope
+136 -8
View File
@@ -140,7 +140,10 @@ impl WalletPrerequisites {
}
}
/// SHA-256 commitment to the typed request, exact plan, and role-tagged account snapshots.
/// SHA-256 commitment to quote-relevant intent, guards, account selection, and snapshots.
///
/// The transaction deadline remains part of the returned plan, but is deliberately not committed:
/// hosts may refresh their deadline window without invalidating an otherwise unchanged quote.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct QuoteCommitment([u8; 32]);
@@ -443,7 +446,7 @@ pub fn prepare_create_pool_transaction(
account: "AMM pool",
expected: "uninitialized pool lifecycle",
}
.into())
.into());
}
};
let first_definition = missing.first_token_definition();
@@ -885,8 +888,7 @@ impl<Q> PreparedTransaction<Q> {
unique_account_ids.push(account_id);
}
let instruction_words = plan
.instruction_data()
plan.instruction_data()
.map_err(|_| TransactionError::InstructionEncoding)?;
let plan_accounts = plan
.accounts()
@@ -904,11 +906,10 @@ impl<Q> PreparedTransaction<Q> {
operation,
intent,
program_id: plan.program_id(),
instruction_words,
guards: CommitmentGuards::from(plan.instruction()),
plan_accounts,
sources,
caller_amounts,
deadline,
};
let words = risc0_zkvm::serde::to_vec(&payload)
.map_err(|_| TransactionError::CommitmentEncoding)?;
@@ -946,11 +947,138 @@ struct PreparedCommitmentPayload {
operation: TransactionOperation,
intent: TransactionIntent,
program_id: ProgramId,
instruction_words: Vec<u32>,
guards: CommitmentGuards,
plan_accounts: Vec<PlanAccountCommitment>,
sources: Vec<SnapshotCommitment>,
caller_amounts: CallerAmounts,
deadline: u64,
}
/// Instruction fields generated from a quote that affect its economic or account-selection
/// meaning. Deadlines are intentionally excluded because they are host transaction policy.
#[derive(Serialize)]
enum CommitmentGuards {
Initialize {
token_program_id: ProgramId,
twap_oracle_program_id: ProgramId,
authority: AccountId,
},
UpdateConfig {
token_program_id: Option<ProgramId>,
twap_oracle_program_id: Option<ProgramId>,
new_authority: Option<AccountId>,
},
CreatePriceObservations {
window_duration: u64,
},
CreateOraclePriceAccount {
window_duration: u64,
},
CreatePool {
token_a_amount: u128,
token_b_amount: u128,
fees: u128,
},
AddLiquidity {
min_amount_liquidity: u128,
max_amount_to_add_token_a: u128,
max_amount_to_add_token_b: u128,
},
RemoveLiquidity {
remove_liquidity_amount: u128,
min_amount_to_remove_token_a: u128,
min_amount_to_remove_token_b: u128,
},
SwapExactInput {
swap_amount_in: u128,
min_amount_out: u128,
},
SwapExactOutput {
exact_amount_out: u128,
max_amount_in: u128,
},
SyncReserves,
}
impl From<&amm_core::Instruction> for CommitmentGuards {
fn from(instruction: &amm_core::Instruction) -> Self {
match instruction {
amm_core::Instruction::Initialize {
token_program_id,
twap_oracle_program_id,
authority,
} => Self::Initialize {
token_program_id: *token_program_id,
twap_oracle_program_id: *twap_oracle_program_id,
authority: *authority,
},
amm_core::Instruction::UpdateConfig {
token_program_id,
twap_oracle_program_id,
new_authority,
} => Self::UpdateConfig {
token_program_id: *token_program_id,
twap_oracle_program_id: *twap_oracle_program_id,
new_authority: *new_authority,
},
amm_core::Instruction::CreatePriceObservations { window_duration } => {
Self::CreatePriceObservations {
window_duration: *window_duration,
}
}
amm_core::Instruction::CreateOraclePriceAccount { window_duration } => {
Self::CreateOraclePriceAccount {
window_duration: *window_duration,
}
}
amm_core::Instruction::NewDefinition {
token_a_amount,
token_b_amount,
fees,
..
} => Self::CreatePool {
token_a_amount: *token_a_amount,
token_b_amount: *token_b_amount,
fees: *fees,
},
amm_core::Instruction::AddLiquidity {
min_amount_liquidity,
max_amount_to_add_token_a,
max_amount_to_add_token_b,
..
} => Self::AddLiquidity {
min_amount_liquidity: *min_amount_liquidity,
max_amount_to_add_token_a: *max_amount_to_add_token_a,
max_amount_to_add_token_b: *max_amount_to_add_token_b,
},
amm_core::Instruction::RemoveLiquidity {
remove_liquidity_amount,
min_amount_to_remove_token_a,
min_amount_to_remove_token_b,
..
} => Self::RemoveLiquidity {
remove_liquidity_amount: *remove_liquidity_amount,
min_amount_to_remove_token_a: *min_amount_to_remove_token_a,
min_amount_to_remove_token_b: *min_amount_to_remove_token_b,
},
amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
..
} => Self::SwapExactInput {
swap_amount_in: *swap_amount_in,
min_amount_out: *min_amount_out,
},
amm_core::Instruction::SwapExactOutput {
exact_amount_out,
max_amount_in,
..
} => Self::SwapExactOutput {
exact_amount_out: *exact_amount_out,
max_amount_in: *max_amount_in,
},
amm_core::Instruction::SyncReserves => Self::SyncReserves,
}
}
}
#[derive(Serialize)]
+51 -61
View File
@@ -98,7 +98,7 @@ impl From<SequencerAccountError> for WireError {
}
#[derive(Clone, Copy, Deserialize)]
#[serde(try_from = "String")]
#[serde(transparent)]
struct ProgramIdInput(ProgramId);
impl From<ProgramIdInput> for ProgramId {
@@ -107,14 +107,6 @@ impl From<ProgramIdInput> for ProgramId {
}
}
impl TryFrom<String> for ProgramIdInput {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
parse_program_id(&value).map(Self)
}
}
#[derive(Deserialize)]
#[serde(tag = "operation", rename_all = "snake_case")]
enum PlanRequest {
@@ -1648,7 +1640,7 @@ pub fn quote_json(value: Value) -> Result<Value, WireError> {
&user_output,
decimal_u128(&amount_in, "amountIn")?,
)?;
Ok(swap_quote_json(quote))
swap_quote_with_pool_spot_change_json(snapshot.pool(), quote)
}
QuoteRequest::PrepareSwapExactInput {
state,
@@ -1673,7 +1665,7 @@ pub fn quote_json(value: Value) -> Result<Value, WireError> {
decimal_u128(&amount_in, "amountIn")?,
slippage_tolerance(&slippage_bps)?,
)?;
Ok(prepared_swap_exact_input_json(prepared))
prepared_swap_exact_input_json(snapshot.pool(), prepared)
}
QuoteRequest::SwapExactInput {
state,
@@ -1698,7 +1690,7 @@ pub fn quote_json(value: Value) -> Result<Value, WireError> {
decimal_u128(&amount_in, "amountIn")?,
decimal_u128(&minimum_amount_out, "minimumAmountOut")?,
)?;
Ok(swap_quote_json(quote))
swap_quote_with_pool_spot_change_json(snapshot.pool(), quote)
}
QuoteRequest::PreviewSwapExactOutput {
state,
@@ -1721,7 +1713,7 @@ pub fn quote_json(value: Value) -> Result<Value, WireError> {
&user_output,
decimal_u128(&exact_amount_out, "exactAmountOut")?,
)?;
Ok(swap_quote_json(quote))
swap_quote_with_pool_spot_change_json(snapshot.pool(), quote)
}
QuoteRequest::PrepareSwapExactOutput {
state,
@@ -1746,7 +1738,7 @@ pub fn quote_json(value: Value) -> Result<Value, WireError> {
decimal_u128(&exact_amount_out, "exactAmountOut")?,
slippage_tolerance(&slippage_bps)?,
)?;
Ok(prepared_swap_exact_output_json(prepared))
prepared_swap_exact_output_json(snapshot.pool(), prepared)
}
QuoteRequest::SwapExactOutput {
state,
@@ -1771,7 +1763,7 @@ pub fn quote_json(value: Value) -> Result<Value, WireError> {
decimal_u128(&exact_amount_out, "exactAmountOut")?,
decimal_u128(&maximum_amount_in, "maximumAmountIn")?,
)?;
Ok(swap_quote_json(quote))
swap_quote_with_pool_spot_change_json(snapshot.pool(), quote)
}
QuoteRequest::SyncReserves { state } => {
let (_, snapshot) = state.validate()?;
@@ -1818,7 +1810,7 @@ fn transaction_plan_json(plan: &TransactionPlan) -> Result<Value, WireError> {
Ok(json!({
"instruction": plan.instruction_name(),
"instructionArgs": instruction_args_json(plan.instruction()),
"programId": program_id_hex(plan.program_id()),
"programId": program_id_words(plan.program_id()),
"accounts": accounts,
"affectedAccountIds": plan
.affected_account_ids()
@@ -1836,8 +1828,8 @@ fn instruction_args_json(instruction: &Instruction) -> Value {
twap_oracle_program_id,
authority,
} => json!({
"tokenProgramId": program_id_hex(*token_program_id),
"twapOracleProgramId": program_id_hex(*twap_oracle_program_id),
"tokenProgramId": program_id_words(*token_program_id),
"twapOracleProgramId": program_id_words(*twap_oracle_program_id),
"authority": authority.to_string(),
}),
Instruction::UpdateConfig {
@@ -1845,8 +1837,8 @@ fn instruction_args_json(instruction: &Instruction) -> Value {
twap_oracle_program_id,
new_authority,
} => json!({
"tokenProgramId": token_program_id.map(program_id_hex),
"twapOracleProgramId": twap_oracle_program_id.map(program_id_hex),
"tokenProgramId": token_program_id.map(program_id_words),
"twapOracleProgramId": twap_oracle_program_id.map(program_id_words),
"newAuthority": new_authority.map(|authority| authority.to_string()),
}),
Instruction::CreatePriceObservations { window_duration }
@@ -2001,7 +1993,7 @@ fn account_snapshot_json(snapshot: &AccountSnapshot) -> Value {
let account = snapshot.account();
json!({
"id": snapshot.account_id().to_string(),
"programOwner": program_id_hex(account.program_owner),
"programOwner": program_id_words(account.program_owner),
"balance": account.balance.to_string(),
"nonce": account.nonce.0.to_string(),
"data": account
@@ -2015,10 +2007,10 @@ fn account_snapshot_json(snapshot: &AccountSnapshot) -> Value {
fn amm_context_json(context: &AmmContext) -> Value {
json!({
"ammProgramId": program_id_hex(context.amm_program_id),
"ammProgramId": program_id_words(context.amm_program_id),
"configId": context.config_id().to_string(),
"tokenProgramId": program_id_hex(context.token_program_id()),
"twapOracleProgramId": program_id_hex(context.twap_oracle_program_id()),
"tokenProgramId": program_id_words(context.token_program_id()),
"twapOracleProgramId": program_id_words(context.twap_oracle_program_id()),
"authority": context.config.authority.to_string(),
})
}
@@ -2289,24 +2281,49 @@ fn swap_quote_json(quote: SwapQuote) -> Value {
})
}
fn prepared_swap_exact_input_json(prepared: PreparedSwapExactInput) -> Value {
json!({
"quote": swap_quote_json(prepared.quote),
fn swap_quote_with_pool_spot_change_json(
before: &PoolDefinition,
quote: SwapQuote,
) -> Result<Value, WireError> {
let pool_spot_change_bps = crate::pool_spot_change_bps(before, &quote)?;
let mut value = swap_quote_json(quote);
let Some(object) = value.as_object_mut() else {
return Err(WireError::new(
"response_serialization_failed",
"swap quote response must be a JSON object",
));
};
object.insert(
String::from("poolSpotChangeBps"),
Value::String(pool_spot_change_bps.to_string()),
);
Ok(value)
}
fn prepared_swap_exact_input_json(
before: &PoolDefinition,
prepared: PreparedSwapExactInput,
) -> Result<Value, WireError> {
Ok(json!({
"quote": swap_quote_with_pool_spot_change_json(before, prepared.quote)?,
"instructionArgs": {
"swapAmountIn": prepared.swap_amount_in.to_string(),
"minAmountOut": prepared.min_amount_out.to_string(),
},
})
}))
}
fn prepared_swap_exact_output_json(prepared: PreparedSwapExactOutput) -> Value {
json!({
"quote": swap_quote_json(prepared.quote),
fn prepared_swap_exact_output_json(
before: &PoolDefinition,
prepared: PreparedSwapExactOutput,
) -> Result<Value, WireError> {
Ok(json!({
"quote": swap_quote_with_pool_spot_change_json(before, prepared.quote)?,
"instructionArgs": {
"exactAmountOut": prepared.exact_amount_out.to_string(),
"maxAmountIn": prepared.max_amount_in.to_string(),
},
})
}))
}
fn sync_reserves_quote_json(quote: SyncReservesQuote) -> Value {
@@ -2326,35 +2343,8 @@ fn oracle_price_quote_json(quote: OraclePriceAccountQuote) -> Value {
})
}
fn parse_program_id(value: &str) -> Result<ProgramId, String> {
if value.len() != 64
|| !value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
{
return Err(String::from(
"program ID must be exactly 64 lowercase hexadecimal characters",
));
}
let bytes = hex_bytes(value, "program ID").map_err(|error| error.to_string())?;
let mut program_id = [0_u32; 8];
for (word, bytes) in program_id.iter_mut().zip(bytes.chunks_exact(4)) {
let mut word_bytes = [0_u8; 4];
word_bytes.copy_from_slice(bytes);
*word = u32::from_le_bytes(word_bytes);
}
Ok(program_id)
}
fn program_id_hex(program_id: ProgramId) -> String {
let mut output = String::with_capacity(64);
for word in program_id {
for byte in word.to_le_bytes() {
output.push_str(&format!("{byte:02x}"));
}
}
output
const fn program_id_words(program_id: ProgramId) -> ProgramId {
program_id
}
fn account_id(value: &str, field: &str) -> Result<AccountId, WireError> {
+2 -8
View File
@@ -1,11 +1,5 @@
use nssa_core::program::ProgramId;
pub fn program_id_hex(program_id: ProgramId) -> String {
let mut output = String::with_capacity(64);
for word in program_id {
for byte in word.to_le_bytes() {
output.push_str(&format!("{byte:02x}"));
}
}
output
pub const fn program_id_words(program_id: ProgramId) -> ProgramId {
program_id
}
@@ -5,7 +5,7 @@ use amm_client::{
Q64_64_ONE,
};
use amm_core::canonical_token_pair;
use common::program_id_hex;
use common::program_id_words;
use nssa_core::account::AccountId;
use serde_json::json;
@@ -41,7 +41,7 @@ fn raw_sequencer_response_becomes_lossless_snapshot() {
assert_eq!(wire["id"], account_id.to_string());
assert_eq!(
wire["programOwner"],
program_id_hex([1, 2, 3, 4, 5, 6, 7, 8])
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");
+14 -13
View File
@@ -12,7 +12,7 @@ use amm_core::{
compute_config_pda, compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda,
AmmConfig, Instruction, PoolDefinition, FEE_TIER_BPS_30, MINIMUM_LIQUIDITY,
};
use common::program_id_hex;
use common::program_id_words;
use nssa_core::{
account::{Account, AccountId, Data, Nonce},
program::ProgramId,
@@ -48,7 +48,7 @@ fn call_json(operation: Operation, request: &Value) -> Value {
fn snapshot(id: AccountId, account: &Account) -> Value {
json!({
"id": id.to_string(),
"programOwner": program_id_hex(account.program_owner),
"programOwner": program_id_words(account.program_owner),
"balance": account.balance.to_string(),
"nonce": account.nonce.0.to_string(),
"data": hex(account.data.as_ref()),
@@ -151,13 +151,14 @@ fn protocol_constants_are_exposed_without_numeric_json_values() {
}
#[test]
fn program_ids_require_canonical_lowercase_hex_strings() {
let canonical = program_id_hex([0xabcdef01; 8]);
fn program_ids_require_exactly_eight_u32_words() {
let canonical = program_id_words([0xabcdef01; 8]);
let authority = AccountId::new([44; 32]).to_string();
for invalid in [
json!([1, 1, 1, 1, 1, 1, 1, 1]),
json!(canonical.to_uppercase()),
json!("0000000000000000000000000000000000000000000000000000000000000000"),
json!([1, 1, 1, 1, 1, 1, 1]),
json!([1, 1, 1, 1, 1, 1, 1, 4_294_967_296_u64]),
] {
let response = call_json(
amm_client_plan,
@@ -188,9 +189,9 @@ fn successful_plan_preserves_u64_above_javascript_range_in_guest_words() {
&json!({
"operation": "create_price_observations",
"context": {
"ammProgramId": program_id_hex(amm_program_id),
"tokenProgramId": program_id_hex(token_program_id),
"twapOracleProgramId": program_id_hex(twap_oracle_program_id),
"ammProgramId": program_id_words(amm_program_id),
"tokenProgramId": program_id_words(token_program_id),
"twapOracleProgramId": program_id_words(twap_oracle_program_id),
"authority": authority.to_string(),
},
"poolId": pool_id.to_string(),
@@ -205,7 +206,7 @@ fn successful_plan_preserves_u64_above_javascript_range_in_guest_words() {
);
assert_eq!(
response["value"]["programId"],
program_id_hex(amm_program_id)
json!(program_id_words(amm_program_id))
);
assert!(response["value"]["accounts"].is_array());
let words: Vec<u32> = serde_json::from_value(response["value"]["instructionWords"].clone())
@@ -263,7 +264,7 @@ fn successful_quote_preserves_u128_above_javascript_range_as_decimal() {
amm_client_quote,
&json!({
"operation": "create_pool",
"ammProgramId": program_id_hex(amm_program_id),
"ammProgramId": program_id_words(amm_program_id),
"config": snapshot(compute_config_pda(amm_program_id), &config_account),
"tokenADefinition": snapshot(token_a_id, &definition("A")),
"tokenBDefinition": snapshot(token_b_id, &definition("B")),
@@ -289,7 +290,7 @@ fn successful_quote_preserves_u128_above_javascript_range_as_decimal() {
amm_client_quote,
&json!({
"operation": "prepare_create_pool",
"ammProgramId": program_id_hex(amm_program_id),
"ammProgramId": program_id_words(amm_program_id),
"config": snapshot(compute_config_pda(amm_program_id), &config_account),
"tokenADefinition": snapshot(token_a_id, &definition("A")),
"tokenBDefinition": snapshot(token_b_id, &definition("B")),
@@ -342,7 +343,7 @@ fn swap_quote_rejects_unrelated_output_holding() {
amm_client_quote,
&json!({
"operation": "preview_swap_exact_input",
"ammProgramId": program_id_hex(amm_program_id),
"ammProgramId": program_id_words(amm_program_id),
"config": snapshot(
compute_config_pda(amm_program_id),
&account(amm_program_id, Data::from(&config)),
@@ -481,7 +481,7 @@ fn exact_output_requires_funding_through_its_maximum_input_guard() {
}
#[test]
fn commitment_is_stable_and_changes_with_bound_snapshot_or_deadline() {
fn commitment_is_stable_across_deadline_refresh_and_changes_with_bound_snapshot() {
let fixture = Fixture::new();
let missing = MissingPairFixture::new(&fixture);
let fresh_lp = AccountSnapshot::new(AccountId::new([30; 32]), Account::default());
@@ -522,10 +522,14 @@ fn commitment_is_stable_and_changes_with_bound_snapshot_or_deadline() {
));
let changed_deadline = prepare(&fixture.caller_first_holding, DEADLINE + 1);
assert_ne!(
assert_eq!(
first.quote_commitment(),
changed_deadline.quote_commitment()
);
assert_ne!(
first.plan().instruction_data(),
changed_deadline.plan().instruction_data()
);
}
#[test]
@@ -6,7 +6,7 @@ use amm_core::{
compute_vault_pda, AmmConfig, PoolDefinition, FEE_TIER_BPS_30, MINIMUM_LIQUIDITY,
};
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
use common::program_id_hex;
use common::program_id_words;
use nssa_core::{
account::{Account, AccountId, Data, Nonce},
program::ProgramId,
@@ -111,7 +111,7 @@ impl PairIds {
fn inspect_request(&self, snapshots: Value) -> Value {
json!({
"operation": "inspect_pair",
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": config_snapshot(),
"firstTokenDefinitionId": self.first_token_id.to_string(),
"secondTokenDefinitionId": self.second_token_id.to_string(),
@@ -205,7 +205,7 @@ impl PairIds {
fn snapshot(id: AccountId, account: &Account) -> Value {
json!({
"id": id.to_string(),
"programOwner": program_id_hex(account.program_owner),
"programOwner": program_id_words(account.program_owner),
"balance": account.balance.to_string(),
"nonce": account.nonce.0.to_string(),
"data": account
@@ -232,7 +232,7 @@ fn discovery_operations_return_exact_string_account_ids() {
let config_id = quote_json(json!({
"operation": "derive_config_id",
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
}))
.expect("legacy schema-less request remains accepted");
assert_eq!(config_id["schema"], WIRE_SCHEMA);
@@ -245,19 +245,22 @@ fn discovery_operations_return_exact_string_account_ids() {
let inspected = quote_json(json!({
"schema": WIRE_SCHEMA,
"operation": "inspect_config",
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": config.clone(),
}))
.expect("config must inspect");
assert_eq!(inspected["schema"], WIRE_SCHEMA);
assert_eq!(inspected["ammProgramId"], program_id_hex(AMM_PROGRAM_ID));
assert_eq!(
inspected["ammProgramId"],
json!(program_id_words(AMM_PROGRAM_ID))
);
assert_eq!(
inspected["tokenProgramId"],
program_id_hex(TOKEN_PROGRAM_ID)
json!(program_id_words(TOKEN_PROGRAM_ID))
);
assert_eq!(
inspected["twapOracleProgramId"],
program_id_hex(TWAP_ORACLE_PROGRAM_ID)
json!(program_id_words(TWAP_ORACLE_PROGRAM_ID))
);
assert_eq!(inspected["authority"], AccountId::new([9; 32]).to_string());
@@ -272,7 +275,7 @@ fn discovery_operations_return_exact_string_account_ids() {
let manifest = quote_json(json!({
"operation": "derive_pair_read_manifest",
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": config,
"firstTokenDefinitionId": first_token_id.to_string(),
"secondTokenDefinitionId": second_token_id.to_string(),
@@ -5,7 +5,7 @@ use amm_core::{
compute_config_pda, compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda,
AmmConfig, PoolDefinition, FEE_TIER_BPS_30,
};
use common::program_id_hex;
use common::program_id_words;
use nssa_core::{
account::{Account, AccountId, Data, Nonce},
program::ProgramId,
@@ -29,7 +29,7 @@ fn account(program_owner: ProgramId, data: Data) -> Account {
fn snapshot(id: AccountId, account: &Account) -> Value {
json!({
"id": id.to_string(),
"programOwner": program_id_hex(account.program_owner),
"programOwner": program_id_words(account.program_owner),
"balance": account.balance.to_string(),
"nonce": account.nonce.0.to_string(),
"data": account
@@ -102,7 +102,7 @@ impl WireFixture {
fees: FEE_TIER_BPS_30,
};
let state = json!({
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": config,
"snapshot": {
"pool": snapshot(pool_id, &account(AMM_PROGRAM_ID, Data::from(&pool))),
@@ -164,7 +164,7 @@ fn prepare_wire_operations_return_lossless_instruction_args() {
let create = quote_json(json!({
"operation": "prepare_create_pool",
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": fixture.config.clone(),
"tokenADefinition": snapshot(fixture.token_a_id, &definition(100_000, None)),
"tokenBDefinition": snapshot(fixture.token_b_id, &definition(100_000, None)),
@@ -264,6 +264,159 @@ fn prepare_wire_operations_return_lossless_instruction_args() {
);
}
#[test]
fn standalone_swap_quotes_include_exact_pool_spot_change() {
let fixture = WireFixture::new();
let input_holding = fixture.user_a.clone();
let output_holding = fixture.user_b.clone();
let input_token_definition_id = fixture.token_a_id.to_string();
let mut preview_exact_input = fixture.request("preview_swap_exact_input");
insert(
&mut preview_exact_input,
"userInputHolding",
input_holding.clone(),
);
insert(
&mut preview_exact_input,
"userOutputHolding",
output_holding.clone(),
);
insert(
&mut preview_exact_input,
"inputTokenDefinitionId",
json!(input_token_definition_id.clone()),
);
insert(&mut preview_exact_input, "amountIn", json!("100"));
let preview_exact_input = quote_json(preview_exact_input).expect("preview must quote");
let mut prepare_exact_input = fixture.request("prepare_swap_exact_input");
insert(
&mut prepare_exact_input,
"userInputHolding",
input_holding.clone(),
);
insert(
&mut prepare_exact_input,
"userOutputHolding",
output_holding.clone(),
);
insert(
&mut prepare_exact_input,
"inputTokenDefinitionId",
json!(input_token_definition_id.clone()),
);
insert(&mut prepare_exact_input, "amountIn", json!("100"));
insert(&mut prepare_exact_input, "slippageBps", json!("100"));
let prepare_exact_input = quote_json(prepare_exact_input).expect("preparation must quote");
let mut exact_input = fixture.request("swap_exact_input");
insert(&mut exact_input, "userInputHolding", input_holding.clone());
insert(
&mut exact_input,
"userOutputHolding",
output_holding.clone(),
);
insert(
&mut exact_input,
"inputTokenDefinitionId",
json!(input_token_definition_id.clone()),
);
insert(&mut exact_input, "amountIn", json!("100"));
insert(&mut exact_input, "minimumAmountOut", json!("1"));
let exact_input = quote_json(exact_input).expect("exact-input quote must succeed");
let mut preview_exact_output = fixture.request("preview_swap_exact_output");
insert(
&mut preview_exact_output,
"userInputHolding",
input_holding.clone(),
);
insert(
&mut preview_exact_output,
"userOutputHolding",
output_holding.clone(),
);
insert(
&mut preview_exact_output,
"inputTokenDefinitionId",
json!(input_token_definition_id.clone()),
);
insert(&mut preview_exact_output, "exactAmountOut", json!("45"));
let preview_exact_output = quote_json(preview_exact_output).expect("preview must quote");
let mut prepare_exact_output = fixture.request("prepare_swap_exact_output");
insert(
&mut prepare_exact_output,
"userInputHolding",
input_holding.clone(),
);
insert(
&mut prepare_exact_output,
"userOutputHolding",
output_holding.clone(),
);
insert(
&mut prepare_exact_output,
"inputTokenDefinitionId",
json!(input_token_definition_id.clone()),
);
insert(&mut prepare_exact_output, "exactAmountOut", json!("45"));
insert(&mut prepare_exact_output, "slippageBps", json!("100"));
let prepare_exact_output =
quote_json(prepare_exact_output).expect("preparation must quote exact output");
let mut exact_output = fixture.request("swap_exact_output");
insert(&mut exact_output, "userInputHolding", input_holding);
insert(&mut exact_output, "userOutputHolding", output_holding);
insert(
&mut exact_output,
"inputTokenDefinitionId",
json!(input_token_definition_id),
);
insert(&mut exact_output, "exactAmountOut", json!("45"));
insert(&mut exact_output, "maximumAmountIn", json!("100"));
let exact_output = quote_json(exact_output).expect("exact-output quote must succeed");
let top_level_results = [
&preview_exact_input,
&exact_input,
&preview_exact_output,
&exact_output,
];
for result in top_level_results {
assert_eq!(result["poolSpotChangeBps"], "2087");
}
for result in [&prepare_exact_input, &prepare_exact_output] {
assert_eq!(result["quote"]["poolSpotChangeBps"], "2087");
}
let large_amount = 1_u128 << 80;
let mut large_input = fixture.request("preview_swap_exact_input");
insert(
&mut large_input,
"userInputHolding",
snapshot(
AccountId::new([20; 32]),
&holding(fixture.token_a_id, large_amount),
),
);
insert(&mut large_input, "userOutputHolding", fixture.user_b);
insert(
&mut large_input,
"inputTokenDefinitionId",
json!(fixture.token_a_id.to_string()),
);
insert(
&mut large_input,
"amountIn",
json!(large_amount.to_string()),
);
let large_input = quote_json(large_input).expect("large preview must quote");
let large_movement = decimal(&large_input["poolSpotChangeBps"]);
assert!(large_movement > (1_u128 << 53));
}
#[test]
fn prepare_wire_rejects_out_of_range_slippage() {
let fixture = WireFixture::new();
@@ -6,7 +6,7 @@ use amm_core::{
compute_vault_pda, AmmConfig, Instruction, PoolDefinition, FEE_TIER_BPS_30, MINIMUM_LIQUIDITY,
};
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
use common::program_id_hex;
use common::program_id_words;
use nssa_core::{
account::{Account, AccountId, Data, Nonce},
program::ProgramId,
@@ -55,7 +55,7 @@ fn holding(definition_id: AccountId, balance: u128) -> Account {
fn snapshot(id: AccountId, account: &Account) -> Value {
json!({
"id": id.to_string(),
"programOwner": program_id_hex(account.program_owner),
"programOwner": program_id_words(account.program_owner),
"balance": account.balance.to_string(),
"nonce": account.nonce.0.to_string(),
"data": account
@@ -212,7 +212,7 @@ impl TransactionFixture {
fn active_common(&self, operation: &str) -> Value {
json!({
"operation": operation,
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": self.config.clone(),
"snapshots": self.active_snapshots.clone(),
"firstTokenDefinitionId": self.first_token_id.to_string(),
@@ -229,7 +229,7 @@ impl TransactionFixture {
fn swap_common(&self, operation: &str) -> Value {
json!({
"operation": operation,
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": self.config.clone(),
"snapshots": self.active_snapshots.clone(),
"inputTokenDefinitionId": self.first_token_id.to_string(),
@@ -326,7 +326,7 @@ fn five_transaction_operations_emit_exact_plans_and_task_artifacts() {
let second_amount = LARGE.checked_mul(2).expect("test amount fits");
let create = plan_json(json!({
"operation": "prepare_create_pool_transaction",
"ammProgramId": program_id_hex(AMM_PROGRAM_ID),
"ammProgramId": program_id_words(AMM_PROGRAM_ID),
"config": fixture.config.clone(),
"snapshots": fixture.missing_snapshots.clone(),
"firstTokenDefinitionId": fixture.first_token_id.to_string(),