交换
swap 指令执行两个主要任务:
计算当我们向 amm 发送一定数量的
mint_y时将收到多少mint_x(反之亦然),并包含手续费。将
from代币转移到 vault,并将to代币转移到用户的代币账户。
所需账户
此上下文需要以下账户:
user:执行 AMM 交换的用户。必须是signer。user_x_ata:用户的 token X 关联代币账户。必须作为mutable传入。user_y_ata:用户的 token Y 关联代币账户。必须作为mutable传入。vault_x:池子持有 token X 的代币账户。必须作为mutable传入。vault_y:池子持有 token Y 的代币账户。必须作为mutable传入。config:AMM 配置账户。保存池子的状态和参数。token_program:代币程序账户。它用于执行转账和铸造等代币操作。必须是executable。
这里,我再次把实现留给你:
pub struct SwapAccounts<'a> {
pub user: &'a AccountInfo,
pub user_x_ata: &'a AccountInfo,
pub user_y_ata: &'a AccountInfo,
pub vault_x: &'a AccountInfo,
pub vault_y: &'a AccountInfo,
pub config: &'a AccountInfo,
pub token_program: &'a AccountInfo,
}
impl<'a> TryFrom<&'a [AccountInfo]> for SwapAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
//..
}
}指令数据
以下是我们需要传入的指令数据:
is_x:该 swap 是从 token X 到 token Y,还是反过来;需要它来正确对齐账户。必须是[u8]amount:用户愿意发送的 token 数量,以换取该交易对中的另一种 token。必须是[u64]min:用户愿意为amount接受的最小 token 数量。必须是[u64]expiration:该订单的过期时间。确保交易必须在一定时间内完成非常重要。必须是[i64]
我们将像初始化一样处理 SwapInstructionData 的实现。所以我将实现留给你:
pub struct SwapInstructionData {
pub is_x: bool,
pub amount: u64,
pub min: u64,
pub expiration: i64,
}
impl<'a> TryFrom<&'a [u8]> for SwapInstructionData {
type Error = ProgramError;
fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
//..
}
}指令逻辑
我们首先反序列化 instruction_data 和 accounts。
然后我们需要:
加载
Config账户以获取其中的所有数据。我们可以使用Config::load()辅助工具来完成。验证
AmmState是否有效(也就是它是否等于AmmState::Initialized)。检查
vault_x和vault_y的派生是否为关联代币账户(Associated Token Accounts)。反序列化所有涉及的代币账户,并使用其中的数据通过
constant-product-curvecrate 计算交换数量,并像这样检查滑点:
// Deserialize the token accounts
let vault_x = unsafe { TokenAccount::from_account_info_unchecked(self.accounts.vault_x)? };
let vault_y = unsafe { TokenAccount::from_account_info_unchecked(self.accounts.vault_y)? };
// Swap Calculations
let mut curve = ConstantProduct::init(
vault_x.amount(),
vault_y.amount(),
vault_x.amount(),
config.fee(),
None,
)
.map_err(|_| ProgramError::Custom(1))?;
let p = match self.instruction_data.is_x {
true => LiquidityPair::X,
false => LiquidityPair::Y,
};
let swap_result = curve
.swap(p, self.instruction_data.amount, self.instruction_data.min)
.map_err(|_| ProgramError::Custom(1))?;
// Check for correct values
if swap_result.deposit == 0 || swap_result.withdraw == 0 {
return Err(ProgramError::InvalidArgument);
}将代币转账批量合并到单一 CPI 中。你已经在
deposit里见过 batch 的模式,所以这里我们对两条Transfer指令做同样的事情,只调用一次代币程序。由于从 vault 转出的那笔转账仍然需要configPDA 签名,batch 应该以batch.invoke_signed(&[signer])?;结束。
const MAX_ACCOUNTS_LEN: usize = Transfer::MAX_ACCOUNTS_LEN * 2;
const MAX_DATA_LEN: usize = Batch::header_data_len(2) + Transfer::DATA_LEN * 2;
// ...
let mut batch_state = BatchState::new(MAX_ACCOUNTS_LEN, MAX_DATA_LEN);
let mut batch = batch_state.as_batch()?;
if self.instruction_data.is_x {
Transfer::new(
// ...
)
.into_batch(&mut batch)?;
Transfer::new(
// ...
)
.into_batch(&mut batch)?;
} else {
Transfer::new(
// ...
)
.into_batch(&mut batch)?;
Transfer::new(
// ...
)
.into_batch(&mut batch)?;
}
batch.invoke_signed(&[signer])?;你应该已经足够熟练可以独立完成这部分内容,所以我将实现部分留给你:
pub struct Swap<'a> {
pub accounts: SwapAccounts<'a>,
pub instruction_data: SwapInstructionData,
}
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Swap<'a> {
type Error = ProgramError;
fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
let accounts = SwapAccounts::try_from(accounts)?;
let instruction_data = SwapInstructionData::try_from(data)?;
// Return the initialized struct
Ok(Self {
accounts,
instruction_data,
})
}
}
impl<'a> Swap<'a> {
pub const DISCRIMINATOR: &'a u8 = &3;
pub fn process(&mut self) -> ProgramResult {
//..
Ok(())
}
}