feat(amm-client): freeze wire contract

This commit is contained in:
Ricardo Guilherme Schmidt
2026-08-10 11:22:44 -03:00
parent 25fc9c3cc3
commit 9d356cfa31
9 changed files with 454 additions and 360 deletions
+6 -6
View File
@@ -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.
+22 -19
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 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.
+6 -6
View File
@@ -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
+334 -295
View File
@@ -90,24 +90,42 @@ impl From<TransactionError> for WireError {
}
}
#[derive(Clone, Copy, Deserialize)]
#[serde(try_from = "String")]
struct ProgramIdInput(ProgramId);
impl From<ProgramIdInput> for ProgramId {
fn from(value: ProgramIdInput) -> Self {
value.0
}
}
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 {
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<ProgramId>,
token_program_id: Option<ProgramIdInput>,
#[serde(rename = "twapOracleProgramId")]
twap_oracle_program_id: Option<ProgramId>,
twap_oracle_program_id: Option<ProgramIdInput>,
#[serde(rename = "newAuthority")]
new_authority: Option<String>,
},
@@ -208,24 +226,137 @@ enum PlanRequest {
context: ContextInput,
pool: PoolInput,
},
PrepareCreatePoolTransaction {
#[serde(rename = "ammProgramId")]
amm_program_id: ProgramIdInput,
config: AccountSnapshotInput,
snapshots: Box<PairReadSnapshotsInput>,
#[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<PairReadSnapshotsInput>,
#[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<String>,
deadline: String,
},
PrepareRemoveLiquidityTransaction {
#[serde(rename = "ammProgramId")]
amm_program_id: ProgramIdInput,
config: AccountSnapshotInput,
snapshots: Box<PairReadSnapshotsInput>,
#[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<String>,
deadline: String,
},
PrepareSwapExactInputTransaction {
#[serde(rename = "ammProgramId")]
amm_program_id: ProgramIdInput,
config: AccountSnapshotInput,
snapshots: Box<PairReadSnapshotsInput>,
#[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<String>,
deadline: String,
},
PrepareSwapExactOutputTransaction {
#[serde(rename = "ammProgramId")]
amm_program_id: ProgramIdInput,
config: AccountSnapshotInput,
snapshots: Box<PairReadSnapshotsInput>,
#[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<String>,
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<AmmContext, WireError> {
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<PairReadSnapshotsInput>,
#[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<PairReadSnapshotsInput>,
#[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<String>,
deadline: String,
},
PrepareRemoveLiquidityTransaction {
#[serde(rename = "ammProgramId")]
amm_program_id: ProgramId,
config: AccountSnapshotInput,
snapshots: Box<PairReadSnapshotsInput>,
#[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<String>,
deadline: String,
},
PrepareSwapExactInputTransaction {
#[serde(rename = "ammProgramId")]
amm_program_id: ProgramId,
config: AccountSnapshotInput,
snapshots: Box<PairReadSnapshotsInput>,
#[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<String>,
deadline: String,
},
PrepareSwapExactOutputTransaction {
#[serde(rename = "ammProgramId")]
amm_program_id: ProgramId,
config: AccountSnapshotInput,
snapshots: Box<PairReadSnapshotsInput>,
#[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<String>,
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<Value, WireError> {
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<Value, WireError> {
.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<Value, WireError> {
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<Value, WireError> {
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<Value, WireError> {
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<Value, WireError> {
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<Value, WireError> {
} => {
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<Value, WireError> {
"maxAmountToAddTokenB",
)?,
deadline: decimal_u64(&deadline, "deadline")?,
})
}))
}
PlanRequest::RemoveLiquidity {
context,
@@ -966,7 +981,7 @@ pub fn plan_json(value: Value) -> Result<Value, WireError> {
} => {
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<Value, WireError> {
"minAmountToRemoveTokenB",
)?,
deadline: decimal_u64(&deadline, "deadline")?,
})
}))
}
PlanRequest::SwapExactInput {
context,
@@ -998,7 +1013,7 @@ pub fn plan_json(value: Value) -> Result<Value, WireError> {
} => {
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<Value, WireError> {
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<Value, WireError> {
} => {
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<Value, WireError> {
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<Value, WireError> {
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::<Vec<_>>(),
})),
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<Value, WireError> {
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<Value, WireError> {
})?;
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<Value, WireError> {
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<Value, WireError> {
})?;
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<Value, WireError> {
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<Value, WireError> {
)?;
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<Value, WireError> {
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<Value, WireError> {
)?;
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<Value, WireError> {
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<Value, WireError> {
)?;
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<Value, WireError> {
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::<Vec<_>>(),
})),
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<Value, WireError> {
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<Value, WireError> {
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<Value, WireError> {
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<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
}
fn account_id(value: &str, field: &str) -> Result<AccountId, WireError> {
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<u128, WireError> {
+11
View File
@@ -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
}
+39 -8
View File
@@ -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<u32> = 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)),
@@ -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(),
@@ -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)),
@@ -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");
}