2026-03-17 18:08:53 +01:00
|
|
|
//! This crate contains core data structures and utilities for the AMM Program.
|
|
|
|
|
|
|
|
|
|
use borsh::{BorshDeserialize, BorshSerialize};
|
|
|
|
|
use nssa_core::{
|
2026-04-08 10:57:47 -03:00
|
|
|
account::{AccountId, AccountWithMetadata, Data},
|
2026-03-17 18:08:53 +01:00
|
|
|
program::{PdaSeed, ProgramId},
|
|
|
|
|
};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
2026-05-11 15:31:31 +02:00
|
|
|
use spel_framework_macros::account_type;
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
// These stable seed bytes are part of the PDA derivation scheme and must stay unchanged for
|
|
|
|
|
// compatibility.
|
|
|
|
|
const LIQUIDITY_TOKEN_PDA_SEED: [u8; 32] = [0; 32];
|
|
|
|
|
const LP_LOCK_HOLDING_PDA_SEED: [u8; 32] = [1; 32];
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
/// AMM Program Instruction.
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
pub enum Instruction {
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
/// Initializes the AMM Program by creating its singleton configuration account.
|
|
|
|
|
///
|
|
|
|
|
/// The configuration account is a PDA derived from the constant `"CONFIG"` seed
|
2026-06-18 09:01:10 +02:00
|
|
|
/// (`compute_config_pda(self_program_id)`). It stores the program IDs the AMM issues chained
|
|
|
|
|
/// calls to (the Token Program and the TWAP oracle program), plus the admin `authority`
|
|
|
|
|
/// allowed to change configuration later via `UpdateConfig`. The Program must be initialized
|
|
|
|
|
/// via this instruction before any pool can be created or interacted with — the other
|
|
|
|
|
/// instructions read these program IDs from this account and reject calls when it does not
|
|
|
|
|
/// yet exist.
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Config Account, uninitialized, derived as `compute_config_pda(self_program_id)`
|
|
|
|
|
Initialize {
|
|
|
|
|
/// Program ID of the Token Program the AMM will issue chained calls to.
|
|
|
|
|
token_program_id: ProgramId,
|
2026-06-18 09:01:10 +02:00
|
|
|
/// Program ID of the TWAP oracle program the AMM will issue chained calls to.
|
|
|
|
|
twap_oracle_program_id: ProgramId,
|
2026-06-18 16:46:38 +02:00
|
|
|
/// Admin authority allowed to change configuration via `UpdateConfig`.
|
|
|
|
|
authority: AccountId,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Updates the AMM Program's configuration. Only the configured admin `authority` may call
|
|
|
|
|
/// this; the authority account must be passed authorized (signed).
|
|
|
|
|
///
|
|
|
|
|
/// Each field is optional — `None` leaves the corresponding value unchanged. Setting
|
|
|
|
|
/// `new_authority` transfers admin control to a different account.
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Config Account (initialized)
|
|
|
|
|
/// - Authority Account — must equal the config's current `authority`, passed authorized.
|
|
|
|
|
UpdateConfig {
|
|
|
|
|
/// New Token Program ID for chained calls, or `None` to keep the current one.
|
|
|
|
|
token_program_id: Option<ProgramId>,
|
2026-06-18 09:01:10 +02:00
|
|
|
/// New TWAP oracle program ID for chained calls, or `None` to keep the current one.
|
|
|
|
|
twap_oracle_program_id: Option<ProgramId>,
|
2026-06-18 16:46:38 +02:00
|
|
|
/// New admin authority (transfers control), or `None` to keep the current admin.
|
|
|
|
|
new_authority: Option<AccountId>,
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
},
|
|
|
|
|
|
2026-06-18 09:01:10 +02:00
|
|
|
/// Creates a TWAP price-observations account for a pool over a time window, on behalf of the
|
|
|
|
|
/// AMM, via a chained call to the configured TWAP oracle program.
|
|
|
|
|
///
|
|
|
|
|
/// The pool acts as the price source: the AMM authorizes it (via its pool PDA seed) so the
|
|
|
|
|
/// oracle ties the observations account to this pool. The feed's initial tick is read from the
|
|
|
|
|
/// pool's [`CurrentTickAccount`](twap_oracle_core::CurrentTickAccount) — the authoritative
|
|
|
|
|
/// tick the AMM previously wrote — rather than being supplied by the caller, so the feed
|
|
|
|
|
/// cannot be seeded at a forged price. Rejects if the observations account already exists.
|
|
|
|
|
/// The clock must be the canonical 1-block LEZ clock.
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Config Account (initialized)
|
|
|
|
|
/// - AMM Pool (initialized; acts as the price source)
|
|
|
|
|
/// - Current Tick Account, the pool's initialized TWAP PDA derived as
|
|
|
|
|
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; supplies the
|
|
|
|
|
/// initial tick
|
|
|
|
|
/// - Price Observations Account, uninitialized TWAP PDA derived as
|
|
|
|
|
/// `compute_price_observations_pda(twap_oracle_program_id, pool.account_id,
|
|
|
|
|
/// window_duration)`
|
|
|
|
|
/// - Clock Account (the canonical 1-block LEZ clock)
|
|
|
|
|
CreatePriceObservations {
|
|
|
|
|
/// Duration of the TWAP window this feed serves, in milliseconds. Part of the
|
|
|
|
|
/// observations PDA seed, so each window gets a distinct account.
|
|
|
|
|
window_duration: u64,
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-10 15:43:13 -03:00
|
|
|
/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
|
2026-03-17 18:08:53 +01:00
|
|
|
///
|
2026-04-08 17:48:13 -03:00
|
|
|
/// On initialization, `MINIMUM_LIQUIDITY` LP tokens are permanently locked
|
|
|
|
|
/// in the LP-lock holding PDA; the caller receives `initial_lp - MINIMUM_LIQUIDITY`.
|
|
|
|
|
///
|
2026-03-17 18:08:53 +01:00
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Pool
|
|
|
|
|
/// - Vault Holding Account for Token A
|
|
|
|
|
/// - Vault Holding Account for Token B
|
|
|
|
|
/// - Pool Liquidity Token Definition
|
2026-05-04 10:34:10 -03:00
|
|
|
/// - LP Lock Holding Account, derived as `compute_lp_lock_holding_pda(self_program_id,
|
2026-04-08 17:48:13 -03:00
|
|
|
/// pool.account_id)`
|
2026-03-17 18:08:53 +01:00
|
|
|
/// - User Holding Account for Token A (authorized)
|
|
|
|
|
/// - User Holding Account for Token B (authorized)
|
2026-04-15 14:55:04 -03:00
|
|
|
/// - User Holding Account for Pool Liquidity (authorized when uninitialized)
|
2026-03-17 18:08:53 +01:00
|
|
|
NewDefinition {
|
|
|
|
|
token_a_amount: u128,
|
|
|
|
|
token_b_amount: u128,
|
2026-03-31 20:45:57 -03:00
|
|
|
fees: u128,
|
2026-04-23 17:19:15 +02:00
|
|
|
/// Unix timestamp (milliseconds) after which this transaction is invalid.
|
|
|
|
|
deadline: u64,
|
2026-03-17 18:08:53 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Adds liquidity to the Pool
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Pool (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token A (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token B (initialized)
|
|
|
|
|
/// - Pool Liquidity Token Definition (initialized)
|
|
|
|
|
/// - User Holding Account for Token A (authorized)
|
|
|
|
|
/// - User Holding Account for Token B (authorized)
|
|
|
|
|
/// - User Holding Account for Pool Liquidity
|
|
|
|
|
AddLiquidity {
|
|
|
|
|
min_amount_liquidity: u128,
|
|
|
|
|
max_amount_to_add_token_a: u128,
|
|
|
|
|
max_amount_to_add_token_b: u128,
|
2026-04-23 17:19:15 +02:00
|
|
|
/// Unix timestamp (milliseconds) after which this transaction is invalid.
|
|
|
|
|
deadline: u64,
|
2026-03-17 18:08:53 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Removes liquidity from the Pool
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Pool (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token A (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token B (initialized)
|
|
|
|
|
/// - Pool Liquidity Token Definition (initialized)
|
|
|
|
|
/// - User Holding Account for Token A (initialized)
|
|
|
|
|
/// - User Holding Account for Token B (initialized)
|
|
|
|
|
/// - User Holding Account for Pool Liquidity (authorized)
|
|
|
|
|
RemoveLiquidity {
|
|
|
|
|
remove_liquidity_amount: u128,
|
|
|
|
|
min_amount_to_remove_token_a: u128,
|
|
|
|
|
min_amount_to_remove_token_b: u128,
|
2026-04-23 17:19:15 +02:00
|
|
|
/// Unix timestamp (milliseconds) after which this transaction is invalid.
|
|
|
|
|
deadline: u64,
|
2026-03-17 18:08:53 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Swap some quantity of Tokens (either Token A or Token B)
|
|
|
|
|
/// while maintaining the Pool constant product.
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Pool (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token A (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token B (initialized)
|
|
|
|
|
/// - User Holding Account for Token A
|
2026-04-23 17:19:15 +02:00
|
|
|
/// - User Holding Account for Token B; either is authorized.
|
2026-04-07 09:31:32 +02:00
|
|
|
SwapExactInput {
|
2026-03-17 18:08:53 +01:00
|
|
|
swap_amount_in: u128,
|
|
|
|
|
min_amount_out: u128,
|
|
|
|
|
token_definition_id_in: AccountId,
|
2026-04-23 17:19:15 +02:00
|
|
|
/// Unix timestamp (milliseconds) after which this transaction is invalid.
|
|
|
|
|
deadline: u64,
|
2026-03-17 18:08:53 +01:00
|
|
|
},
|
2026-04-08 10:57:47 -03:00
|
|
|
|
2026-04-02 17:16:53 +02:00
|
|
|
/// Swap tokens specifying the exact desired output amount,
|
|
|
|
|
/// while maintaining the Pool constant product.
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
|
|
|
|
/// - AMM Pool (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token A (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token B (initialized)
|
|
|
|
|
/// - User Holding Account for Token A
|
2026-04-23 17:19:15 +02:00
|
|
|
/// - User Holding Account for Token B; either is authorized.
|
2026-04-02 17:16:53 +02:00
|
|
|
SwapExactOutput {
|
|
|
|
|
exact_amount_out: u128,
|
|
|
|
|
max_amount_in: u128,
|
|
|
|
|
token_definition_id_in: AccountId,
|
2026-04-23 17:19:15 +02:00
|
|
|
/// Unix timestamp (milliseconds) after which this transaction is invalid.
|
|
|
|
|
deadline: u64,
|
2026-04-02 17:16:53 +02:00
|
|
|
},
|
|
|
|
|
|
2026-04-08 10:57:47 -03:00
|
|
|
/// Sync pool reserves with current vault balances.
|
|
|
|
|
///
|
|
|
|
|
/// Required accounts:
|
2026-04-10 15:43:13 -03:00
|
|
|
/// - AMM Pool (initialized, with LP supply at or above minimum liquidity)
|
2026-04-08 10:57:47 -03:00
|
|
|
/// - Vault Holding Account for Token A (initialized)
|
|
|
|
|
/// - Vault Holding Account for Token B (initialized)
|
|
|
|
|
SyncReserves,
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
pub const MINIMUM_LIQUIDITY: u128 = 1_000;
|
|
|
|
|
|
2026-05-11 15:31:31 +02:00
|
|
|
#[account_type]
|
2026-03-17 18:08:53 +01:00
|
|
|
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
|
|
|
|
pub struct PoolDefinition {
|
|
|
|
|
pub definition_token_a_id: AccountId,
|
|
|
|
|
pub definition_token_b_id: AccountId,
|
|
|
|
|
pub vault_a_id: AccountId,
|
|
|
|
|
pub vault_b_id: AccountId,
|
|
|
|
|
pub liquidity_pool_id: AccountId,
|
2026-04-10 15:43:13 -03:00
|
|
|
/// Total LP supply tracked by the pool. After initialization it includes the permanently
|
|
|
|
|
/// locked `MINIMUM_LIQUIDITY`; a zero supply means the pool is uninitialized
|
2026-03-17 18:08:53 +01:00
|
|
|
pub liquidity_pool_supply: u128,
|
|
|
|
|
pub reserve_a: u128,
|
|
|
|
|
pub reserve_b: u128,
|
2026-03-31 20:45:57 -03:00
|
|
|
/// Fee tier in basis points.
|
2026-03-17 18:08:53 +01:00
|
|
|
pub fees: u128,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 20:45:57 -03:00
|
|
|
pub const FEE_BPS_DENOMINATOR: u128 = 10_000;
|
|
|
|
|
pub const FEE_TIER_BPS_1: u128 = 1;
|
|
|
|
|
pub const FEE_TIER_BPS_5: u128 = 5;
|
|
|
|
|
pub const FEE_TIER_BPS_30: u128 = 30;
|
|
|
|
|
pub const FEE_TIER_BPS_100: u128 = 100;
|
|
|
|
|
|
|
|
|
|
pub fn is_supported_fee_tier(fees: u128) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
fees,
|
|
|
|
|
FEE_TIER_BPS_1 | FEE_TIER_BPS_5 | FEE_TIER_BPS_30 | FEE_TIER_BPS_100
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn assert_supported_fee_tier(fees: u128) {
|
|
|
|
|
assert!(
|
|
|
|
|
is_supported_fee_tier(fees),
|
|
|
|
|
"Fee tier must be one of 1, 5, 30, or 100 basis points"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
impl TryFrom<&Data> for PoolDefinition {
|
|
|
|
|
type Error = std::io::Error;
|
|
|
|
|
|
|
|
|
|
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
|
|
|
|
PoolDefinition::try_from_slice(data.as_ref())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&PoolDefinition> for Data {
|
|
|
|
|
fn from(definition: &PoolDefinition) -> Self {
|
|
|
|
|
// Using size_of_val as size hint for Vec allocation
|
|
|
|
|
let mut data = Vec::with_capacity(std::mem::size_of_val(definition));
|
|
|
|
|
|
|
|
|
|
BorshSerialize::serialize(definition, &mut data)
|
|
|
|
|
.expect("Serialization to Vec should not fail");
|
|
|
|
|
|
|
|
|
|
Data::try_from(data).expect("Token definition encoded data should fit into Data")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
/// Singleton configuration account for the AMM Program.
|
|
|
|
|
///
|
|
|
|
|
/// Stored at the PDA derived from the constant `"CONFIG"` seed
|
|
|
|
|
/// (`compute_config_pda(amm_program_id)`). Created once via the `Initialize` instruction; its
|
|
|
|
|
/// existence is the Program's "initialized" flag. Every chained-call instruction reads
|
|
|
|
|
/// `token_program_id` from here instead of trusting the program owner of a caller-supplied
|
|
|
|
|
/// account.
|
|
|
|
|
#[account_type]
|
|
|
|
|
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
|
|
|
|
pub struct AmmConfig {
|
|
|
|
|
/// Program ID of the Token Program the AMM issues chained calls to.
|
|
|
|
|
pub token_program_id: ProgramId,
|
2026-06-18 09:01:10 +02:00
|
|
|
/// Program ID of the TWAP oracle program the AMM issues chained calls to.
|
|
|
|
|
pub twap_oracle_program_id: ProgramId,
|
2026-06-18 16:46:38 +02:00
|
|
|
/// Admin authority allowed to change this configuration via `UpdateConfig`.
|
|
|
|
|
pub authority: AccountId,
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TryFrom<&Data> for AmmConfig {
|
|
|
|
|
type Error = std::io::Error;
|
|
|
|
|
|
|
|
|
|
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
|
|
|
|
AmmConfig::try_from_slice(data.as_ref())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&AmmConfig> for Data {
|
|
|
|
|
fn from(config: &AmmConfig) -> Self {
|
|
|
|
|
let mut data = Vec::with_capacity(std::mem::size_of_val(config));
|
|
|
|
|
|
|
|
|
|
BorshSerialize::serialize(config, &mut data).expect("Serialization to Vec should not fail");
|
|
|
|
|
|
|
|
|
|
Data::try_from(data).expect("AMM config encoded data should fit into Data")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stable seed marker for the singleton config PDA. The literal `"CONFIG"` bytes are hashed into
|
|
|
|
|
// the 32-byte seed; this must stay unchanged for address compatibility.
|
|
|
|
|
const CONFIG_PDA_SEED: &[u8] = b"CONFIG";
|
|
|
|
|
|
|
|
|
|
/// Derives the [`AccountId`] of the AMM Program's singleton config PDA.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn compute_config_pda(amm_program_id: ProgramId) -> AccountId {
|
|
|
|
|
AccountId::for_public_pda(&amm_program_id, &compute_config_pda_seed())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Derives the [`PdaSeed`] of the AMM Program's singleton config PDA from the `"CONFIG"` bytes.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn compute_config_pda_seed() -> PdaSeed {
|
|
|
|
|
use risc0_zkvm::sha::{Impl, Sha256};
|
|
|
|
|
|
|
|
|
|
PdaSeed::new(
|
|
|
|
|
Impl::hash_bytes(CONFIG_PDA_SEED)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("Hash output must be exactly 32 bytes long"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
pub fn compute_pool_pda(
|
|
|
|
|
amm_program_id: ProgramId,
|
|
|
|
|
definition_token_a_id: AccountId,
|
|
|
|
|
definition_token_b_id: AccountId,
|
|
|
|
|
) -> AccountId {
|
2026-05-11 15:29:41 +02:00
|
|
|
AccountId::for_public_pda(
|
2026-03-17 18:08:53 +01:00
|
|
|
&amm_program_id,
|
|
|
|
|
&compute_pool_pda_seed(definition_token_a_id, definition_token_b_id),
|
2026-05-11 15:29:41 +02:00
|
|
|
)
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_pool_pda_seed(
|
|
|
|
|
definition_token_a_id: AccountId,
|
|
|
|
|
definition_token_b_id: AccountId,
|
|
|
|
|
) -> PdaSeed {
|
|
|
|
|
use risc0_zkvm::sha::{Impl, Sha256};
|
|
|
|
|
|
|
|
|
|
let (token_1, token_2) = match definition_token_a_id
|
|
|
|
|
.value()
|
|
|
|
|
.cmp(definition_token_b_id.value())
|
|
|
|
|
{
|
|
|
|
|
std::cmp::Ordering::Less => (definition_token_b_id, definition_token_a_id),
|
|
|
|
|
std::cmp::Ordering::Greater => (definition_token_a_id, definition_token_b_id),
|
|
|
|
|
std::cmp::Ordering::Equal => panic!("Definitions match"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut bytes = [0; 64];
|
2026-05-06 17:08:15 -03:00
|
|
|
let (token_1_bytes, token_2_bytes) = bytes.split_at_mut(32);
|
|
|
|
|
token_1_bytes.copy_from_slice(&token_1.to_bytes());
|
|
|
|
|
token_2_bytes.copy_from_slice(&token_2.to_bytes());
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
PdaSeed::new(
|
|
|
|
|
Impl::hash_bytes(&bytes)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("Hash output must be exactly 32 bytes long"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_vault_pda(
|
|
|
|
|
amm_program_id: ProgramId,
|
|
|
|
|
pool_id: AccountId,
|
|
|
|
|
definition_token_id: AccountId,
|
|
|
|
|
) -> AccountId {
|
2026-05-11 15:29:41 +02:00
|
|
|
AccountId::for_public_pda(
|
2026-03-17 18:08:53 +01:00
|
|
|
&amm_program_id,
|
|
|
|
|
&compute_vault_pda_seed(pool_id, definition_token_id),
|
2026-05-11 15:29:41 +02:00
|
|
|
)
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed {
|
|
|
|
|
use risc0_zkvm::sha::{Impl, Sha256};
|
|
|
|
|
|
|
|
|
|
let mut bytes = [0; 64];
|
2026-05-06 17:08:15 -03:00
|
|
|
let (pool_bytes, definition_bytes) = bytes.split_at_mut(32);
|
|
|
|
|
pool_bytes.copy_from_slice(&pool_id.to_bytes());
|
|
|
|
|
definition_bytes.copy_from_slice(&definition_token_id.to_bytes());
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
PdaSeed::new(
|
|
|
|
|
Impl::hash_bytes(&bytes)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("Hash output must be exactly 32 bytes long"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
|
2026-05-11 15:29:41 +02:00
|
|
|
AccountId::for_public_pda(&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed {
|
|
|
|
|
use risc0_zkvm::sha::{Impl, Sha256};
|
|
|
|
|
|
|
|
|
|
let mut bytes = [0; 64];
|
2026-05-06 17:08:15 -03:00
|
|
|
let (pool_bytes, seed_bytes) = bytes.split_at_mut(32);
|
|
|
|
|
pool_bytes.copy_from_slice(&pool_id.to_bytes());
|
|
|
|
|
seed_bytes.copy_from_slice(&LIQUIDITY_TOKEN_PDA_SEED);
|
2026-04-08 17:48:13 -03:00
|
|
|
|
|
|
|
|
PdaSeed::new(
|
|
|
|
|
Impl::hash_bytes(&bytes)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("Hash output must be exactly 32 bytes long"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_lp_lock_holding_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
|
2026-05-11 15:29:41 +02:00
|
|
|
AccountId::for_public_pda(&amm_program_id, &compute_lp_lock_holding_pda_seed(pool_id))
|
2026-04-08 17:48:13 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_lp_lock_holding_pda_seed(pool_id: AccountId) -> PdaSeed {
|
|
|
|
|
use risc0_zkvm::sha::{Impl, Sha256};
|
|
|
|
|
|
|
|
|
|
let mut bytes = [0; 64];
|
2026-05-06 17:08:15 -03:00
|
|
|
let (pool_bytes, seed_bytes) = bytes.split_at_mut(32);
|
|
|
|
|
pool_bytes.copy_from_slice(&pool_id.to_bytes());
|
|
|
|
|
seed_bytes.copy_from_slice(&LP_LOCK_HOLDING_PDA_SEED);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
PdaSeed::new(
|
|
|
|
|
Impl::hash_bytes(&bytes)
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.expect("Hash output must be exactly 32 bytes long"),
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-04-08 10:57:47 -03:00
|
|
|
|
|
|
|
|
fn read_fungible_holding(account: &AccountWithMetadata, context: &str) -> (AccountId, u128) {
|
|
|
|
|
let token_holding = token_core::TokenHolding::try_from(&account.account.data)
|
|
|
|
|
.unwrap_or_else(|_| panic!("{context}: AMM Program expects a valid Token Holding Account"));
|
|
|
|
|
|
|
|
|
|
let token_core::TokenHolding::Fungible {
|
|
|
|
|
definition_id,
|
|
|
|
|
balance,
|
|
|
|
|
} = token_holding
|
|
|
|
|
else {
|
|
|
|
|
panic!("{context}: AMM Program expects a valid Fungible Token Holding Account");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(definition_id, balance)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn read_vault_fungible_balances(
|
|
|
|
|
context: &str,
|
|
|
|
|
vault_a: &AccountWithMetadata,
|
|
|
|
|
vault_b: &AccountWithMetadata,
|
|
|
|
|
) -> (u128, u128) {
|
|
|
|
|
let vault_a_context = format!("{context}: Vault A");
|
|
|
|
|
let vault_b_context = format!("{context}: Vault B");
|
|
|
|
|
let (_, vault_a_balance) = read_fungible_holding(vault_a, &vault_a_context);
|
|
|
|
|
let (_, vault_b_balance) = read_fungible_holding(vault_b, &vault_b_context);
|
|
|
|
|
|
|
|
|
|
(vault_a_balance, vault_b_balance)
|
|
|
|
|
}
|