diff --git a/programs/amm/client/README.md b/programs/amm/client/README.md index e5b1958..b6593ae 100644 --- a/programs/amm/client/README.md +++ b/programs/amm/client/README.md @@ -71,9 +71,9 @@ Every call returns an owned JSON envelope. Release it exactly once with `amm_cli `NULL` to the free function is allowed. See [`include/amm_client.h`](include/amm_client.h) and [`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 their canonical base58 -display form, program IDs use eight JSON `u32` words, 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`. Both C entrypoints accept the five -snapshot-bound `prepare_*_transaction` operations. +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. diff --git a/programs/amm/client/docs/wire-api.md b/programs/amm/client/docs/wire-api.md index 8eef669..b533215 100644 --- a/programs/amm/client/docs/wire-api.md +++ b/programs/amm/client/docs/wire-api.md @@ -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 arrays of eight `u32` -words. Signed ticks are decimal strings. Account IDs are base58 strings. Account `data` is an -even-length hexadecimal string. +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. +Account `data` is an even-length hexadecimal string. ## Shared inputs @@ -24,9 +25,9 @@ Plan context: ```json { - "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], + "ammProgramId": "0000000000000000000000000000000000000000000000000000000000000000", + "tokenProgramId": "0000000000000000000000000000000000000000000000000000000000000000", + "twapOracleProgramId": "0000000000000000000000000000000000000000000000000000000000000000", "authority": "base58-account-id" } ``` @@ -53,7 +54,7 @@ Fetched account snapshot used by quotes: ```json { "id": "base58-account-id", - "programOwner": [0, 0, 0, 0, 0, 0, 0, 0], + "programOwner": "0000000000000000000000000000000000000000000000000000000000000000", "balance": "0", "nonce": "0", "data": "00ff" @@ -64,7 +65,7 @@ Existing-pool quote operations include these top-level state fields: ```json { - "ammProgramId": [0, 0, 0, 0, 0, 0, 0, 0], + "ammProgramId": "0000000000000000000000000000000000000000000000000000000000000000", "config": { "...": "account snapshot" }, "snapshot": { "pool": { "...": "account snapshot" }, @@ -128,7 +129,7 @@ A successful plan value contains the following fields (`instructionWords` is abb "maxAmountToAddTokenB": "100", "deadline": "1900000000000" }, - "programId": [0, 0, 0, 0, 0, 0, 0, 0], + "programId": "0000000000000000000000000000000000000000000000000000000000000000", "accounts": [ { "id": "base58-account-id", @@ -152,8 +153,8 @@ strings. Account rows follow guest/IDL order. ## Quote operations Send requests to `amm_client_quote` or `wire::quote_json`. Pool economic operations use the -existing-pool quote state described above. Discovery, opening intent, and task-transaction -operations use the fields shown in this table and the sections below. +existing-pool quote state described above. Discovery and opening-intent operations use the fields +shown in this table and the sections below. | `operation` | Additional fields | |---|---| @@ -185,11 +186,6 @@ operations use the fields shown in this table and the sections below. | `swap_exact_output` | `userInputHolding`, `userOutputHolding`, `inputTokenDefinitionId`, `exactAmountOut`, `maximumAmountIn` | | `sync_reserves` | no additional fields | | `create_oracle_price_account` | `windowDuration` | -| `prepare_create_pool_transaction` | task-transaction fields below | -| `prepare_add_liquidity_transaction` | task-transaction fields below | -| `prepare_remove_liquidity_transaction` | task-transaction fields below | -| `prepare_swap_exact_input_transaction` | task-transaction fields below | -| `prepare_swap_exact_output_transaction` | task-transaction fields below | Quote values use these result shapes: @@ -234,9 +230,10 @@ amounts. ## Task transactions -The five snapshot-bound task operations are accepted by both `amm_client_plan`/`wire::plan_json` -and `amm_client_quote`/`wire::quote_json`. Every request includes `ammProgramId`, raw `config`, the -complete caller-ordered `snapshots`, and decimal-string `deadline`. +The five snapshot-bound task operations are accepted only by +`amm_client_plan`/`wire::plan_json`. Every request includes `ammProgramId`, raw `config`, the +complete caller-ordered `snapshots`, and decimal-string `deadline`. The quote endpoint rejects +these operation tags with `invalid_request`. | `operation` | Additional fields | |---|---| @@ -314,5 +311,11 @@ The client validates account decoding, configured owners, canonical PDAs, pool/v relationships, swap input/output pairing, and required input balances. Quote arithmetic failures retain the stable `amm_program::quote::QuoteError` code. +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. + 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 4dc680a..5444644 100644 --- a/programs/amm/client/include/amm_client.h +++ b/programs/amm/client/include/amm_client.h @@ -19,18 +19,18 @@ 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; and prepare_create_pool_transaction, - * prepare_add_liquidity_transaction, prepare_remove_liquidity_transaction, - * prepare_swap_exact_input_transaction, and - * prepare_swap_exact_output_transaction. See docs/wire-api.md for fields. + * operations; reserve synchronization; and oracle-price initialization. + * Snapshot-bound prepare_*_transaction operations belong to amm_client_plan. + * See docs/wire-api.md for fields. * Release the result with amm_client_free. */ char *amm_client_quote(const char *request_json); /* * Raw u128, u64, and signed tick values are decimal JSON strings. Program IDs - * and instruction words are JSON u32 arrays. Account IDs are base58 strings and - * account data is hexadecimal. Requests may carry schema "amm-client.v1"; + * are 64-character lowercase hexadecimal strings. 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 * with ok=false and error={"code":...,"message":...}. Plan values contain diff --git a/programs/amm/client/src/wire.rs b/programs/amm/client/src/wire.rs index ab9625d..fa545b0 100644 --- a/programs/amm/client/src/wire.rs +++ b/programs/amm/client/src/wire.rs @@ -90,24 +90,42 @@ impl From for WireError { } } +#[derive(Clone, Copy, Deserialize)] +#[serde(try_from = "String")] +struct ProgramIdInput(ProgramId); + +impl From for ProgramId { + fn from(value: ProgramIdInput) -> Self { + value.0 + } +} + +impl TryFrom for ProgramIdInput { + type Error = String; + + fn try_from(value: String) -> Result { + parse_program_id(&value).map(Self) + } +} + #[derive(Deserialize)] #[serde(tag = "operation", rename_all = "snake_case")] enum PlanRequest { Initialize { #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, #[serde(rename = "tokenProgramId")] - token_program_id: ProgramId, + token_program_id: ProgramIdInput, #[serde(rename = "twapOracleProgramId")] - twap_oracle_program_id: ProgramId, + twap_oracle_program_id: ProgramIdInput, authority: String, }, UpdateConfig { context: ContextInput, #[serde(rename = "tokenProgramId")] - token_program_id: Option, + token_program_id: Option, #[serde(rename = "twapOracleProgramId")] - twap_oracle_program_id: Option, + twap_oracle_program_id: Option, #[serde(rename = "newAuthority")] new_authority: Option, }, @@ -208,24 +226,137 @@ enum PlanRequest { context: ContextInput, pool: PoolInput, }, + PrepareCreatePoolTransaction { + #[serde(rename = "ammProgramId")] + amm_program_id: ProgramIdInput, + config: AccountSnapshotInput, + snapshots: Box, + #[serde(rename = "firstTokenDefinitionId")] + first_token_definition_id: String, + #[serde(rename = "secondTokenDefinitionId")] + second_token_definition_id: String, + #[serde(rename = "firstTokenHolding")] + first_token_holding: AccountSnapshotInput, + #[serde(rename = "secondTokenHolding")] + second_token_holding: AccountSnapshotInput, + #[serde(rename = "liquidityHolding")] + liquidity_holding: AccountSnapshotInput, + #[serde(rename = "firstAmount")] + first_amount: String, + #[serde(rename = "secondAmount")] + second_amount: String, + #[serde(rename = "feeBps")] + fee_bps: String, + deadline: String, + }, + PrepareAddLiquidityTransaction { + #[serde(rename = "ammProgramId")] + amm_program_id: ProgramIdInput, + config: AccountSnapshotInput, + snapshots: Box, + #[serde(rename = "firstTokenDefinitionId")] + first_token_definition_id: String, + #[serde(rename = "secondTokenDefinitionId")] + second_token_definition_id: String, + #[serde(rename = "firstTokenHolding")] + first_token_holding: AccountSnapshotInput, + #[serde(rename = "secondTokenHolding")] + second_token_holding: AccountSnapshotInput, + #[serde(rename = "liquidityHolding")] + liquidity_holding: AccountSnapshotInput, + #[serde(rename = "maxFirstAmount")] + max_first_amount: String, + #[serde(rename = "maxSecondAmount")] + max_second_amount: String, + #[serde(rename = "slippageBps")] + slippage_bps: String, + #[serde(rename = "expectedFeeBps")] + expected_fee_bps: Option, + deadline: String, + }, + PrepareRemoveLiquidityTransaction { + #[serde(rename = "ammProgramId")] + amm_program_id: ProgramIdInput, + config: AccountSnapshotInput, + snapshots: Box, + #[serde(rename = "firstTokenDefinitionId")] + first_token_definition_id: String, + #[serde(rename = "secondTokenDefinitionId")] + second_token_definition_id: String, + #[serde(rename = "firstTokenHolding")] + first_token_holding: AccountSnapshotInput, + #[serde(rename = "secondTokenHolding")] + second_token_holding: AccountSnapshotInput, + #[serde(rename = "liquidityHolding")] + liquidity_holding: AccountSnapshotInput, + #[serde(rename = "removeLiquidityAmount")] + remove_liquidity_amount: String, + #[serde(rename = "slippageBps")] + slippage_bps: String, + #[serde(rename = "expectedFeeBps")] + expected_fee_bps: Option, + deadline: String, + }, + PrepareSwapExactInputTransaction { + #[serde(rename = "ammProgramId")] + amm_program_id: ProgramIdInput, + config: AccountSnapshotInput, + snapshots: Box, + #[serde(rename = "inputTokenDefinitionId")] + input_token_definition_id: String, + #[serde(rename = "outputTokenDefinitionId")] + output_token_definition_id: String, + #[serde(rename = "inputHolding")] + input_holding: AccountSnapshotInput, + #[serde(rename = "outputHolding")] + output_holding: AccountSnapshotInput, + #[serde(rename = "amountIn")] + amount_in: String, + #[serde(rename = "slippageBps")] + slippage_bps: String, + #[serde(rename = "expectedFeeBps")] + expected_fee_bps: Option, + deadline: String, + }, + PrepareSwapExactOutputTransaction { + #[serde(rename = "ammProgramId")] + amm_program_id: ProgramIdInput, + config: AccountSnapshotInput, + snapshots: Box, + #[serde(rename = "inputTokenDefinitionId")] + input_token_definition_id: String, + #[serde(rename = "outputTokenDefinitionId")] + output_token_definition_id: String, + #[serde(rename = "inputHolding")] + input_holding: AccountSnapshotInput, + #[serde(rename = "outputHolding")] + output_holding: AccountSnapshotInput, + #[serde(rename = "exactAmountOut")] + exact_amount_out: String, + #[serde(rename = "slippageBps")] + slippage_bps: String, + #[serde(rename = "expectedFeeBps")] + expected_fee_bps: Option, + deadline: String, + }, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ContextInput { - amm_program_id: ProgramId, - token_program_id: ProgramId, - twap_oracle_program_id: ProgramId, + amm_program_id: ProgramIdInput, + token_program_id: ProgramIdInput, + twap_oracle_program_id: ProgramIdInput, authority: String, } impl ContextInput { fn into_context(self) -> Result { Ok(AmmContext::new( - self.amm_program_id, + self.amm_program_id.into(), AmmConfig { - token_program_id: self.token_program_id, - twap_oracle_program_id: self.twap_oracle_program_id, + token_program_id: self.token_program_id.into(), + twap_oracle_program_id: self.twap_oracle_program_id.into(), authority: account_id(&self.authority, "context.authority")?, }, )) @@ -281,11 +412,11 @@ enum QuoteRequest { ProtocolConstants, DeriveConfigId { #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, }, InspectConfig { #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, config: AccountSnapshotInput, }, CanonicalPair { @@ -296,7 +427,7 @@ enum QuoteRequest { }, DerivePairReadManifest { #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, config: AccountSnapshotInput, #[serde(rename = "firstTokenDefinitionId")] first_token_definition_id: String, @@ -305,7 +436,7 @@ enum QuoteRequest { }, InspectPair { #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, config: AccountSnapshotInput, #[serde(rename = "firstTokenDefinitionId")] first_token_definition_id: String, @@ -324,119 +455,6 @@ enum QuoteRequest { fee_bps: String, intent: OpeningLiquidityIntentInput, }, - PrepareCreatePoolTransaction { - #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, - config: AccountSnapshotInput, - snapshots: Box, - #[serde(rename = "firstTokenDefinitionId")] - first_token_definition_id: String, - #[serde(rename = "secondTokenDefinitionId")] - second_token_definition_id: String, - #[serde(rename = "firstTokenHolding")] - first_token_holding: AccountSnapshotInput, - #[serde(rename = "secondTokenHolding")] - second_token_holding: AccountSnapshotInput, - #[serde(rename = "liquidityHolding")] - liquidity_holding: AccountSnapshotInput, - #[serde(rename = "firstAmount")] - first_amount: String, - #[serde(rename = "secondAmount")] - second_amount: String, - #[serde(rename = "feeBps")] - fee_bps: String, - deadline: String, - }, - PrepareAddLiquidityTransaction { - #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, - config: AccountSnapshotInput, - snapshots: Box, - #[serde(rename = "firstTokenDefinitionId")] - first_token_definition_id: String, - #[serde(rename = "secondTokenDefinitionId")] - second_token_definition_id: String, - #[serde(rename = "firstTokenHolding")] - first_token_holding: AccountSnapshotInput, - #[serde(rename = "secondTokenHolding")] - second_token_holding: AccountSnapshotInput, - #[serde(rename = "liquidityHolding")] - liquidity_holding: AccountSnapshotInput, - #[serde(rename = "maxFirstAmount")] - max_first_amount: String, - #[serde(rename = "maxSecondAmount")] - max_second_amount: String, - #[serde(rename = "slippageBps")] - slippage_bps: String, - #[serde(rename = "expectedFeeBps")] - expected_fee_bps: Option, - deadline: String, - }, - PrepareRemoveLiquidityTransaction { - #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, - config: AccountSnapshotInput, - snapshots: Box, - #[serde(rename = "firstTokenDefinitionId")] - first_token_definition_id: String, - #[serde(rename = "secondTokenDefinitionId")] - second_token_definition_id: String, - #[serde(rename = "firstTokenHolding")] - first_token_holding: AccountSnapshotInput, - #[serde(rename = "secondTokenHolding")] - second_token_holding: AccountSnapshotInput, - #[serde(rename = "liquidityHolding")] - liquidity_holding: AccountSnapshotInput, - #[serde(rename = "removeLiquidityAmount")] - remove_liquidity_amount: String, - #[serde(rename = "slippageBps")] - slippage_bps: String, - #[serde(rename = "expectedFeeBps")] - expected_fee_bps: Option, - deadline: String, - }, - PrepareSwapExactInputTransaction { - #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, - config: AccountSnapshotInput, - snapshots: Box, - #[serde(rename = "inputTokenDefinitionId")] - input_token_definition_id: String, - #[serde(rename = "outputTokenDefinitionId")] - output_token_definition_id: String, - #[serde(rename = "inputHolding")] - input_holding: AccountSnapshotInput, - #[serde(rename = "outputHolding")] - output_holding: AccountSnapshotInput, - #[serde(rename = "amountIn")] - amount_in: String, - #[serde(rename = "slippageBps")] - slippage_bps: String, - #[serde(rename = "expectedFeeBps")] - expected_fee_bps: Option, - deadline: String, - }, - PrepareSwapExactOutputTransaction { - #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, - config: AccountSnapshotInput, - snapshots: Box, - #[serde(rename = "inputTokenDefinitionId")] - input_token_definition_id: String, - #[serde(rename = "outputTokenDefinitionId")] - output_token_definition_id: String, - #[serde(rename = "inputHolding")] - input_holding: AccountSnapshotInput, - #[serde(rename = "outputHolding")] - output_holding: AccountSnapshotInput, - #[serde(rename = "exactAmountOut")] - exact_amount_out: String, - #[serde(rename = "slippageBps")] - slippage_bps: String, - #[serde(rename = "expectedFeeBps")] - expected_fee_bps: Option, - deadline: String, - }, PrepareMinimumOpeningPair { #[serde(rename = "desiredPriceQ64_64")] desired_price_q64_64: String, @@ -479,7 +497,7 @@ enum QuoteRequest { }, CreatePool { #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, config: AccountSnapshotInput, #[serde(rename = "tokenADefinition")] token_a_definition: AccountSnapshotInput, @@ -494,7 +512,7 @@ enum QuoteRequest { }, PrepareCreatePool { #[serde(rename = "ammProgramId")] - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, config: AccountSnapshotInput, #[serde(rename = "tokenADefinition")] token_a_definition: AccountSnapshotInput, @@ -660,7 +678,7 @@ enum QuoteRequest { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct PoolStateInput { - amm_program_id: ProgramId, + amm_program_id: ProgramIdInput, config: AccountSnapshotInput, snapshot: PoolSnapshotInput, } @@ -668,7 +686,7 @@ struct PoolStateInput { impl PoolStateInput { fn validate(self) -> Result<(AmmContext, ValidatedPoolSnapshot), WireError> { let config = self.config.into_snapshot()?; - let context = AmmContext::from_config_account(self.amm_program_id, &config)?; + let context = AmmContext::from_config_account(self.amm_program_id.into(), &config)?; let snapshot = self.snapshot.validate(&context)?; Ok((context, snapshot)) } @@ -807,7 +825,7 @@ impl PoolSnapshotInput { #[serde(rename_all = "camelCase")] struct AccountSnapshotInput { id: String, - program_owner: ProgramId, + program_owner: ProgramIdInput, balance: String, nonce: String, data: String, @@ -821,7 +839,7 @@ impl AccountSnapshotInput { Ok(AccountSnapshot::new( account_id(&self.id, "account.id")?, Account { - program_owner: self.program_owner, + program_owner: self.program_owner.into(), balance: decimal_u128(&self.balance, "account.balance")?, data, nonce: Nonce(decimal_u128(&self.nonce, "account.nonce")?), @@ -833,27 +851,20 @@ impl AccountSnapshotInput { /// Builds a canonical low-level plan or prepares a snapshot-bound task transaction from JSON. pub fn plan_json(value: Value) -> Result { validate_wire_schema(&value)?; - if value - .get("operation") - .and_then(Value::as_str) - .is_some_and(is_prepared_transaction_operation) - { - return quote_json(value); - } let request: PlanRequest = serde_json::from_value(value) .map_err(|error| invalid_request(format!("invalid plan request: {error}")))?; - let plan = match request { + versioned(match request { PlanRequest::Initialize { amm_program_id, token_program_id, twap_oracle_program_id, authority, - } => plan_initialize(InitializePlanInput { - amm_program_id, - token_program_id, - twap_oracle_program_id, + } => transaction_plan_json(&plan_initialize(InitializePlanInput { + amm_program_id: amm_program_id.into(), + token_program_id: token_program_id.into(), + twap_oracle_program_id: twap_oracle_program_id.into(), authority: account_id(&authority, "authority")?, - }), + })), PlanRequest::UpdateConfig { context, token_program_id, @@ -865,12 +876,12 @@ pub fn plan_json(value: Value) -> Result { .as_deref() .map(|value| account_id(value, "newAuthority")) .transpose()?; - plan_update_config(UpdateConfigPlanInput { + transaction_plan_json(&plan_update_config(UpdateConfigPlanInput { context: &context, - token_program_id, - twap_oracle_program_id, + token_program_id: token_program_id.map(Into::into), + twap_oracle_program_id: twap_oracle_program_id.map(Into::into), new_authority, - }) + })) } PlanRequest::CreatePriceObservations { context, @@ -878,11 +889,13 @@ pub fn plan_json(value: Value) -> Result { window_duration, } => { let context = context.into_context()?; - plan_create_price_observations(CreatePriceObservationsPlanInput { - context: &context, - pool_id: account_id(&pool_id, "poolId")?, - window_duration: decimal_u64(&window_duration, "windowDuration")?, - }) + transaction_plan_json(&plan_create_price_observations( + CreatePriceObservationsPlanInput { + context: &context, + pool_id: account_id(&pool_id, "poolId")?, + window_duration: decimal_u64(&window_duration, "windowDuration")?, + }, + )) } PlanRequest::CreateOraclePriceAccount { context, @@ -890,11 +903,13 @@ pub fn plan_json(value: Value) -> Result { window_duration, } => { let context = context.into_context()?; - plan_create_oracle_price_account(CreateOraclePriceAccountPlanInput { - context: &context, - pool_id: account_id(&pool_id, "poolId")?, - window_duration: decimal_u64(&window_duration, "windowDuration")?, - }) + transaction_plan_json(&plan_create_oracle_price_account( + CreateOraclePriceAccountPlanInput { + context: &context, + pool_id: account_id(&pool_id, "poolId")?, + window_duration: decimal_u64(&window_duration, "windowDuration")?, + }, + )) } PlanRequest::CreatePool { context, @@ -909,7 +924,7 @@ pub fn plan_json(value: Value) -> Result { deadline, } => { let context = context.into_context()?; - plan_create_pool(CreatePoolPlanInput { + transaction_plan_json(&plan_create_pool(CreatePoolPlanInput { context: &context, token_a_definition_id: account_id(&token_a_definition_id, "tokenADefinitionId")?, token_b_definition_id: account_id(&token_b_definition_id, "tokenBDefinitionId")?, @@ -920,7 +935,7 @@ pub fn plan_json(value: Value) -> Result { token_b_amount: decimal_u128(&token_b_amount, "tokenBAmount")?, fees: decimal_u128(&fees, "fees")?, deadline: decimal_u64(&deadline, "deadline")?, - })? + })?) } PlanRequest::AddLiquidity { context, @@ -935,7 +950,7 @@ pub fn plan_json(value: Value) -> Result { } => { let context = context.into_context()?; let (pool_id, pool) = pool.into_pool()?; - plan_add_liquidity(AddLiquidityPlanInput { + transaction_plan_json(&plan_add_liquidity(AddLiquidityPlanInput { context: &context, pool: PoolContext::new(&context, pool_id, &pool)?, user_holding_a: account_id(&user_holding_a, "userHoldingA")?, @@ -951,7 +966,7 @@ pub fn plan_json(value: Value) -> Result { "maxAmountToAddTokenB", )?, deadline: decimal_u64(&deadline, "deadline")?, - }) + })) } PlanRequest::RemoveLiquidity { context, @@ -966,7 +981,7 @@ pub fn plan_json(value: Value) -> Result { } => { let context = context.into_context()?; let (pool_id, pool) = pool.into_pool()?; - plan_remove_liquidity(RemoveLiquidityPlanInput { + transaction_plan_json(&plan_remove_liquidity(RemoveLiquidityPlanInput { context: &context, pool: PoolContext::new(&context, pool_id, &pool)?, user_holding_a: account_id(&user_holding_a, "userHoldingA")?, @@ -985,7 +1000,7 @@ pub fn plan_json(value: Value) -> Result { "minAmountToRemoveTokenB", )?, deadline: decimal_u64(&deadline, "deadline")?, - }) + })) } PlanRequest::SwapExactInput { context, @@ -998,7 +1013,7 @@ pub fn plan_json(value: Value) -> Result { } => { let context = context.into_context()?; let (pool_id, pool) = pool.into_pool()?; - plan_swap_exact_input(SwapExactInputPlanInput { + transaction_plan_json(&plan_swap_exact_input(SwapExactInputPlanInput { context: &context, pool: PoolContext::new(&context, pool_id, &pool)?, user_input_holding: account_id(&user_input_holding, "userInputHolding")?, @@ -1006,7 +1021,7 @@ pub fn plan_json(value: Value) -> Result { swap_amount_in: decimal_u128(&swap_amount_in, "swapAmountIn")?, min_amount_out: decimal_u128(&min_amount_out, "minAmountOut")?, deadline: decimal_u64(&deadline, "deadline")?, - }) + })) } PlanRequest::SwapExactOutput { context, @@ -1019,7 +1034,7 @@ pub fn plan_json(value: Value) -> Result { } => { let context = context.into_context()?; let (pool_id, pool) = pool.into_pool()?; - plan_swap_exact_output(SwapExactOutputPlanInput { + transaction_plan_json(&plan_swap_exact_output(SwapExactOutputPlanInput { context: &context, pool: PoolContext::new(&context, pool_id, &pool)?, user_input_holding: account_id(&user_input_holding, "userInputHolding")?, @@ -1027,117 +1042,17 @@ pub fn plan_json(value: Value) -> Result { exact_amount_out: decimal_u128(&exact_amount_out, "exactAmountOut")?, max_amount_in: decimal_u128(&max_amount_in, "maxAmountIn")?, deadline: decimal_u64(&deadline, "deadline")?, - }) + })) } PlanRequest::SyncReserves { context, pool } => { let context = context.into_context()?; let (pool_id, pool) = pool.into_pool()?; - plan_sync_reserves(SyncReservesPlanInput { + transaction_plan_json(&plan_sync_reserves(SyncReservesPlanInput { context: &context, pool: PoolContext::new(&context, pool_id, &pool)?, - }) + })) } - }; - - versioned(transaction_plan_json(&plan)) -} - -fn is_prepared_transaction_operation(operation: &str) -> bool { - matches!( - operation, - "prepare_create_pool_transaction" - | "prepare_add_liquidity_transaction" - | "prepare_remove_liquidity_transaction" - | "prepare_swap_exact_input_transaction" - | "prepare_swap_exact_output_transaction" - ) -} - -/// Evaluates one reusable AMM economic quote from tagged JSON. -pub fn quote_json(value: Value) -> Result { - validate_wire_schema(&value)?; - let request: QuoteRequest = serde_json::from_value(value) - .map_err(|error| invalid_request(format!("invalid quote request: {error}")))?; - versioned(match request { - QuoteRequest::ProtocolConstants => Ok(json!({ - "minimumLiquidity": MINIMUM_LIQUIDITY.to_string(), - "feeBpsDenominator": FEE_BPS_DENOMINATOR.to_string(), - "slippageBpsDenominator": SLIPPAGE_BPS_DENOMINATOR.to_string(), - "supportedFeeTiers": SUPPORTED_FEE_TIERS - .iter() - .map(u128::to_string) - .collect::>(), - })), - QuoteRequest::DeriveConfigId { amm_program_id } => Ok(json!({ - "configId": discovery::derive_config_id(amm_program_id).to_string(), - })), - QuoteRequest::InspectConfig { - amm_program_id, - config, - } => { - let config = config.into_snapshot()?; - let context = discovery::inspect_config(amm_program_id, &config)?; - Ok(amm_context_json(&context)) - } - QuoteRequest::CanonicalPair { - first_token_definition_id, - second_token_definition_id, - } => { - let pair = discovery::canonical_pair( - account_id(&first_token_definition_id, "firstTokenDefinitionId")?, - account_id(&second_token_definition_id, "secondTokenDefinitionId")?, - )?; - Ok(canonical_pair_json(pair)) - } - QuoteRequest::DerivePairReadManifest { - amm_program_id, - config, - first_token_definition_id, - second_token_definition_id, - } => { - let config = config.into_snapshot()?; - let context = discovery::inspect_config(amm_program_id, &config)?; - let manifest = discovery::derive_pair_read_manifest( - &context, - account_id(&first_token_definition_id, "firstTokenDefinitionId")?, - account_id(&second_token_definition_id, "secondTokenDefinitionId")?, - )?; - Ok(pair_read_manifest_json(manifest)) - } - QuoteRequest::InspectPair { - amm_program_id, - config, - first_token_definition_id, - second_token_definition_id, - snapshots, - } => { - let config = config.into_snapshot()?; - let context = discovery::inspect_config(amm_program_id, &config)?; - let snapshots = snapshots.into_snapshots()?; - let inspected = discovery::inspect_pair( - &context, - account_id(&first_token_definition_id, "firstTokenDefinitionId")?, - account_id(&second_token_definition_id, "secondTokenDefinitionId")?, - snapshots.as_borrowed(), - )?; - Ok(pair_inspection_json(inspected)) - } - QuoteRequest::PrepareCallerOpeningPair { - first_token_definition_id, - second_token_definition_id, - desired_price_q64_64, - fee_bps, - intent, - } => Ok(prepared_caller_opening_pair_json( - crate::prepare_caller_opening_pair( - account_id(&first_token_definition_id, "firstTokenDefinitionId")?, - account_id(&second_token_definition_id, "secondTokenDefinitionId")?, - decimal_u128(&desired_price_q64_64, "desiredPriceQ64_64")?, - decimal_u128(&fee_bps, "feeBps")?, - intent.into_intent()?, - )?, - )), - QuoteRequest::PrepareCreatePoolTransaction { + PlanRequest::PrepareCreatePoolTransaction { amm_program_id, config, snapshots, @@ -1158,7 +1073,7 @@ pub fn quote_json(value: Value) -> Result { let liquidity_holding = liquidity_holding.into_snapshot()?; let prepared = crate::prepare_create_pool_transaction(crate::CreatePoolTransactionInput { - amm_program_id, + amm_program_id: amm_program_id.into(), config: &config, pair: snapshots.as_borrowed(), first_token_definition_id: account_id( @@ -1179,7 +1094,7 @@ pub fn quote_json(value: Value) -> Result { })?; prepared_transaction_json(&prepared, create_pool_quote_json(*prepared.quote())) } - QuoteRequest::PrepareAddLiquidityTransaction { + PlanRequest::PrepareAddLiquidityTransaction { amm_program_id, config, snapshots, @@ -1201,7 +1116,7 @@ pub fn quote_json(value: Value) -> Result { let liquidity_holding = liquidity_holding.into_snapshot()?; let prepared = crate::prepare_add_liquidity_transaction(crate::AddLiquidityTransactionInput { - amm_program_id, + amm_program_id: amm_program_id.into(), pool_accounts: crate::PoolAccountSnapshots { config: &config, pair: snapshots.as_borrowed(), @@ -1225,7 +1140,7 @@ pub fn quote_json(value: Value) -> Result { })?; prepared_transaction_json(&prepared, add_liquidity_quote_json(*prepared.quote())) } - QuoteRequest::PrepareRemoveLiquidityTransaction { + PlanRequest::PrepareRemoveLiquidityTransaction { amm_program_id, config, snapshots, @@ -1246,7 +1161,7 @@ pub fn quote_json(value: Value) -> Result { let liquidity_holding = liquidity_holding.into_snapshot()?; let prepared = crate::prepare_remove_liquidity_transaction( crate::RemoveLiquidityTransactionInput { - amm_program_id, + amm_program_id: amm_program_id.into(), pool_accounts: crate::PoolAccountSnapshots { config: &config, pair: snapshots.as_borrowed(), @@ -1273,7 +1188,7 @@ pub fn quote_json(value: Value) -> Result { )?; prepared_transaction_json(&prepared, remove_liquidity_quote_json(*prepared.quote())) } - QuoteRequest::PrepareSwapExactInputTransaction { + PlanRequest::PrepareSwapExactInputTransaction { amm_program_id, config, snapshots, @@ -1292,7 +1207,7 @@ pub fn quote_json(value: Value) -> Result { let output_holding = output_holding.into_snapshot()?; let prepared = crate::prepare_swap_exact_input_transaction( crate::SwapExactInputTransactionInput { - amm_program_id, + amm_program_id: amm_program_id.into(), pool_accounts: crate::PoolAccountSnapshots { config: &config, pair: snapshots.as_borrowed(), @@ -1315,7 +1230,7 @@ pub fn quote_json(value: Value) -> Result { )?; prepared_transaction_json(&prepared, swap_quote_json(*prepared.quote())) } - QuoteRequest::PrepareSwapExactOutputTransaction { + PlanRequest::PrepareSwapExactOutputTransaction { amm_program_id, config, snapshots, @@ -1334,7 +1249,7 @@ pub fn quote_json(value: Value) -> Result { let output_holding = output_holding.into_snapshot()?; let prepared = crate::prepare_swap_exact_output_transaction( crate::SwapExactOutputTransactionInput { - amm_program_id, + amm_program_id: amm_program_id.into(), pool_accounts: crate::PoolAccountSnapshots { config: &config, pair: snapshots.as_borrowed(), @@ -1357,6 +1272,93 @@ pub fn quote_json(value: Value) -> Result { )?; prepared_transaction_json(&prepared, swap_quote_json(*prepared.quote())) } + }) +} + +/// Evaluates one reusable AMM economic quote from tagged JSON. +pub fn quote_json(value: Value) -> Result { + validate_wire_schema(&value)?; + let request: QuoteRequest = serde_json::from_value(value) + .map_err(|error| invalid_request(format!("invalid quote request: {error}")))?; + versioned(match request { + QuoteRequest::ProtocolConstants => Ok(json!({ + "minimumLiquidity": MINIMUM_LIQUIDITY.to_string(), + "feeBpsDenominator": FEE_BPS_DENOMINATOR.to_string(), + "slippageBpsDenominator": SLIPPAGE_BPS_DENOMINATOR.to_string(), + "supportedFeeTiers": SUPPORTED_FEE_TIERS + .iter() + .map(u128::to_string) + .collect::>(), + })), + QuoteRequest::DeriveConfigId { amm_program_id } => Ok(json!({ + "configId": discovery::derive_config_id(amm_program_id.into()).to_string(), + })), + QuoteRequest::InspectConfig { + amm_program_id, + config, + } => { + let config = config.into_snapshot()?; + let context = discovery::inspect_config(amm_program_id.into(), &config)?; + Ok(amm_context_json(&context)) + } + QuoteRequest::CanonicalPair { + first_token_definition_id, + second_token_definition_id, + } => { + let pair = discovery::canonical_pair( + account_id(&first_token_definition_id, "firstTokenDefinitionId")?, + account_id(&second_token_definition_id, "secondTokenDefinitionId")?, + )?; + Ok(canonical_pair_json(pair)) + } + QuoteRequest::DerivePairReadManifest { + amm_program_id, + config, + first_token_definition_id, + second_token_definition_id, + } => { + let config = config.into_snapshot()?; + let context = discovery::inspect_config(amm_program_id.into(), &config)?; + let manifest = discovery::derive_pair_read_manifest( + &context, + account_id(&first_token_definition_id, "firstTokenDefinitionId")?, + account_id(&second_token_definition_id, "secondTokenDefinitionId")?, + )?; + Ok(pair_read_manifest_json(manifest)) + } + QuoteRequest::InspectPair { + amm_program_id, + config, + first_token_definition_id, + second_token_definition_id, + snapshots, + } => { + let config = config.into_snapshot()?; + let context = discovery::inspect_config(amm_program_id.into(), &config)?; + let snapshots = snapshots.into_snapshots()?; + let inspected = discovery::inspect_pair( + &context, + account_id(&first_token_definition_id, "firstTokenDefinitionId")?, + account_id(&second_token_definition_id, "secondTokenDefinitionId")?, + snapshots.as_borrowed(), + )?; + Ok(pair_inspection_json(inspected)) + } + QuoteRequest::PrepareCallerOpeningPair { + first_token_definition_id, + second_token_definition_id, + desired_price_q64_64, + fee_bps, + intent, + } => Ok(prepared_caller_opening_pair_json( + crate::prepare_caller_opening_pair( + account_id(&first_token_definition_id, "firstTokenDefinitionId")?, + account_id(&second_token_definition_id, "secondTokenDefinitionId")?, + decimal_u128(&desired_price_q64_64, "desiredPriceQ64_64")?, + decimal_u128(&fee_bps, "feeBps")?, + intent.into_intent()?, + )?, + )), QuoteRequest::PrepareMinimumOpeningPair { desired_price_q64_64, fee_bps, @@ -1429,7 +1431,7 @@ pub fn quote_json(value: Value) -> Result { fee_bps, } => { let config = config.into_snapshot()?; - let context = AmmContext::from_config_account(amm_program_id, &config)?; + let context = AmmContext::from_config_account(amm_program_id.into(), &config)?; let token_a_definition = token_a_definition.into_snapshot()?; let token_b_definition = token_b_definition.into_snapshot()?; let token_a = ValidatedFungibleDefinition::new(&context, &token_a_definition)?; @@ -1454,7 +1456,7 @@ pub fn quote_json(value: Value) -> Result { fee_bps, } => { let config = config.into_snapshot()?; - let context = AmmContext::from_config_account(amm_program_id, &config)?; + let context = AmmContext::from_config_account(amm_program_id.into(), &config)?; let token_a_definition = token_a_definition.into_snapshot()?; let token_b_definition = token_b_definition.into_snapshot()?; let token_a = ValidatedFungibleDefinition::new(&context, &token_a_definition)?; @@ -1763,7 +1765,7 @@ fn transaction_plan_json(plan: &TransactionPlan) -> Result { Ok(json!({ "instruction": plan.instruction_name(), "instructionArgs": instruction_args_json(plan.instruction()), - "programId": plan.program_id(), + "programId": program_id_hex(plan.program_id()), "accounts": accounts, "affectedAccountIds": plan .affected_account_ids() @@ -1781,8 +1783,8 @@ fn instruction_args_json(instruction: &Instruction) -> Value { twap_oracle_program_id, authority, } => json!({ - "tokenProgramId": token_program_id, - "twapOracleProgramId": twap_oracle_program_id, + "tokenProgramId": program_id_hex(*token_program_id), + "twapOracleProgramId": program_id_hex(*twap_oracle_program_id), "authority": authority.to_string(), }), Instruction::UpdateConfig { @@ -1790,8 +1792,8 @@ fn instruction_args_json(instruction: &Instruction) -> Value { twap_oracle_program_id, new_authority, } => json!({ - "tokenProgramId": token_program_id, - "twapOracleProgramId": twap_oracle_program_id, + "tokenProgramId": token_program_id.map(program_id_hex), + "twapOracleProgramId": twap_oracle_program_id.map(program_id_hex), "newAuthority": new_authority.map(|authority| authority.to_string()), }), Instruction::CreatePriceObservations { window_duration } @@ -1944,10 +1946,10 @@ fn pool_update_json(pool: PoolUpdate) -> Value { fn amm_context_json(context: &AmmContext) -> Value { json!({ - "ammProgramId": context.amm_program_id, + "ammProgramId": program_id_hex(context.amm_program_id), "configId": context.config_id().to_string(), - "tokenProgramId": context.token_program_id(), - "twapOracleProgramId": context.twap_oracle_program_id(), + "tokenProgramId": program_id_hex(context.token_program_id()), + "twapOracleProgramId": program_id_hex(context.twap_oracle_program_id()), "authority": context.config.authority.to_string(), }) } @@ -2255,9 +2257,46 @@ fn oracle_price_quote_json(quote: OraclePriceAccountQuote) -> Value { }) } +fn parse_program_id(value: &str) -> Result { + 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 +} + fn account_id(value: &str, field: &str) -> Result { - AccountId::from_str(value) - .map_err(|error| invalid_request(format!("{field} is not a valid account ID: {error}"))) + let account_id = AccountId::from_str(value) + .map_err(|error| invalid_request(format!("{field} is not a valid account ID: {error}")))?; + if account_id.to_string() != value { + return Err(invalid_request(format!( + "{field} must use canonical base58 encoding" + ))); + } + Ok(account_id) } fn decimal_u128(value: &str, field: &str) -> Result { diff --git a/programs/amm/client/tests/common/mod.rs b/programs/amm/client/tests/common/mod.rs new file mode 100644 index 0000000..e4dfc60 --- /dev/null +++ b/programs/amm/client/tests/common/mod.rs @@ -0,0 +1,11 @@ +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 +} diff --git a/programs/amm/client/tests/ffi_contract.rs b/programs/amm/client/tests/ffi_contract.rs index a8c9d38..72a31ee 100644 --- a/programs/amm/client/tests/ffi_contract.rs +++ b/programs/amm/client/tests/ffi_contract.rs @@ -3,6 +3,8 @@ reason = "contract tests call the exported C ABI and release its owned pointers" )] +mod common; + use std::ffi::{c_char, CStr, CString}; use amm_client::{amm_client_free, amm_client_plan, amm_client_quote, wire::WIRE_SCHEMA}; @@ -10,6 +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 nssa_core::{ account::{Account, AccountId, Data, Nonce}, program::ProgramId, @@ -45,7 +48,7 @@ fn call_json(operation: Operation, request: &Value) -> Value { fn snapshot(id: AccountId, account: &Account) -> Value { json!({ "id": id.to_string(), - "programOwner": account.program_owner, + "programOwner": program_id_hex(account.program_owner), "balance": account.balance.to_string(), "nonce": account.nonce.0.to_string(), "data": hex(account.data.as_ref()), @@ -147,6 +150,31 @@ 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]); + let authority = AccountId::new([44; 32]).to_string(); + + for invalid in [ + json!([1, 1, 1, 1, 1, 1, 1, 1]), + json!(canonical.to_uppercase()), + ] { + let response = call_json( + amm_client_plan, + &json!({ + "operation": "initialize", + "ammProgramId": invalid, + "tokenProgramId": canonical, + "twapOracleProgramId": canonical, + "authority": authority, + }), + ); + + assert_eq!(response["ok"], false); + assert_eq!(response["error"]["code"], "invalid_request"); + } +} + #[test] fn successful_plan_preserves_u64_above_javascript_range_in_guest_words() { let amm_program_id: ProgramId = [11; 8]; @@ -160,9 +188,9 @@ fn successful_plan_preserves_u64_above_javascript_range_in_guest_words() { &json!({ "operation": "create_price_observations", "context": { - "ammProgramId": amm_program_id, - "tokenProgramId": token_program_id, - "twapOracleProgramId": twap_oracle_program_id, + "ammProgramId": program_id_hex(amm_program_id), + "tokenProgramId": program_id_hex(token_program_id), + "twapOracleProgramId": program_id_hex(twap_oracle_program_id), "authority": authority.to_string(), }, "poolId": pool_id.to_string(), @@ -175,7 +203,10 @@ fn successful_plan_preserves_u64_above_javascript_range_in_guest_words() { response["value"]["instruction"], "create_price_observations" ); - assert_eq!(response["value"]["programId"], json!(amm_program_id)); + assert_eq!( + response["value"]["programId"], + program_id_hex(amm_program_id) + ); assert!(response["value"]["accounts"].is_array()); let words: Vec = serde_json::from_value(response["value"]["instructionWords"].clone()) .expect("instruction words must be u32 JSON numbers"); @@ -232,7 +263,7 @@ fn successful_quote_preserves_u128_above_javascript_range_as_decimal() { amm_client_quote, &json!({ "operation": "create_pool", - "ammProgramId": amm_program_id, + "ammProgramId": program_id_hex(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")), @@ -258,7 +289,7 @@ fn successful_quote_preserves_u128_above_javascript_range_as_decimal() { amm_client_quote, &json!({ "operation": "prepare_create_pool", - "ammProgramId": amm_program_id, + "ammProgramId": program_id_hex(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")), @@ -311,7 +342,7 @@ fn swap_quote_rejects_unrelated_output_holding() { amm_client_quote, &json!({ "operation": "preview_swap_exact_input", - "ammProgramId": amm_program_id, + "ammProgramId": program_id_hex(amm_program_id), "config": snapshot( compute_config_pda(amm_program_id), &account(amm_program_id, Data::from(&config)), diff --git a/programs/amm/client/tests/wire_discovery_intent_contract.rs b/programs/amm/client/tests/wire_discovery_intent_contract.rs index 498d9fd..0420a44 100644 --- a/programs/amm/client/tests/wire_discovery_intent_contract.rs +++ b/programs/amm/client/tests/wire_discovery_intent_contract.rs @@ -1,9 +1,12 @@ +mod common; + use amm_client::wire::{quote_json, WIRE_SCHEMA}; use amm_core::{ compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda, 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 nssa_core::{ account::{Account, AccountId, Data, Nonce}, program::ProgramId, @@ -108,7 +111,7 @@ impl PairIds { fn inspect_request(&self, snapshots: Value) -> Value { json!({ "operation": "inspect_pair", - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), "config": config_snapshot(), "firstTokenDefinitionId": self.first_token_id.to_string(), "secondTokenDefinitionId": self.second_token_id.to_string(), @@ -202,7 +205,7 @@ impl PairIds { fn snapshot(id: AccountId, account: &Account) -> Value { json!({ "id": id.to_string(), - "programOwner": account.program_owner, + "programOwner": program_id_hex(account.program_owner), "balance": account.balance.to_string(), "nonce": account.nonce.0.to_string(), "data": account @@ -229,7 +232,7 @@ fn discovery_operations_return_exact_string_account_ids() { let config_id = quote_json(json!({ "operation": "derive_config_id", - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), })) .expect("legacy schema-less request remains accepted"); assert_eq!(config_id["schema"], WIRE_SCHEMA); @@ -242,16 +245,19 @@ fn discovery_operations_return_exact_string_account_ids() { let inspected = quote_json(json!({ "schema": WIRE_SCHEMA, "operation": "inspect_config", - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), "config": config.clone(), })) .expect("config must inspect"); assert_eq!(inspected["schema"], WIRE_SCHEMA); - assert_eq!(inspected["ammProgramId"], json!(AMM_PROGRAM_ID)); - assert_eq!(inspected["tokenProgramId"], json!(TOKEN_PROGRAM_ID)); + assert_eq!(inspected["ammProgramId"], program_id_hex(AMM_PROGRAM_ID)); + assert_eq!( + inspected["tokenProgramId"], + program_id_hex(TOKEN_PROGRAM_ID) + ); assert_eq!( inspected["twapOracleProgramId"], - json!(TWAP_ORACLE_PROGRAM_ID) + program_id_hex(TWAP_ORACLE_PROGRAM_ID) ); assert_eq!(inspected["authority"], AccountId::new([9; 32]).to_string()); @@ -266,7 +272,7 @@ fn discovery_operations_return_exact_string_account_ids() { let manifest = quote_json(json!({ "operation": "derive_pair_read_manifest", - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), "config": config, "firstTokenDefinitionId": first_token_id.to_string(), "secondTokenDefinitionId": second_token_id.to_string(), diff --git a/programs/amm/client/tests/wire_prepare_contract.rs b/programs/amm/client/tests/wire_prepare_contract.rs index a8ba37b..f4d2e68 100644 --- a/programs/amm/client/tests/wire_prepare_contract.rs +++ b/programs/amm/client/tests/wire_prepare_contract.rs @@ -1,8 +1,11 @@ +mod common; + use amm_client::{maximum_guard_amount, minimum_guard_amount, wire::quote_json, SlippageTolerance}; 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 nssa_core::{ account::{Account, AccountId, Data, Nonce}, program::ProgramId, @@ -26,7 +29,7 @@ fn account(program_owner: ProgramId, data: Data) -> Account { fn snapshot(id: AccountId, account: &Account) -> Value { json!({ "id": id.to_string(), - "programOwner": account.program_owner, + "programOwner": program_id_hex(account.program_owner), "balance": account.balance.to_string(), "nonce": account.nonce.0.to_string(), "data": account @@ -99,7 +102,7 @@ impl WireFixture { fees: FEE_TIER_BPS_30, }; let state = json!({ - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), "config": config, "snapshot": { "pool": snapshot(pool_id, &account(AMM_PROGRAM_ID, Data::from(&pool))), @@ -161,7 +164,7 @@ fn prepare_wire_operations_return_lossless_instruction_args() { let create = quote_json(json!({ "operation": "prepare_create_pool", - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(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)), diff --git a/programs/amm/client/tests/wire_transaction_contract.rs b/programs/amm/client/tests/wire_transaction_contract.rs index 2359f84..f89020f 100644 --- a/programs/amm/client/tests/wire_transaction_contract.rs +++ b/programs/amm/client/tests/wire_transaction_contract.rs @@ -1,9 +1,12 @@ +mod common; + use amm_client::wire::{plan_json, quote_json}; use amm_core::{ compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda, 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 nssa_core::{ account::{Account, AccountId, Data, Nonce}, program::ProgramId, @@ -52,7 +55,7 @@ fn holding(definition_id: AccountId, balance: u128) -> Account { fn snapshot(id: AccountId, account: &Account) -> Value { json!({ "id": id.to_string(), - "programOwner": account.program_owner, + "programOwner": program_id_hex(account.program_owner), "balance": account.balance.to_string(), "nonce": account.nonce.0.to_string(), "data": account @@ -209,7 +212,7 @@ impl TransactionFixture { fn active_common(&self, operation: &str) -> Value { json!({ "operation": operation, - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), "config": self.config.clone(), "snapshots": self.active_snapshots.clone(), "firstTokenDefinitionId": self.first_token_id.to_string(), @@ -226,7 +229,7 @@ impl TransactionFixture { fn swap_common(&self, operation: &str) -> Value { json!({ "operation": operation, - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), "config": self.config.clone(), "snapshots": self.active_snapshots.clone(), "inputTokenDefinitionId": self.first_token_id.to_string(), @@ -321,9 +324,9 @@ fn five_transaction_operations_emit_exact_plans_and_task_artifacts() { let fixture = TransactionFixture::new(); let second_amount = LARGE.checked_mul(2).expect("test amount fits"); - let create = quote_json(json!({ + let create = plan_json(json!({ "operation": "prepare_create_pool_transaction", - "ammProgramId": AMM_PROGRAM_ID, + "ammProgramId": program_id_hex(AMM_PROGRAM_ID), "config": fixture.config.clone(), "snapshots": fixture.missing_snapshots.clone(), "firstTokenDefinitionId": fixture.first_token_id.to_string(), @@ -379,11 +382,10 @@ fn five_transaction_operations_emit_exact_plans_and_task_artifacts() { let mut add_request = fixture.active_common("prepare_add_liquidity_transaction"); insert(&mut add_request, "maxFirstAmount", json!("100")); insert(&mut add_request, "maxSecondAmount", json!("400")); - let add = quote_json(add_request.clone()).expect("add transaction must prepare"); - assert_eq!( - plan_json(add_request).expect("plan entrypoint must prepare task transactions"), - add - ); + let quote_error = quote_json(add_request.clone()) + .expect_err("quote endpoint must reject transaction preparation"); + assert_eq!(quote_error.code(), "invalid_request"); + let add = plan_json(add_request).expect("add transaction must prepare"); assert_common_contract(&add, "add_liquidity", false); assert_eq!(add["callerAmounts"]["first"], "100"); assert_eq!(add["callerAmounts"]["second"], "200"); @@ -428,7 +430,7 @@ fn five_transaction_operations_emit_exact_plans_and_task_artifacts() { let mut remove_request = fixture.active_common("prepare_remove_liquidity_transaction"); insert(&mut remove_request, "removeLiquidityAmount", json!("500")); - let remove = quote_json(remove_request).expect("remove transaction must prepare"); + let remove = plan_json(remove_request).expect("remove transaction must prepare"); assert_common_contract(&remove, "remove_liquidity", false); assert_eq!(remove["callerAmounts"]["first"], "125"); assert_eq!(remove["callerAmounts"]["second"], "250"); @@ -469,8 +471,7 @@ fn five_transaction_operations_emit_exact_plans_and_task_artifacts() { let mut exact_input_request = fixture.swap_common("prepare_swap_exact_input_transaction"); insert(&mut exact_input_request, "amountIn", json!("100")); - let exact_input = - quote_json(exact_input_request).expect("exact-input transaction must prepare"); + let exact_input = plan_json(exact_input_request).expect("exact-input transaction must prepare"); assert_common_contract(&exact_input, "swap_exact_input", true); assert_eq!(exact_input["callerAmounts"]["first"], "100"); assert_eq!( @@ -503,7 +504,7 @@ fn five_transaction_operations_emit_exact_plans_and_task_artifacts() { let mut exact_output_request = fixture.swap_common("prepare_swap_exact_output_transaction"); insert(&mut exact_output_request, "exactAmountOut", json!("100")); let exact_output = - quote_json(exact_output_request).expect("exact-output transaction must prepare"); + plan_json(exact_output_request).expect("exact-output transaction must prepare"); assert_common_contract(&exact_output, "swap_exact_output", true); assert_eq!(exact_output["callerAmounts"]["second"], "100"); match decode_instruction(&exact_output) { @@ -560,6 +561,6 @@ fn transaction_wire_rejects_expected_fee_mismatch() { insert(&mut request, "maxSecondAmount", json!("400")); insert(&mut request, "expectedFeeBps", json!("100")); - let error = quote_json(request).expect_err("wrong expected fee must fail"); + let error = plan_json(request).expect_err("wrong expected fee must fail"); assert_eq!(error.code(), "fee_mismatch"); }