mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(wallet): add reusable program account selector
This commit is contained in:
@@ -176,14 +176,16 @@ pub enum Instruction {
|
||||
///
|
||||
/// Swap direction is determined by the input holding: `user_input_holding`'s token definition
|
||||
/// selects which pool token is sold. That holding must be signed so the downstream token
|
||||
/// transfer can debit it; `user_output_holding` only receives and needs no signature.
|
||||
/// transfer can debit it. `user_output_holding` may be initialized, or fresh and signed so the
|
||||
/// downstream token transfer can claim and initialize it.
|
||||
///
|
||||
/// Required accounts:
|
||||
/// - AMM Pool (initialized)
|
||||
/// - Vault Holding Account for Token A (initialized)
|
||||
/// - Vault Holding Account for Token B (initialized)
|
||||
/// - User Input Holding Account (initialized, signed) — the token being sold
|
||||
/// - User Output Holding Account (initialized) — receives the token being bought
|
||||
/// - User Output Holding Account (initialized, or uninitialized and signed) — receives the
|
||||
/// token being bought
|
||||
/// - Current Tick Account, the pool's TWAP PDA derived as
|
||||
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
|
||||
/// with the new spot price
|
||||
|
||||
@@ -298,7 +298,8 @@ mod amm {
|
||||
/// Swap some quantity of tokens while maintaining the pool constant product.
|
||||
///
|
||||
/// The swap direction is the input holding's own token; `user_input_holding` must be signed so
|
||||
/// the downstream token transfer can debit it. `user_output_holding` only receives.
|
||||
/// the downstream token transfer can debit it. `user_output_holding` may be initialized, or
|
||||
/// fresh and authorized so the downstream transfer can claim and initialize it.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction interface requires explicit pool, vault, user accounts, and bounds"
|
||||
|
||||
@@ -6,7 +6,7 @@ use amm_core::{
|
||||
pub use amm_core::{compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition};
|
||||
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||
use nssa_core::{
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
account::{Account, AccountId, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
@@ -49,6 +49,18 @@ fn validate_swap_setup(
|
||||
pool_def_data
|
||||
}
|
||||
|
||||
fn assert_user_holding_owner_or_fresh(
|
||||
holding: &AccountWithMetadata,
|
||||
token_program_id: ProgramId,
|
||||
message: &str,
|
||||
) {
|
||||
assert!(
|
||||
holding.account.program_owner == token_program_id
|
||||
|| (holding.account == Account::default() && holding.is_authorized),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Assembles the swap post-states (including the echoed current-tick and clock accounts) and the
|
||||
/// chained call that refreshes the pool's TWAP current tick from the post-swap spot price.
|
||||
#[expect(
|
||||
@@ -188,13 +200,15 @@ pub fn swap_exact_input(
|
||||
} else {
|
||||
panic!("Swap exact input: input holding token is not part of the pool");
|
||||
};
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the configured Token Program"
|
||||
assert_user_holding_owner_or_fresh(
|
||||
&user_holding_a,
|
||||
token_program_id,
|
||||
"User Token A holding must be owned by the configured Token Program",
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
assert_user_holding_owner_or_fresh(
|
||||
&user_holding_b,
|
||||
token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program",
|
||||
);
|
||||
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
||||
// clock here so the swap is rejected early with an AMM-level error.
|
||||
|
||||
@@ -736,6 +736,14 @@ impl AccountWithMetadataForTests {
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh_user_output_holding() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new([48; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
fn vault_a_init() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
@@ -2734,6 +2742,60 @@ fn test_call_swap_chained_call_successful_1() {
|
||||
assert_update_tick_call(&chained_calls, pool_post.account());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_swap_exact_input_accepts_fresh_authorized_output() {
|
||||
let fresh_output = AccountWithMetadataForTests::fresh_user_output_holding();
|
||||
let (_, chained_calls) = swap_exact_input(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
fresh_output.clone(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_a_low(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
|
||||
let mut vault_b = AccountWithMetadataForTests::vault_b_init();
|
||||
vault_b.is_authorized = true;
|
||||
let expected = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![vault_b, fresh_output],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: BalanceForTests::swap_amount_out_b(),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
IdForTests::token_b_definition_id(),
|
||||
)]);
|
||||
|
||||
assert_eq!(chained_calls[1], expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "User Token B holding must be owned by the configured Token Program")]
|
||||
fn test_call_swap_exact_input_rejects_fresh_unauthorized_output() {
|
||||
let mut fresh_output = AccountWithMetadataForTests::fresh_user_output_holding();
|
||||
fresh_output.is_authorized = false;
|
||||
let _ = swap_exact_input(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
fresh_output,
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_a_low(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_swap_chained_call_successful_2() {
|
||||
let (post_states, chained_calls) = swap_exact_input(
|
||||
|
||||
@@ -34,6 +34,10 @@ impl Keys {
|
||||
PrivateKey::try_new([33; 32]).expect("valid private key")
|
||||
}
|
||||
|
||||
fn fresh_output() -> PrivateKey {
|
||||
PrivateKey::try_new([35; 32]).expect("valid private key")
|
||||
}
|
||||
|
||||
fn admin() -> PrivateKey {
|
||||
PrivateKey::try_new([34; 32]).expect("valid private key")
|
||||
}
|
||||
@@ -131,6 +135,10 @@ impl Ids {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_lp()))
|
||||
}
|
||||
|
||||
fn fresh_output() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::fresh_output()))
|
||||
}
|
||||
|
||||
fn admin() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::admin()))
|
||||
}
|
||||
@@ -2825,6 +2833,53 @@ fn amm_swap_a_to_b() {
|
||||
assert_eq!(tick_account.tick, expected_tick);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_swap_exact_input_creates_fresh_output_holding() {
|
||||
let mut state = state_for_amm_tests();
|
||||
let instruction = amm_core::Instruction::SwapExactInput {
|
||||
swap_amount_in: Balances::swap_amount_in(),
|
||||
min_amount_out: Balances::swap_min_out(),
|
||||
deadline: u64::MAX,
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::amm_program(),
|
||||
vec![
|
||||
Ids::config(),
|
||||
Ids::pool_definition(),
|
||||
Ids::vault_a(),
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::fresh_output(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(&state, Ids::user_a()), Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(
|
||||
&message,
|
||||
&[&Keys::user_a(), &Keys::fresh_output()],
|
||||
);
|
||||
|
||||
state
|
||||
.transition_from_public_transaction(&PublicTransaction::new(message, witness_set), 0, 0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::fresh_output()),
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::token_b_definition(),
|
||||
balance: Balances::user_b_swap_2() - Balances::user_b_init(),
|
||||
}),
|
||||
nonce: Nonce(1),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_swap_exact_output_refreshes_current_tick() {
|
||||
let mut state = state_for_amm_tests();
|
||||
|
||||
Reference in New Issue
Block a user