Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions artifacts/amm-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
{
"name": "initialize",
"accounts": [
{
"name": "owner",
"writable": true,
"signer": true,
"init": false
},
{
"name": "config",
"writable": true,
Expand All @@ -13,6 +19,15 @@
}
],
"args": [
{
"name": "nonce",
"type": {
"array": [
"u8",
32
]
}
},
{
"name": "token_program_id",
"type": "program_id"
Expand Down
6 changes: 3 additions & 3 deletions modules/amm/ffi/src/api/admin.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use amm_core::{compute_config_pda, Instruction};
use amm_core::Instruction;
use serde_json::{json, Value};

use super::{config::load_config, TransferOwnershipPlanRequest};
Expand All @@ -13,7 +13,7 @@ pub(super) fn transfer_ownership_plan(
) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let new_authority = account_id_from_hex(&request.new_authority_id, "new authority id")?;
let Ok(config) = load_config(amm_program, &request.config) else {
let Ok((config_id, config)) = load_config(amm_program, &request.config) else {
return Err(String::from("config_unavailable"));
};

Expand All @@ -23,7 +23,7 @@ pub(super) fn transfer_ownership_plan(
// Fixed IDL account order for UpdateConfig: the config account (mut, updated in place, not a
// signer) and the current admin authority (signs). `new_authority` is instruction data, not
// an account.
let account_ids = [compute_config_pda(amm_program), config.authority];
let account_ids = [config_id, config.authority];
let signing_requirements = [false, true];

Ok(json!({
Expand Down
40 changes: 29 additions & 11 deletions modules/amm/ffi/src/api/config.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
use amm_core::{compute_config_pda, AmmConfig};
use lee_core::{account::Account, program::ProgramId};
use lee_core::{
account::{Account, AccountId},
program::ProgramId,
};
use serde_json::{json, Value};

use super::{ConfigAccountRequest, ConfigIdRequest};
use crate::account::{
account_id_hex, decode_account, parse_program_id, program_id_base58, AccountRead,
account_id_hex, decode_account, parse_base58_id, parse_hex_32, parse_program_id,
program_id_base58, AccountRead,
};

pub(super) fn config_id(request: ConfigIdRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let owner = parse_base58_id(&request.owner, "owner")?;
// An omitted / empty nonce selects the owner's default (all-zero) instance.
let nonce = if request.nonce.is_empty() {
[0_u8; 32]
} else {
parse_hex_32(&request.nonce, "nonce")?
};
Ok(json!({
"status": "ok",
"configId": account_id_hex(compute_config_pda(amm_program)),
"configId": account_id_hex(compute_config_pda(amm_program, owner, nonce)),
}))
}

Expand All @@ -21,27 +32,34 @@ pub(super) fn config_id(request: ConfigIdRequest) -> Result<Value, String> {
/// `config_id` for address derivation.
pub(super) fn config_account(request: ConfigAccountRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let Ok(config) = load_config(amm_program, &request.config) else {
let Ok((config_id, config)) = load_config(amm_program, &request.config) else {
return Ok(json!({ "status": "error", "error": "config_unavailable" }));
};
Ok(json!({
"status": "ok",
"error": "",
"configId": compute_config_pda(amm_program).to_string(),
"configId": config_id.to_string(),
"ammProgramId": program_id_base58(amm_program),
"authority": config.authority.to_string(),
"tokenProgramId": program_id_base58(config.token_program_id),
"twapOracleProgramId": program_id_base58(config.twap_oracle_program_id),
}))
}

pub(super) fn load_config(amm_program: ProgramId, read: &AccountRead) -> Result<AmmConfig, String> {
/// Decodes and validates a passed AMM config account, returning its id (the namespace root
/// callers derive pools under) alongside the decoded config. Since the config PDA is now
/// namespaced by `(owner, nonce)`, the id can no longer be recomputed here without those
/// inputs — the caller-supplied account's id IS the namespace root. Program ownership and a
/// non-default, parseable account are still enforced.
pub(super) fn load_config(
amm_program: ProgramId,
read: &AccountRead,
) -> Result<(AccountId, AmmConfig), String> {
let (id, account) = decode_account(read)?;
if id != compute_config_pda(amm_program)
|| account.program_owner != amm_program
|| account == Account::default()
{
if account.program_owner != amm_program || account == Account::default() {
return Err(String::from("AMM config is unavailable"));
}
AmmConfig::try_from(&account.data).map_err(|_| String::from("AMM config is invalid"))
let config =
AmmConfig::try_from(&account.data).map_err(|_| String::from("AMM config is invalid"))?;
Ok((id, config))
}
2 changes: 1 addition & 1 deletion modules/amm/ffi/src/api/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::account::{account_id_from_hex, decode_account, parse_program_id, Acco
/// requested id with no returned row as unresolved/unavailable.
pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let Ok(config) = load_config(amm_program, &request.config) else {
let Ok((_, config)) = load_config(amm_program, &request.config) else {
return Ok(json!({ "status": "error", "code": "config_unavailable", "tokens": [] }));
};

Expand Down
25 changes: 16 additions & 9 deletions modules/amm/ffi/src/api/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,13 @@ mod tests {
}
}

/// The namespace root (config PDA id) the plan tests derive pools under. A fixed
/// `(owner, nonce)` instance is enough — the tests only need it to be consistent between
/// `valid_config` and the expected `compute_pool_pda`.
fn config_id(amm: lee_core::program::ProgramId) -> AccountId {
compute_config_pda(amm, AccountId::new([0x07; 32]), [0; 32])
}

/// A valid AMM config account read so `derive_pair` succeeds in plan tests.
fn valid_config(amm: lee_core::program::ProgramId) -> AccountRead {
let token_program = parse_program_id(&"01".repeat(32)).unwrap();
Expand All @@ -669,7 +676,7 @@ mod tests {
}),
..Account::default()
};
account_read(compute_config_pda(amm), &account)
account_read(config_id(amm), &account)
}

#[test]
Expand Down Expand Up @@ -797,8 +804,8 @@ mod tests {
.map(|value| value.as_bool().unwrap())
.collect();

let pool = compute_pool_pda(amm, canonical_a, canonical_b);
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
let pool = compute_pool_pda(amm, config_id(amm), canonical_a, canonical_b);
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(ids[1], account_id_hex(pool));
// Canonical vaults, in canonical order.
assert_eq!(
Expand Down Expand Up @@ -1062,10 +1069,10 @@ mod tests {
serde_json::json!(words.iter().map(|w| u64::from(*w)).collect::<Vec<u64>>())
};
let assert_aligned = |ids: &[String], instruction: &serde_json::Value| {
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(
ids[1],
account_id_hex(compute_pool_pda(amm, token_a, token_b))
account_id_hex(compute_pool_pda(amm, config_id(amm), token_a, token_b))
);
assert_eq!(ids[2], account_id_hex(vault_a));
assert_eq!(ids[3], account_id_hex(vault_b));
Expand Down Expand Up @@ -1363,10 +1370,10 @@ mod tests {
]);
let assert_aligned =
|ids: &[String], instruction: &serde_json::Value, signers: &serde_json::Value| {
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(
ids[1],
account_id_hex(compute_pool_pda(amm, token_a, token_b))
account_id_hex(compute_pool_pda(amm, config_id(amm), token_a, token_b))
);
assert_eq!(ids[2], account_id_hex(vault_a));
assert_eq!(ids[3], account_id_hex(vault_b));
Expand Down Expand Up @@ -1475,10 +1482,10 @@ mod tests {
.map(|v| v.as_str().unwrap().to_string())
.collect::<Vec<String>>();
assert_eq!(ids.len(), 6);
assert_eq!(ids[0], account_id_hex(compute_config_pda(amm)));
assert_eq!(ids[0], account_id_hex(config_id(amm)));
assert_eq!(
ids[1],
account_id_hex(compute_pool_pda(amm, token_a, token_b))
account_id_hex(compute_pool_pda(amm, config_id(amm), token_a, token_b))
);
assert_eq!(ids[2], account_id_hex(vault_a)); // pool's stored vaults
assert_eq!(ids[3], account_id_hex(vault_b));
Expand Down
8 changes: 3 additions & 5 deletions modules/amm/ffi/src/api/pair.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use amm_core::{
compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda,
compute_vault_pda,
compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda, compute_vault_pda,
};
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
use lee_core::{account::AccountId, program::ProgramId};
Expand Down Expand Up @@ -57,9 +56,8 @@ pub(super) fn derive_pair(
token_b: AccountId,
config_read: &AccountRead,
) -> Result<PairIds, String> {
let config_id = compute_config_pda(amm_program);
let config = load_config(amm_program, config_read)?;
let pool = compute_pool_pda(amm_program, token_a, token_b);
let (config_id, config) = load_config(amm_program, config_read)?;
let pool = compute_pool_pda(amm_program, config_id, token_a, token_b);
Ok(PairIds {
token_a,
token_b,
Expand Down
10 changes: 10 additions & 0 deletions modules/amm/ffi/src/api/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ use serde::Deserialize;

use crate::account::AccountRead;

/// Derives a namespaced AMM instance's config PDA. `owner` (base58 account id) and `nonce`
/// (64-char hex; omitted / empty ⇒ the all-zero default, i.e. the owner's default instance)
/// select the instance. This is the one op that supplies the namespace directly — every other
/// op derives it from the config account it is passed.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ConfigIdRequest {
pub amm_program_id: String,
pub owner: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing C++ callers still send the old request shapes. AmmModuleImpl::readConfig() calls amm_config_id with only ammProgramId, so making owner required causes every config read to fail with ``invalid request JSON: missing field owner```. Likewise, swapExactInQuote()` and `swapExactOutQuote()` call `amm_pool_id` without the newly required `config`, so quotes fail before reading a pool. Pool creation, swaps, liquidity operations, and admin operations also use these calls. Please wire the selected instance's owner/nonce or config ID through the module and update both sets of JSON callers in this PR. The Rust tests construct the new request fields directly, so they do not exercise these production callers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also lines 134–136

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is addressed in #358

#[serde(default)]
pub nonce: String,
}

/// Decodes the singleton AMM config account. `config` is the read of the config PDA the module
Expand Down Expand Up @@ -124,6 +131,9 @@ pub struct PoolIdRequest {
pub amm_program_id: String,
pub token_in_id: String,
pub token_out_id: String,
/// AMM config account read — its id is the namespace root the pool PDA is derived under
/// (pools are namespaced by config since the namespacing change).
pub config: AccountRead,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
Expand Down
39 changes: 32 additions & 7 deletions modules/amm/ffi/src/api/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,22 @@ pub(super) fn resolve_pool(request: ResolvePoolRequest) -> Result<Value, String>
}))
}

/// Derives the pool PDA for a swap pair (tokens in either order). Config-free —
/// the pool address depends only on the AMM program id and the two token ids, so
/// a caller that just needs to read the pool doesn't have to load the config
/// first (unlike `swap_pair`, which also derives the config-dependent tick PDA).
/// Derives the pool PDA for a swap pair (tokens in either order). The pool is now namespaced by
/// its AMM instance, so the caller supplies the instance's `config` account: its id is the
/// namespace root the pool PDA is derived under (only the config account is decoded — unlike
/// `swap_pair`, no config-dependent tick PDA is derived, so `AmmConfig` need not be valid).
pub(super) fn pool_id(request: PoolIdRequest) -> Result<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
let token_in = account_id_from_hex(&request.token_in_id, "token in id")?;
let token_out = account_id_from_hex(&request.token_out_id, "token out id")?;
if token_in == token_out {
return Err(String::from("same_token_pair"));
}
let (config_id, _) = decode_account(&request.config)?;
let (token_a, token_b) = canonical_pair(token_in, token_out);
Ok(json!({ "poolId": account_id_hex(compute_pool_pda(amm_program, token_a, token_b)) }))
Ok(json!({
"poolId": account_id_hex(compute_pool_pda(amm_program, config_id, token_a, token_b))
}))
}

/// Prices a `SwapExactInput`: orients the pool's reserves to the requested in/out
Expand Down Expand Up @@ -827,41 +830,63 @@ mod tests {
);
}

/// A config account read whose id is `config_id` (the namespace root pool derivation uses).
/// `pool_id` only decodes the account for its id, so the config bytes/owner are irrelevant.
fn config_read(config_id: AccountId) -> AccountRead {
AccountRead {
id: account_id_hex(config_id),
status: String::from("ok"),
account: Some(WalletAccount {
program_owner: "00".repeat(32),
balance: "0".repeat(32),
nonce: "0".repeat(32),
data: String::new(),
}),
}
}

#[test]
fn pool_id_is_order_independent_and_matches_core() {
let program = "00".repeat(32);
let config_id = AccountId::new([0xEE; 32]);
let a = AccountId::new([0xCC; 32]);
let b = AccountId::new([0xDD; 32]);

let ab = pool_id(PoolIdRequest {
amm_program_id: program.clone(),
token_in_id: account_id_hex(a),
token_out_id: account_id_hex(b),
config: config_read(config_id),
})
.unwrap();
let ba = pool_id(PoolIdRequest {
amm_program_id: program.clone(),
token_in_id: account_id_hex(b),
token_out_id: account_id_hex(a),
config: config_read(config_id),
})
.unwrap();
// Canonical ordering makes the pool id independent of swap direction.
assert_eq!(ab, ba);

// And it matches amm_core's PDA for the canonical pair.
// And it matches amm_core's PDA for the canonical pair under this config namespace.
let amm = parse_program_id(&program).unwrap();
let (ca, cb) = if is_canonical_pair(a, b) {
(a, b)
} else {
(b, a)
};
assert_eq!(ab["poolId"], account_id_hex(compute_pool_pda(amm, ca, cb)));
assert_eq!(
ab["poolId"],
account_id_hex(compute_pool_pda(amm, config_id, ca, cb))
);

// Same token in/out is rejected.
assert!(pool_id(PoolIdRequest {
amm_program_id: program,
token_in_id: account_id_hex(a),
token_out_id: account_id_hex(a),
config: config_read(config_id),
})
.is_err());
}
Expand Down
Loading
Loading