mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-03-24 11:13:06 +00:00
feat(programs/amm): add swap exact output functionality
This commit is contained in:
parent
a62310ce82
commit
c279a581ba
@ -131,6 +131,25 @@ fn main() {
|
||||
token_definition_id_in,
|
||||
)
|
||||
}
|
||||
Instruction::SwapExactOutput {
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition_id_in,
|
||||
} => {
|
||||
let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states
|
||||
.try_into()
|
||||
.expect("SwapExactOutput instruction requires exactly five accounts");
|
||||
amm_program::swap::swap_exact_output(
|
||||
pool,
|
||||
vault_a,
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition_id_in,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
write_nssa_outputs_with_chained_call(
|
||||
|
||||
@ -73,6 +73,22 @@ pub enum Instruction {
|
||||
min_amount_out: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
},
|
||||
|
||||
/// 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
|
||||
/// - User Holding Account for Token B
|
||||
/// Either User Holding Account for Token A or Token B is authorized.
|
||||
SwapExactOutput {
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
|
||||
@ -129,18 +129,6 @@ pub fn new_definition(
|
||||
},
|
||||
);
|
||||
|
||||
// Chain call for liquidity token (TokenLP definition -> User LP Holding)
|
||||
let instruction = if pool.account == Account::default() {
|
||||
token_core::Instruction::NewFungibleDefinition {
|
||||
name: String::from("LP Token"),
|
||||
total_supply: initial_lp,
|
||||
}
|
||||
} else {
|
||||
token_core::Instruction::Mint {
|
||||
amount_to_mint: initial_lp,
|
||||
}
|
||||
};
|
||||
|
||||
let mut pool_lp_auth = pool_definition_lp.clone();
|
||||
pool_lp_auth.is_authorized = true;
|
||||
|
||||
|
||||
@ -4,6 +4,90 @@ use nssa_core::{
|
||||
program::{AccountPostState, ChainedCall},
|
||||
};
|
||||
|
||||
/// Validates swap setup: checks pool is active, vaults match, and reserves are sufficient.
|
||||
fn validate_swap_setup(
|
||||
pool: &AccountWithMetadata,
|
||||
vault_a: &AccountWithMetadata,
|
||||
vault_b: &AccountWithMetadata,
|
||||
) -> PoolDefinition {
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("AMM Program expects a valid Pool Definition Account");
|
||||
|
||||
assert!(pool_def_data.active, "Pool is inactive");
|
||||
assert_eq!(
|
||||
vault_a.account_id, pool_def_data.vault_a_id,
|
||||
"Vault A was not provided"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
|
||||
.expect("AMM Program expects a valid Token Holding Account for Vault A");
|
||||
let token_core::TokenHolding::Fungible {
|
||||
definition_id: _,
|
||||
balance: vault_a_balance,
|
||||
} = vault_a_token_holding
|
||||
else {
|
||||
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault A");
|
||||
};
|
||||
|
||||
assert!(
|
||||
vault_a_balance >= pool_def_data.reserve_a,
|
||||
"Reserve for Token A exceeds vault balance"
|
||||
);
|
||||
|
||||
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
|
||||
.expect("AMM Program expects a valid Token Holding Account for Vault B");
|
||||
let token_core::TokenHolding::Fungible {
|
||||
definition_id: _,
|
||||
balance: vault_b_balance,
|
||||
} = vault_b_token_holding
|
||||
else {
|
||||
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault B");
|
||||
};
|
||||
|
||||
assert!(
|
||||
vault_b_balance >= pool_def_data.reserve_b,
|
||||
"Reserve for Token B exceeds vault balance"
|
||||
);
|
||||
|
||||
pool_def_data
|
||||
}
|
||||
|
||||
/// Creates post-state and returns reserves after swap.
|
||||
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||
fn create_swap_post_states(
|
||||
pool: AccountWithMetadata,
|
||||
pool_def_data: PoolDefinition,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
deposit_a: u128,
|
||||
withdraw_a: u128,
|
||||
deposit_b: u128,
|
||||
withdraw_b: u128,
|
||||
) -> Vec<AccountPostState> {
|
||||
let mut pool_post = pool.account.clone();
|
||||
let pool_post_definition = PoolDefinition {
|
||||
reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a,
|
||||
reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b,
|
||||
..pool_def_data
|
||||
};
|
||||
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
vec![
|
||||
AccountPostState::new(pool_post.clone()),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
AccountPostState::new(user_holding_a.account.clone()),
|
||||
AccountPostState::new(user_holding_b.account.clone()),
|
||||
]
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||
#[must_use]
|
||||
pub fn swap(
|
||||
@ -16,51 +100,7 @@ pub fn swap(
|
||||
min_amount_out: u128,
|
||||
token_in_id: AccountId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
// Verify vaults are in fact vaults
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("Swap: AMM Program expects a valid Pool Definition Account");
|
||||
|
||||
assert!(pool_def_data.active, "Pool is inactive");
|
||||
assert_eq!(
|
||||
vault_a.account_id, pool_def_data.vault_a_id,
|
||||
"Vault A was not provided"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
// fetch pool reserves
|
||||
// validates reserves is at least the vaults' balances
|
||||
let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
|
||||
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault A");
|
||||
let token_core::TokenHolding::Fungible {
|
||||
definition_id: _,
|
||||
balance: vault_a_balance,
|
||||
} = vault_a_token_holding
|
||||
else {
|
||||
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A");
|
||||
};
|
||||
|
||||
assert!(
|
||||
vault_a_balance >= pool_def_data.reserve_a,
|
||||
"Reserve for Token A exceeds vault balance"
|
||||
);
|
||||
|
||||
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
|
||||
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault B");
|
||||
let token_core::TokenHolding::Fungible {
|
||||
definition_id: _,
|
||||
balance: vault_b_balance,
|
||||
} = vault_b_token_holding
|
||||
else {
|
||||
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B");
|
||||
};
|
||||
|
||||
assert!(
|
||||
vault_b_balance >= pool_def_data.reserve_b,
|
||||
"Reserve for Token B exceeds vault balance"
|
||||
);
|
||||
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
if token_in_id == pool_def_data.definition_token_a_id {
|
||||
@ -95,23 +135,18 @@ pub fn swap(
|
||||
panic!("AccountId is not a token type for the pool");
|
||||
};
|
||||
|
||||
// Update pool account
|
||||
let mut pool_post = pool.account;
|
||||
let pool_post_definition = PoolDefinition {
|
||||
reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a,
|
||||
reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b,
|
||||
..pool_def_data
|
||||
};
|
||||
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account),
|
||||
AccountPostState::new(vault_b.account),
|
||||
AccountPostState::new(user_holding_a.account),
|
||||
AccountPostState::new(user_holding_b.account),
|
||||
];
|
||||
let post_states = create_swap_post_states(
|
||||
pool,
|
||||
pool_def_data,
|
||||
vault_a,
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
deposit_a,
|
||||
withdraw_a,
|
||||
deposit_b,
|
||||
withdraw_b,
|
||||
);
|
||||
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
@ -175,3 +210,132 @@ fn swap_logic(
|
||||
|
||||
(chained_calls, swap_amount_in, withdraw_amount)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||
pub fn swap_exact_output(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_in_id: AccountId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
if token_in_id == pool_def_data.definition_token_a_id {
|
||||
let (chained_calls, deposit_a, withdraw_b) = exact_output_swap_logic(
|
||||
user_holding_a.clone(),
|
||||
vault_a.clone(),
|
||||
vault_b.clone(),
|
||||
user_holding_b.clone(),
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
pool_def_data.reserve_a,
|
||||
pool_def_data.reserve_b,
|
||||
pool.account_id,
|
||||
);
|
||||
|
||||
(chained_calls, [deposit_a, 0], [0, withdraw_b])
|
||||
} else if token_in_id == pool_def_data.definition_token_b_id {
|
||||
let (chained_calls, deposit_b, withdraw_a) = exact_output_swap_logic(
|
||||
user_holding_b.clone(),
|
||||
vault_b.clone(),
|
||||
vault_a.clone(),
|
||||
user_holding_a.clone(),
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
pool_def_data.reserve_b,
|
||||
pool_def_data.reserve_a,
|
||||
pool.account_id,
|
||||
);
|
||||
|
||||
(chained_calls, [0, withdraw_a], [deposit_b, 0])
|
||||
} else {
|
||||
panic!("AccountId is not a token type for the pool");
|
||||
};
|
||||
|
||||
let post_states = create_swap_post_states(
|
||||
pool,
|
||||
pool_def_data,
|
||||
vault_a,
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
deposit_a,
|
||||
withdraw_a,
|
||||
deposit_b,
|
||||
withdraw_b,
|
||||
);
|
||||
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||
fn exact_output_swap_logic(
|
||||
user_deposit: AccountWithMetadata,
|
||||
vault_deposit: AccountWithMetadata,
|
||||
vault_withdraw: AccountWithMetadata,
|
||||
user_withdraw: AccountWithMetadata,
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
reserve_deposit_vault_amount: u128,
|
||||
reserve_withdraw_vault_amount: u128,
|
||||
pool_id: AccountId,
|
||||
) -> (Vec<ChainedCall>, u128, u128) {
|
||||
// Guard: exact_amount_out must be nonzero
|
||||
assert!(exact_amount_out != 0, "Exact amount out must be nonzero");
|
||||
|
||||
// Guard: exact_amount_out must be less than reserve_withdraw_vault_amount
|
||||
assert!(
|
||||
exact_amount_out < reserve_withdraw_vault_amount,
|
||||
"Exact amount out exceeds reserve"
|
||||
);
|
||||
|
||||
// Compute deposit amount using ceiling division
|
||||
// Formula: amount_in = ceil(reserve_in * exact_amount_out / (reserve_out - exact_amount_out))
|
||||
let deposit_amount = (reserve_deposit_vault_amount * exact_amount_out)
|
||||
.div_ceil(reserve_withdraw_vault_amount - exact_amount_out);
|
||||
|
||||
// Slippage check
|
||||
assert!(
|
||||
deposit_amount <= max_amount_in,
|
||||
"Required input exceeds maximum amount in"
|
||||
);
|
||||
|
||||
let token_program_id = user_deposit.account.program_owner;
|
||||
|
||||
let mut chained_calls = Vec::new();
|
||||
chained_calls.push(ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_deposit, vault_deposit],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: deposit_amount,
|
||||
},
|
||||
));
|
||||
|
||||
let mut vault_withdraw = vault_withdraw.clone();
|
||||
vault_withdraw.is_authorized = true;
|
||||
|
||||
let pda_seed = compute_vault_pda_seed(
|
||||
pool_id,
|
||||
token_core::TokenHolding::try_from(&vault_withdraw.account.data)
|
||||
.expect("Exact Output Swap Logic: AMM Program expects valid token data")
|
||||
.definition_id(),
|
||||
);
|
||||
|
||||
chained_calls.push(
|
||||
ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![vault_withdraw, user_withdraw],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: exact_amount_out,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![pda_seed]),
|
||||
);
|
||||
|
||||
(chained_calls, deposit_amount, exact_amount_out)
|
||||
}
|
||||
|
||||
@ -14,7 +14,10 @@ use nssa_core::{
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
|
||||
use crate::{
|
||||
add::add_liquidity, new_definition::new_definition, remove::remove_liquidity, swap::swap,
|
||||
add::add_liquidity,
|
||||
new_definition::new_definition,
|
||||
remove::remove_liquidity,
|
||||
swap::{swap, swap_exact_output},
|
||||
};
|
||||
|
||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||
@ -153,6 +156,10 @@ impl BalanceForTests {
|
||||
200
|
||||
}
|
||||
|
||||
fn max_amount_in() -> u128 {
|
||||
166
|
||||
}
|
||||
|
||||
fn vault_a_add_successful() -> u128 {
|
||||
1_400
|
||||
}
|
||||
@ -243,6 +250,74 @@ impl ChainedCallForTests {
|
||||
)
|
||||
}
|
||||
|
||||
fn cc_swap_exact_output_token_a_test_1() -> ChainedCall {
|
||||
let swap_amount: u128 = 498;
|
||||
|
||||
ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::vault_a_init(),
|
||||
],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: swap_amount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn cc_swap_exact_output_token_b_test_1() -> ChainedCall {
|
||||
let swap_amount: u128 = 166;
|
||||
|
||||
let mut vault_b_auth = AccountForTests::vault_b_init();
|
||||
vault_b_auth.is_authorized = true;
|
||||
|
||||
ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![vault_b_auth, AccountForTests::user_holding_b()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: swap_amount,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
IdForTests::token_b_definition_id(),
|
||||
)])
|
||||
}
|
||||
|
||||
fn cc_swap_exact_output_token_a_test_2() -> ChainedCall {
|
||||
let swap_amount: u128 = 285;
|
||||
|
||||
let mut vault_a_auth = AccountForTests::vault_a_init();
|
||||
vault_a_auth.is_authorized = true;
|
||||
|
||||
ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![vault_a_auth, AccountForTests::user_holding_a()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: swap_amount,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
)])
|
||||
}
|
||||
|
||||
fn cc_swap_exact_output_token_b_test_2() -> ChainedCall {
|
||||
let swap_amount: u128 = 200;
|
||||
|
||||
ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
AccountForTests::user_holding_b(),
|
||||
AccountForTests::vault_b_init(),
|
||||
],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: swap_amount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn cc_add_token_a() -> ChainedCall {
|
||||
ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
@ -829,6 +904,54 @@ impl AccountWithMetadataForTests {
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_definition_swap_exact_output_test_1() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: ProgramId::default(),
|
||||
balance: 0u128,
|
||||
data: Data::from(&PoolDefinition {
|
||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||
vault_a_id: IdForTests::vault_a_id(),
|
||||
vault_b_id: IdForTests::vault_b_id(),
|
||||
liquidity_pool_id: IdForTests::token_lp_definition_id(),
|
||||
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
|
||||
reserve_a: 1498u128,
|
||||
reserve_b: 334u128,
|
||||
fees: 0u128,
|
||||
active: true,
|
||||
}),
|
||||
nonce: 0,
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_definition_swap_exact_output_test_2() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: ProgramId::default(),
|
||||
balance: 0u128,
|
||||
data: Data::from(&PoolDefinition {
|
||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||
vault_a_id: IdForTests::vault_a_id(),
|
||||
vault_b_id: IdForTests::vault_b_id(),
|
||||
liquidity_pool_id: IdForTests::token_lp_definition_id(),
|
||||
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
|
||||
reserve_a: BalanceForTests::vault_a_swap_test_2(),
|
||||
reserve_b: BalanceForTests::vault_b_swap_test_2(),
|
||||
fees: 0u128,
|
||||
active: true,
|
||||
}),
|
||||
nonce: 0,
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_definition_add_zero_lp() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
@ -2566,6 +2689,173 @@ fn call_swap_chained_call_successful_2() {
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "AccountId is not a token type for the pool")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_incorrect_token_type() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_lp_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Vault A was not provided")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_vault_a_omitted() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_with_wrong_id(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Vault B was not provided")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_vault_b_omitted() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_with_wrong_id(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Reserve for Token A exceeds vault balance")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_reserves_vault_mismatch_1() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_init_low(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Reserve for Token B exceeds vault balance")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_reserves_vault_mismatch_2() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init_low(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Pool is inactive")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_inactive() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_inactive(),
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
// #[should_panic(expected = "Required input exceeds maximum amount in")]
|
||||
// #[test]
|
||||
// fn test_call_swap_exact_output_exceeds_max_in() {
|
||||
// let _post_states = swap_exact_output(
|
||||
// AccountForTests::pool_definition_init(),
|
||||
// AccountForTests::vault_a_init(),
|
||||
// AccountForTests::vault_b_init(),
|
||||
// AccountForTests::user_holding_a(),
|
||||
// AccountForTests::user_holding_b(),
|
||||
// 166u128,
|
||||
// 100u128,
|
||||
// IdForTests::token_a_definition_id(),
|
||||
// );
|
||||
// }
|
||||
|
||||
#[should_panic(expected = "Exact amount out must be nonzero")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_zero() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
0u128,
|
||||
500u128,
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Exact amount out exceeds reserve")]
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_exceeds_reserve() {
|
||||
let _post_states = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::vault_b_reserve_init(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_swap_exact_output_chained_call_successful() {
|
||||
let (post_states, chained_calls) = swap_exact_output(
|
||||
AccountForTests::pool_definition_init(),
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
BalanceForTests::vault_b_reserve_init(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
|
||||
let pool_post = post_states[0].clone();
|
||||
|
||||
assert!(
|
||||
AccountForTests::pool_definition_swap_exact_output_test_1().account == *pool_post.account()
|
||||
);
|
||||
|
||||
let chained_call_a = chained_calls[0].clone();
|
||||
let chained_call_b = chained_calls[1].clone();
|
||||
|
||||
assert_eq!(
|
||||
chained_call_a,
|
||||
ChainedCallForTests::cc_swap_exact_output_token_a_test_1()
|
||||
);
|
||||
assert_eq!(
|
||||
chained_call_b,
|
||||
ChainedCallForTests::cc_swap_exact_output_token_b_test_1()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_definition_lp_asymmetric_amounts() {
|
||||
let (post_states, chained_calls) = new_definition(
|
||||
|
||||
@ -52,7 +52,27 @@ pub enum AmmProgramAgnosticSubcommand {
|
||||
#[arg(long)]
|
||||
token_definition: String,
|
||||
},
|
||||
/// Add liquidity.
|
||||
/// Swap specifying exact output amount
|
||||
///
|
||||
/// The account associated with swapping token must be owned
|
||||
///
|
||||
/// Only public execution allowed
|
||||
SwapExactOutput {
|
||||
/// user_holding_a - valid 32 byte base58 string with privacy prefix
|
||||
#[arg(long)]
|
||||
user_holding_a: String,
|
||||
/// user_holding_b - valid 32 byte base58 string with privacy prefix
|
||||
#[arg(long)]
|
||||
user_holding_b: String,
|
||||
#[arg(long)]
|
||||
exact_amount_out: u128,
|
||||
#[arg(long)]
|
||||
max_amount_in: u128,
|
||||
/// token_definition - valid 32 byte base58 string WITHOUT privacy prefix
|
||||
#[arg(long)]
|
||||
token_definition: String,
|
||||
},
|
||||
/// Add liquidity
|
||||
///
|
||||
/// `user_holding_a` and `user_holding_b` must be owned.
|
||||
///
|
||||
@ -185,6 +205,41 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::SwapExactOutput {
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition,
|
||||
} => {
|
||||
let (user_holding_a, user_holding_a_privacy) =
|
||||
parse_addr_with_privacy_prefix(&user_holding_a)?;
|
||||
let (user_holding_b, user_holding_b_privacy) =
|
||||
parse_addr_with_privacy_prefix(&user_holding_b)?;
|
||||
|
||||
let user_holding_a: AccountId = user_holding_a.parse()?;
|
||||
let user_holding_b: AccountId = user_holding_b.parse()?;
|
||||
|
||||
match (user_holding_a_privacy, user_holding_b_privacy) {
|
||||
(AccountPrivacyKind::Public, AccountPrivacyKind::Public) => {
|
||||
Amm(wallet_core)
|
||||
.send_swap_exact_output(
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition.parse()?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
_ => {
|
||||
// ToDo: Implement after private multi-chain calls is available
|
||||
anyhow::bail!("Only public execution allowed for Amm calls");
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::AddLiquidity {
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
|
||||
@ -209,6 +209,111 @@ impl Amm<'_> {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn send_swap_exact_output(
|
||||
&self,
|
||||
user_holding_a: AccountId,
|
||||
user_holding_b: AccountId,
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let instruction = amm_core::Instruction::SwapExactOutput {
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition_id_in,
|
||||
};
|
||||
let program = Program::amm();
|
||||
let amm_program_id = Program::amm().id();
|
||||
|
||||
let user_a_acc = self
|
||||
.0
|
||||
.get_account_public(user_holding_a)
|
||||
.await
|
||||
.map_err(|_| ExecutionFailureKind::SequencerError)?;
|
||||
let user_b_acc = self
|
||||
.0
|
||||
.get_account_public(user_holding_b)
|
||||
.await
|
||||
.map_err(|_| ExecutionFailureKind::SequencerError)?;
|
||||
|
||||
let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data)
|
||||
.map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_a))?
|
||||
.definition_id();
|
||||
let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data)
|
||||
.map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))?
|
||||
.definition_id();
|
||||
|
||||
let amm_pool =
|
||||
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id);
|
||||
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
|
||||
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
|
||||
|
||||
let account_ids = vec![
|
||||
amm_pool,
|
||||
vault_holding_a,
|
||||
vault_holding_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
];
|
||||
|
||||
let account_id_auth;
|
||||
|
||||
// Checking, which account are associated with TokenDefinition
|
||||
let token_holder_acc_a = self
|
||||
.0
|
||||
.get_account_public(user_holding_a)
|
||||
.await
|
||||
.map_err(|_| ExecutionFailureKind::SequencerError)?;
|
||||
let token_holder_acc_b = self
|
||||
.0
|
||||
.get_account_public(user_holding_b)
|
||||
.await
|
||||
.map_err(|_| ExecutionFailureKind::SequencerError)?;
|
||||
|
||||
let token_holder_a = TokenHolding::try_from(&token_holder_acc_a.data)
|
||||
.map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_a))?;
|
||||
let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data)
|
||||
.map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))?;
|
||||
|
||||
if token_holder_a.definition_id() == token_definition_id_in {
|
||||
account_id_auth = user_holding_a;
|
||||
} else if token_holder_b.definition_id() == token_definition_id_in {
|
||||
account_id_auth = user_holding_b;
|
||||
} else {
|
||||
return Err(ExecutionFailureKind::AccountDataError(
|
||||
token_definition_id_in,
|
||||
));
|
||||
}
|
||||
|
||||
let nonces = self
|
||||
.0
|
||||
.get_accounts_nonces(vec![account_id_auth])
|
||||
.await
|
||||
.map_err(|_| ExecutionFailureKind::SequencerError)?;
|
||||
|
||||
let signing_key = self
|
||||
.0
|
||||
.storage
|
||||
.user_data
|
||||
.get_pub_account_signing_key(account_id_auth)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set =
|
||||
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
|
||||
|
||||
let tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
||||
}
|
||||
|
||||
pub async fn send_add_liquidity(
|
||||
&self,
|
||||
user_holding_a: AccountId,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user