swap_in / swap_out on the AMM program and read the returned amount.
You do not need the AMM program’s source crate. Anchor’s declare_program! macro generates a full CPI client from the program’s published IDL. If you’re not on Anchor, a raw instruction recipe is at the bottom.
AMM program id: jupZ4m2GqUCJ5iueMfzQf8khFfH31d4XAQt3RzCT9Vd.
Generate the CPI client from the IDL
Fetch the on-chain IDL into your program’sidls/ directory (requires Anchor 0.30+):
dex::cpi::swap_in / swap_out, the dex::cpi::accounts::DexOperation account struct, and the program id, all matching the deployed program.
The account context (DexOperation)
A swap uses 25 accounts, in this exact order. Your instruction declares them in its own#[derive(Accounts)], then maps them into DexOperation:
| # | Account | Flags | What it is |
|---|---|---|---|
| 1 | signer | mut, signer | funds leave its token account |
| 2 | dex | mut | the Dex pool account |
| 3 | user_token_0_account | mut | signer’s token0 ATA (authority = signer) |
| 4 | user_token_1_account | mut | signer’s token1 ATA (authority = signer) |
| 5 | recipient | optional | output owner; omit for signer |
| 6 | recipient_token_0_account | mut, optional | recipient’s token0 ATA |
| 7 | recipient_token_1_account | mut, optional | recipient’s token1 ATA |
| 8 | token_0 | readonly | token0 mint |
| 9 | token_1 | readonly | token1 mint |
| 10 | token_0_reserve | mut | Liquidity Layer reserve for token0 |
| 11 | token_1_reserve | mut | Liquidity Layer reserve for token1 |
| 12 | token_0_rate_model | readonly | Liquidity Layer rate model for token0 |
| 13 | token_1_rate_model | readonly | Liquidity Layer rate model for token1 |
| 14 | token_0_vault | mut | Liquidity Layer vault ATA for token0 |
| 15 | token_1_vault | mut | Liquidity Layer vault ATA for token1 |
| 16 | dex_supply_position_token_0 | mut, optional | pool’s token0 supply position |
| 17 | dex_supply_position_token_1 | mut, optional | pool’s token1 supply position |
| 18 | dex_borrow_position_token_0 | mut, optional | pool’s token0 borrow position |
| 19 | dex_borrow_position_token_1 | mut, optional | pool’s token1 borrow position |
| 20 | liquidity | readonly | Liquidity Layer root account |
| 21 | liquidity_program | readonly | Liquidity Layer program |
| 22 | token_0_program | readonly | Token / Token-2022 for token0 |
| 23 | token_1_program | readonly | Token / Token-2022 for token1 |
| 24 | oracle_program | readonly | Oracle program |
| 25 | associated_token_program | readonly | ATA program |
Calling swap_in / swap_out
dex::cpi::swap_out(cpi_ctx, swap0to1, amount_out, amount_in_max)?.get(), which returns the amount_in spent.
Reading the return value
Both instructions returnResult<u64>. Through CPI that arrives as an Anchor Return<u64>; call .get(). This is the authoritative amount (the same value the LogSwap event records). No get_return_data boilerplate or log parsing needed.
Signing as a PDA
If the swapper is a PDA your program owns (the usual case: a vault paying from its own token account), build the context withnew_with_signer and your seeds:
user_token_*_accounts must have authority = signer: the PDA must own the token accounts funds leave from.
External center price and remaining accounts
Pools priced by an external oracle require the oracle plus its price sources as leading remaining accounts, in order[center_price_address, ...sources]. remaining_accounts[0] must equal the pool’s center_price_address, or the swap fails with DexInvalidExternalCenterPrice (6068) / DexMissingExternalCenterPrice (6067).
Dex.center_price_address (all-zero means internal) to tell which a pool uses.
Edge cases and gotchas
- All four positions required. The most common CPI mistake.
Nonefails with error6077. token0/token1ordering is fixed by pubkey sort, not by your call.swap0to1 = truealways means “lower-pubkey mint to higher-pubkey mint”. Derive the direction from the mints you hold.- Token-2022.
token_0_program/token_1_programmust each match the mint’s owning program; a pool can mix classic SPL and Token-2022. Don’t hardcodeTOKEN_PROGRAM_ID. - All-or-nothing. A routed swap performs up to two Liquidity Layer CPIs plus the oracle update; if any leg reverts, the whole swap reverts (no partial fill).
- Re-entrancy lock. The pool locks for the duration; a second swap on the same pool within one instruction fails with
DexAlreadyEntered(6017). - Compute budget. A two-pool routed swap plus oracle update is CU-heavy. Add a
ComputeBudgetProgram::set_compute_unit_limitto the outer transaction; the default 200k is often not enough with your own logic included. ADDRESS_DEADrecipient reverts. Ifrecipientresolves to the all-zero pubkey, the instruction intentionally errors (DexSwapInResult6081 /DexSwapOutResult6082). That’s the off-chain simulation sentinel; never pass it from a real CPI.- Slippage still applies.
amount_out_min/amount_in_maxare enforced inside the CPI. Pass a real bound;0/u64::MAXdisables the guard. - Dust and size floors apply as they do off-chain. See
DexLimitingAmountsSwapAndNonPerfect(6060) andDexSwapInLimitingAmounts(6052).
Raw instruction (without Anchor)
If you can’t usedeclare_program!, build the instruction by hand. Discriminator (8 bytes) plus Borsh args, in order:
| Instruction | Discriminator (bytes) | Args (Borsh) |
|---|---|---|
swap_in | [141,172,10,208,69,9,56,154] | swap0to1: bool, amount_in: u64, amount_out_min: u64 |
swap_out | [206,36,149,14,163,132,148,1] | swap0to1: bool, amount_out: u64, amount_in_max: u64 |
bool is 1 byte (0/1); each u64 is 8 bytes little-endian. So the data is discriminator ++ [swap0to1] ++ amount_a.to_le_bytes() ++ amount_b.to_le_bytes() (17 bytes of args).
Account metas follow the 25-account order above with the listed signer / writable flags, then any center-price remaining accounts (all readonly). For an omitted optional account (recipient when you want output to go to the signer), pass the AMM program id as the placeholder pubkey (readonly, non-signer) in that slot. This is the Anchor convention for None. Then invoke / invoke_signed the Instruction.
See Errors for the full swap-path error table.