Skip to main content
Use the Rise Rust CPI surface when your Solana program already has AccountInfo handles and needs to invoke Phoenix, Ember, Flight, or Hawkeye directly. The SDK does not fetch accounts or derive every PDA at CPI time. Your program validates and resolves accounts, then adapts them into typed CPI contexts such as phoenix::PlaceMarketOrder, phoenix::PlaceStopLoss, or hawkeye::ViewMargin. Reference implementations:

Example Program Map

Use the example program as the source of truth for complete account lists. The docs below show the shape of each CPI, but the linked files show the full outer instruction, parameter struct, account metas, LiteSVM test setup, and log assertions.
FlowProgram codeTest coverage
Market order, limit order, cancel by id, matching-engine return data, and Hawkeye margin readmarket.rs, place_market_order.rs, place_limit_order.rs, cancel_limit_order.rsmarket_orders.rs
Ember wrap then Phoenix depositdeposit_ember_then_phoenix.rsdeposit_withdraw.rs
Phoenix withdraw then Ember unwrapwithdraw_phoenix_then_ember.rsdeposit_withdraw.rs
Stop-loss placement and cancellationplace_stop_loss.rs, cancel_stop_loss.rsstop_loss.rs
Register isolated subaccount, sync parent capabilities, transfer collateral, trade, close, and sweepregister_subaccount_sync_transfer_and_market_order.rs, close_subaccount_position_and_sweep.rssubaccounts.rs
Shared fixture setup, dynamic accounts, account metas, compute budget, and log helperstests/common/mod.rsprograms/example-program/tests

Dependencies

Most on-chain programs should depend on the phoenix-rise facade with only the cpi feature enabled. That profile exposes account byte decoders, instruction layouts, and Pinocchio CPI helpers without the HTTP, WebSocket, RPC, or transaction-builder graph.
Cargo
[dependencies]
phoenix-rise = { version = "0.3", default-features = false, features = ["cpi"] }
pinocchio = "0.9.4"

# Optional, but used by the public example program for instruction params,
# program ids, and program-id validation helpers.
borsh = { version = "1.6", features = ["derive"] }
pinocchio-pubkey = "0.3"
solana-pubkey = { version = "~3.0", default-features = false }
If you only want the instruction and CPI layer, depend on phoenix-rise-ix directly.
Cargo
[dependencies]
phoenix-rise-ix = { version = "0.3", default-features = false, features = ["cpi"] }
pinocchio = "0.9.4"
The public example program uses the facade from the local workspace:
Cargo
phoenix-rise = { path = "../../rust/sdk", default-features = false, features = [
  "cpi",
] }
Source: example-program/Cargo.toml.

Imports

Using the facade crate:
Rust
use phoenix_rise::ix;
use phoenix_rise::ix::types::{OrderFlags, SelfTradeBehavior, Side};
use pinocchio::account_info::AccountInfo;
use pinocchio::program_error::ProgramError;
use pinocchio::{ProgramResult, msg};
Using the low-level instruction crate directly:
Rust
use phoenix_rise_ix::cpi::{CpiScratch, hawkeye, phoenix};
use phoenix_rise_ix::types::{OrderFlags, SelfTradeBehavior, Side};
use pinocchio::account_info::AccountInfo;
use pinocchio::program_error::ProgramError;

CPI Shape

The typed CPI contexts are account-context structs. They borrow accounts from your outer instruction and encode the expected Phoenix account order for you. They do not own accounts and they do not resolve missing accounts from chain state. Phoenix order CPIs usually need:
  • Phoenix program id and log authority.
  • Global config and PerpAssetMap.
  • Trader signer or delegated authority, plus the trader account.
  • Market orderbook and spline collection.
  • Dynamic global_trader_index accounts.
  • Dynamic active_trader_buffer accounts.
  • Optional accounts for stop losses, conditionals, Hawkeye, Ember, or Flight.
The example program keeps a fixed account prefix and passes the dynamic global_trader_index and active_trader_buffer accounts at the tail. The tail counts are carried in instruction data, so the loader can split the account slice without allocation.
Rust
pub(crate) fn dynamic_tail<'a>(
    accounts: &'a [AccountInfo],
    fixed_account_count: usize,
    global_trader_index_count: usize,
    active_trader_buffer_count: usize,
) -> Result<(&'a [AccountInfo], &'a [AccountInfo]), ProgramError> {
    let gti_end = fixed_account_count
        .checked_add(global_trader_index_count)
        .ok_or(ProgramError::InvalidInstructionData)?;
    let expected = gti_end
        .checked_add(active_trader_buffer_count)
        .ok_or(ProgramError::InvalidInstructionData)?;

    require_exact_accounts(accounts, expected)?;
    Ok((
        &accounts[fixed_account_count..gti_end],
        &accounts[gti_end..expected],
    ))
}
Source: example-program/src/common.rs.

Market Context

A useful integration pattern is to create one outer context that knows how to load and validate your instruction accounts, then add small helper methods that project those accounts into the SDK’s typed CPI contexts.
Rust
use phoenix_rise::ix;
use pinocchio::account_info::AccountInfo;
use pinocchio::program_error::ProgramError;
use pinocchio::{ProgramResult, msg};

use crate::common::dynamic_tail;
use crate::cpi::check_program_id;

pub(crate) struct MarketContext<'a> {
    phoenix_program: &'a AccountInfo,
    hawkeye_program: &'a AccountInfo,
    log_authority: &'a AccountInfo,
    global_config: &'a AccountInfo,
    trader: &'a AccountInfo,
    trader_account: &'a AccountInfo,
    perp_asset_map: &'a AccountInfo,
    orderbook: &'a AccountInfo,
    spline_collection: &'a AccountInfo,
    global_trader_index: &'a [AccountInfo],
    active_trader_buffer: &'a [AccountInfo],
}

impl<'a> MarketContext<'a> {
    const FIXED_ACCOUNT_COUNT: usize = 9;

    pub(crate) fn load(
        accounts: &'a [AccountInfo],
        global_trader_index_count: usize,
        active_trader_buffer_count: usize,
    ) -> Result<Self, ProgramError> {
        let (global_trader_index, active_trader_buffer) = dynamic_tail(
            accounts,
            Self::FIXED_ACCOUNT_COUNT,
            global_trader_index_count,
            active_trader_buffer_count,
        )?;

        let context = Self {
            phoenix_program: &accounts[0],
            hawkeye_program: &accounts[1],
            log_authority: &accounts[2],
            global_config: &accounts[3],
            trader: &accounts[4],
            trader_account: &accounts[5],
            perp_asset_map: &accounts[6],
            orderbook: &accounts[7],
            spline_collection: &accounts[8],
            global_trader_index,
            active_trader_buffer,
        };
        context.validate()?;
        Ok(context)
    }

    fn validate(&self) -> ProgramResult {
        check_program_id(self.phoenix_program, &ix::PHOENIX_PROGRAM_ID, "Phoenix")?;
        check_program_id(self.hawkeye_program, &ix::HAWKEYE_PROGRAM_ID, "Hawkeye")?;
        if !self.trader.is_signer() {
            msg!("trader must sign market action");
            return Err(ProgramError::MissingRequiredSignature);
        }
        Ok(())
    }

    fn place_market_order_accounts(&self) -> ix::cpi::phoenix::PlaceMarketOrder<'a> {
        ix::cpi::phoenix::PlaceMarketOrder {
            phoenix_program: self.phoenix_program,
            log_authority: self.log_authority,
            global_config: self.global_config,
            trader: self.trader,
            trader_account: self.trader_account,
            perp_asset_map: self.perp_asset_map,
            global_trader_index: self.global_trader_index,
            active_trader_buffer: self.active_trader_buffer,
            orderbook: self.orderbook,
            spline_collection: self.spline_collection,
        }
    }
}
Source: example-program/src/market.rs.

Invoke Phoenix

Once your outer context can build the typed CPI context, the invoke step is small: create CpiScratch, pass instruction-specific args, and let the SDK write the account metas and instruction data.
Rust
use crate::cpi::MAX_CPI_ACCOUNTS;
use crate::params::MarketOrderCpiParams;

impl<'a> MarketContext<'a> {
    pub(crate) fn invoke_market_order(
        &self,
        params: &MarketOrderCpiParams,
    ) -> ProgramResult {
        let market_order = self.place_market_order_accounts();

        let mut scratch = ix::cpi::CpiScratch::<
            { MAX_CPI_ACCOUNTS },
            { ix::cpi::phoenix::PlaceMarketOrder::MAX_DATA_LEN },
        >::new(self.phoenix_program);

        market_order.invoke(
            ix::cpi::phoenix::PlaceMarketOrderArgs {
                side: params.side,
                price_in_ticks: params.price_in_ticks,
                num_base_lots: params.num_base_lots,
                num_quote_lots: params.num_quote_lots,
                min_base_lots_to_fill: params.min_base_lots_to_fill,
                min_quote_lots_to_fill: params.min_quote_lots_to_fill,
                self_trade_behavior: params.self_trade_behavior,
                match_limit: params.match_limit,
                client_order_id: params.client_order_id,
                last_valid_slot: params.last_valid_slot,
                order_flags: params.order_flags,
                cancel_existing: params.cancel_existing,
            },
            &mut scratch,
        )
    }
}
CpiScratch owns fixed-size stack buffers for account infos, account metas, and instruction data. For dynamic market accounts, size it to your integration’s upper bound and check ctx.account_count() before invoking if the account count is user-controlled. If you already have reusable storage, use CpiBuffers and invoke_with_buffers(...) instead.

Stop Losses And Conditionals

Stop losses and conditional orders follow the same pattern, but the account context includes the funder, position authority, conditional or stop-loss account, and system program. Creating a stop-loss or conditional-order account can incur rent if the account does not already exist.
Rust
let place_stop_loss = ix::cpi::phoenix::PlaceStopLoss {
    phoenix_program,
    log_authority,
    global_config,
    funder,
    trader_account,
    perp_asset_map,
    global_trader_index,
    active_trader_buffer,
    orderbook,
    spline_collection,
    position_authority,
    stop_loss_account,
    system_program,
};

let mut scratch = ix::cpi::CpiScratch::<
    { MAX_CPI_ACCOUNTS },
    { ix::cpi::phoenix::PlaceStopLoss::DATA_LEN },
>::new(phoenix_program);

place_stop_loss.invoke(
    ix::cpi::phoenix::PlaceStopLossArgs {
        trigger_price,
        execution_price,
        trade_side,
        execution_direction,
        order_kind,
    },
    &mut scratch,
)?;
Source: example-program/src/place_stop_loss.rs.

Collateral CPIs

Collateral flows often compose Ember and Phoenix CPIs in one outer instruction. The example program wraps fake USDC into Phoenix collateral through Ember, then deposits that collateral into the Phoenix trader account.
Rust
let ember_deposit = ix::cpi::ember::EmberDeposit {
    ember_program,
    trader,
    ember_state,
    usdc_mint,
    canonical_mint,
    trader_usdc_account,
    trader_phoenix_account,
    ember_vault,
    token_program,
};
let mut ember_scratch = ix::cpi::CpiScratch::<
    { ix::cpi::ember::EmberDeposit::ACCOUNT_COUNT },
    { ix::cpi::ember::EmberDeposit::DATA_LEN },
>::new(ember_program);
ember_deposit.invoke(
    ix::cpi::ember::EmberDepositArgs { amount: ember_amount },
    &mut ember_scratch,
)?;

let phoenix_deposit = ix::cpi::phoenix::PhoenixDeposit {
    phoenix_program,
    log_authority,
    global_config,
    trader,
    trader_token_account: trader_phoenix_account,
    trader_account,
    global_vault,
    token_program,
    global_trader_index,
    active_trader_buffer,
    permission_account: None,
};
let mut phoenix_scratch = ix::cpi::CpiScratch::<
    { MAX_CPI_ACCOUNTS },
    { ix::cpi::phoenix::PhoenixDeposit::DATA_LEN },
>::new(phoenix_program);
phoenix_deposit.invoke(
    ix::cpi::phoenix::PhoenixDepositArgs { amount: phoenix_amount },
    &mut phoenix_scratch,
)?;
Source: example-program/src/deposit_ember_then_phoenix.rs. Withdrawals reverse the sequence: Phoenix withdraw first, then Ember unwrap. Source: example-program/src/withdraw_phoenix_then_ember.rs.

Subaccount CPIs

Subaccount flows are a good example of composing several typed contexts. The example registers the child trader account, syncs parent capabilities, transfers collateral to the child, then reuses MarketContext to place the child order.
Rust
register.invoke(
    ix::cpi::phoenix::RegisterTraderArgs {
        max_positions,
        trader_pda_index,
        subaccount_index: child_subaccount_index,
    },
    &mut register_scratch,
)?;

sync.invoke(
    ix::cpi::phoenix::SyncParentToChildArgs,
    &mut sync_scratch,
)?;

transfer.invoke(
    ix::cpi::phoenix::TransferCollateralArgs { amount },
    &mut transfer_scratch,
)?;

let market_context = MarketContext::from_refs(MarketAccountRefs {
    phoenix_program,
    hawkeye_program,
    log_authority,
    global_config,
    trader: trader_authority,
    trader_account: child_trader_account,
    perp_asset_map,
    orderbook,
    spline_collection,
    global_trader_index,
    active_trader_buffer,
})?;
market_context.invoke_market_order_without_arenas(&order, "subaccount-open-order")?;
Source: example-program/src/register_subaccount_sync_transfer_and_market_order.rs.

Return Data

Phoenix order CPIs can return matching-engine data. Decode return data instead of parsing logs.
Rust
use phoenix_rise::ix::return_data::decode_matching_engine_cpi_response;
use pinocchio::cpi::get_return_data;

let Some(return_data) = get_return_data() else {
    return Ok(());
};

let response = decode_matching_engine_cpi_response(return_data.as_slice())?;
if let Some(order_id) = response.order_id() {
    msg!(&format!(
        "order_id_price_in_ticks={} order_sequence_number={}",
        order_id.price_in_ticks,
        order_id.order_sequence_number,
    ));
}
Source: example-program/src/common.rs.

Hawkeye Views

Hawkeye is the read-only program for authoritative margin, liquidation-price, BBO, and funding views. Invoke it when local SDK math is not enough and you need the result the on-chain programs would use. Hawkeye writes versioned return data; decode those bytes instead of parsing logs. Hawkeye program id: RiSeVw3ZjNfsaXPRb4mgaqYaEEt41pNNJoDvVh7pgQj. Use it to validate the CPI program account on-chain and to assert that simulation return data came from Hawkeye off-chain.
import { HAWKEYE_PROGRAM_ADDRESS } from "@ellipsis-labs/rise";

const result = await client.rpc.hawkeye.viewMargin({
  authority: "AUTHORITY_PUBKEY",
  traderPdaIndex: 0,
  traderSubaccountIndex: 0,
});

if (result.returnData?.programId !== HAWKEYE_PROGRAM_ADDRESS) {
  throw new Error("unexpected Hawkeye return-data program");
}
Hawkeye instructions are normal Solana instructions, so they can be invoked off-chain in a transaction simulation or on-chain through CPI. The current typed Pinocchio CPI helper in the Rust SDK is ix::cpi::hawkeye::ViewMargin; the other views are exposed through off-chain instruction builders and can be mirrored on-chain with the same account metas and discriminators if your program needs them.
ViewHawkeye ixReturn dataSDK entry points
Account marginview_marginViewMarginReturn / HawkeyeMarginReturnOn-chain ix::cpi::hawkeye::ViewMargin; TypeScript client.rpc.hawkeye.viewMargin(...) or buildHawkeyeViewMarginIx(...); Rust PhoenixHawkeyeClient::view_margin(...) or create_hawkeye_view_margin_ix(...)
Asset marginview_margin_for_assetViewAssetReturn / HawkeyeAssetReturnTypeScript client.rpc.hawkeye.viewMarginForAsset(...) or buildHawkeyeViewMarginForAssetIx(...); Rust PhoenixHawkeyeClient::view_margin_for_asset(...) or create_hawkeye_view_margin_for_asset_ix(...)
Liquidation priceview_liquidation_priceViewLiquidationPriceReturn / HawkeyeLiquidationPriceReturnTypeScript client.rpc.hawkeye.viewLiquidationPrice(...) or buildHawkeyeViewLiquidationPriceIx(...); Rust PhoenixHawkeyeClient::view_liquidation_price(...) or create_hawkeye_view_liquidation_price_ix(...)
Best bid/offerview_bboViewBboReturn / HawkeyeBboReturnTypeScript client.rpc.hawkeye.viewBbo(...) or buildHawkeyeViewBboIx(...); Rust PhoenixHawkeyeClient::view_bbo(...) or create_hawkeye_view_bbo_ix(...)
Fundingview_fundingViewFundingReturn / HawkeyeFundingReturnTypeScript client.rpc.hawkeye.viewFunding(...) or buildHawkeyeViewFundingIx(...); Rust PhoenixHawkeyeClient::view_funding(...) or create_hawkeye_view_funding_ix(...)
Sources: rust/ix/src/hawkeye.rs, ts/src/hawkeye.ts, rust/core/src/hawkeye_client.rs, and ts/src/rpc.ts.

On-chain CPI

On-chain programs read Hawkeye return data the same way they read Phoenix return data: invoke the program, call get_return_data() immediately, assert the return-data program id is Hawkeye, then decode the expected struct. Return data is overwritten by the next CPI that sets it, so decode or copy it before another Phoenix or Hawkeye call. The example program invokes hawkeye::ViewMargin after market orders and decodes the return into ViewMarginReturn.
Rust
let view_margin = ix::cpi::hawkeye::ViewMargin {
    hawkeye_program,
    phoenix_program,
    global_config,
    global_trader_index,
    active_trader_buffer,
    perp_asset_map,
    trader: trader_account,
};

let mut scratch = ix::cpi::CpiScratch::<
    { MAX_CPI_ACCOUNTS },
    { ix::cpi::hawkeye::ViewMargin::DATA_LEN },
>::new(hawkeye_program);

let margin = view_margin.invoke_and_decode(&mut scratch)?;
msg!(&format!(
    "collateral={} free={} maintenance={} liquidatable={}",
    margin.collateral_quote_lots,
    margin.free_collateral_quote_lots,
    margin.maintenance_margin_quote_lots,
    margin.is_liquidatable,
));
If you need the manual decode path, use the same guard the helper uses internally.
Rust
use phoenix_rise::ix::hawkeye::{ViewMarginReturn, decode_hawkeye_return};
use pinocchio::cpi::get_return_data;

let return_data = get_return_data().ok_or(ProgramError::InvalidAccountData)?;
if return_data.program_id() != &ix::HAWKEYE_PROGRAM_ID.to_bytes() {
    return Err(ProgramError::InvalidAccountData);
}

let margin = decode_hawkeye_return::<ViewMarginReturn>(
    return_data.as_slice(),
    "view_margin",
)
.map_err(|_| ProgramError::InvalidAccountData)?;
Source: example-program/src/market.rs and rust/ix/src/cpi.rs.

Off-chain simulation

For applications and services, prefer the SDK Hawkeye RPC helpers. They build a read-only simulation transaction, validate that return data came from the Hawkeye program id, and decode the bytes into the typed return shape.
const margin = await client.rpc.hawkeye.viewMargin({
  authority: "AUTHORITY_PUBKEY",
  traderPdaIndex: 0,
  traderSubaccountIndex: 0,
});

const liquidation = await client.rpc.hawkeye.viewLiquidationPrice({
  authority: "AUTHORITY_PUBKEY",
  traderPdaIndex: 0,
  traderSubaccountIndex: 0,
  symbol: "SOL",
});

const bbo = await client.rpc.hawkeye.viewBbo({ symbol: "SOL" });

console.log(margin.returnData?.decoded.freeCollateralQuoteLots);
console.log(liquidation.returnData?.decoded.liquidationPriceTicks);
console.log(bbo.returnData?.decoded.markPriceTicks);
If you already have accounts resolved, you can build a Hawkeye instruction directly and still let the SDK decode the return data.
import { buildHawkeyeViewLiquidationPriceIx } from "@ellipsis-labs/rise";

const ix = buildHawkeyeViewLiquidationPriceIx({
  phoenixProgramAddress,
  globalConfigurationAddress,
  globalTraderIndex,
  activeTraderBuffer,
  perpAssetMap,
  traderAccount,
  assetId: 0,
});

const simulation = await client.rpc.hawkeye.simulateInstruction(ix);
const decoded = simulation.returnData?.decoded ?? null;

if (decoded?.kind === "view_liquidation_price") {
  console.log(decoded.liquidationPriceTicks);
}
Reference coverage: ts/tests/sdk-localnet-flows.test.ts executes all five Hawkeye views in LiteSVM, and rust/sdk/tests/sdk_localnet_fixture_tests.rs asserts Hawkeye return data is emitted by the expected program id.

Supported CPI Contexts

The typed CPI surface includes:
  • Trader lifecycle: RegisterTrader, SetTraderCapabilitiesDelegated, UpdateTraderState, SyncParentToChild.
  • Order flow: PlaceMarketOrder, PlaceLimitOrder, PlaceMarketOrderDelegated, PlaceMultiLimitOrder, CancelAll, CancelUpTo, CancelOrdersById, CancelAllPlusConditional.
  • Collateral and subaccounts: PhoenixDeposit, PhoenixWithdraw, TransferCollateral, TransferCollateralChildToParent.
  • Stop losses and conditionals: CreateConditionalOrdersAccount, PlaceStopLoss, CancelStopLoss, PlacePositionConditionalOrder, PlaceAttachedConditionalOrder, PlaceLimitOrderWithConditionals, CancelConditionalOrder.
  • Ember collateral movement: ember::EmberDeposit, ember::EmberWithdraw.
  • Hawkeye reads: hawkeye::ViewMargin / hawkeye::HawkeyeViewMargin.
For full account lists and data lengths, inspect rust/ix/src/cpi.rs.

Testing

Use LiteSVM Testing for local integration tests. The example program tests cover deposits, withdrawals, market orders, limit orders, cancels, stop losses, Hawkeye reads, and isolated subaccount collateral flows.