Rust SDK
Example code to interact with Phoenix in Rust
Get all markets
You can get all markets by calling the getProgramAccounts
RPC, filtering for accounts that match the market discriminant. Sample code available here.
Viewing the state of the order book
To get the state of a market's order book, call SDKClient.get_market_orderbook(&market_pubkey)
. The order book can be pretty-printed with orderbook.print_ladder()
.
let client = EllipsisClient::from_rpc(
RpcClient::new_with_commitment(
"https://api.mainnet-beta.solana.com".to_string(),
CommitmentConfig::confirmed(),
),
&payer,
)?;
let sdk_client = SDKClient::new_from_ellipsis_client(client).await?;
let sol_market = Pubkey::from_str("4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg")?;
let orderbook = sdk_client.get_market_orderbook(&sol_market).await?;
orderbook.print_ladder(5, 4);
19.9240 214.3670
19.9090 53.5920
46.8330 19.8860
37.4840 19.8710
Fetching market events
All market events are logged for easy data indexing and trader convenience. Events are recorded in instruction data via an authorized self-CPI to ensure no logs are dropped.
By listening to all transactions that touch a given market, one can create a stream of all market events as they are confirmed by the blockchain with a polling loop. For latency sensitive operations, look into running a Geyser Plugin.
let payer = Keypair::new();
let client = EllipsisClient::from_rpc(
RpcClient::new_with_commitment(
"https://api.mainnet-beta.solana.com".to_string(),
CommitmentConfig::confirmed(),
),
&payer,
)?;
let sdk_client = Arc::new(SDKClient::new_from_ellipsis_client(client).await?);
let sol_market = Pubkey::from_str("4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg")?;
let mut until = None;
loop {
let config = match until {
None => GetConfirmedSignaturesForAddress2Config {
before: None,
until: None,
limit: Some(1),
commitment: Some(CommitmentConfig::confirmed()),
},
Some(until) => GetConfirmedSignaturesForAddress2Config {
before: None,
until: Some(until),
limit: None,
commitment: Some(CommitmentConfig::confirmed()),
},
};
let signatures = sdk_client
.client
.get_signatures_for_address_with_config(&sol_market, config)
.await
.unwrap_or_default()
.iter()
.map(|tx| Signature::from_str(&tx.signature).unwrap())
.rev()
.collect::<Vec<_>>();
if !signatures.is_empty() {
until = Some(signatures[0]);
}
let mut handles = vec![];
for signature in signatures {
let sdk = sdk_client.clone();
let handle =
tokio::spawn(
async move { sdk.parse_events_from_transaction(&signature).await }
);
handles.push(handle);
}
for handle in handles {
let events = handle.await?;
events.map(|events| {
events.iter().for_each(|e| {
// Here we only print the event, but in practice, you can do
// a lot more
println!("{:#?}", e);
});
});
}
// Note: this is a basic polling loop, if there are >1000 signatures in 200ms
// events will get dropped
tokio::time::sleep(Duration::from_millis(200)).await;
}
Sample output:
PhoenixEvent {
market: 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg,
sequence_number: 10048932,
slot: 185021845,
timestamp: 1679941569,
signature: 2AH2Ywtk9fiR1PLmcQE1SrPRWpwMyfiB2Rr9QSwapw4DUBpNExst6haPEkjwaSCgAFt4jMYGqhVkUVfDd6D6zkTg,
signer: 3HBWHuyxWv4uN8U8SeukocrWPfLZJqrtj9DgDHsGo2HR,
event_index: 1,
details: Place(
Place {
order_sequence_number: 8079036,
client_order_id: 3,
maker: 3HBWHuyxWv4uN8U8SeukocrWPfLZJqrtj9DgDHsGo2HR,
price_in_ticks: 19909,
base_lots_placed: 214567,
},
),
}
Placing cross orders
Directly call the send_ioc
function exposed by the SDK.
let (_sig, fills) = sdk
.send_ioc(market_key, price_in_ticks * tick_size, side, size_in_base_lots)
.await
.unwrap();
Alternatively, to batch multiple instructions into a single atomic transaction, use get_ioc_ix
to create instructions and send them manually.
let ixs = ioc_orders_to_place
.iter()
.map(|(price, side, size)| sdk.get_ioc_ix(market_key, price, side, size))
.collect::<Vec>();
let signature = sdk
.client
.sign_send_instructions(ixs, vec![])
.await
.ok()?;
let fills = sdk.parse_fills(&signature).await;
Placing limit orders
Directly call the send_limit_order
function exposed by the SDK. Alternatively, use get_limit_order_ix
to create instructions and send them manually.
Canceling orders
Use send_cancel_ids
, send_cancel_multiple
, and send_cancel_all
functions to cancel orders.
Place and cancel example
Here is an end-to-end example of how to use a generated client_order_id
to send a limit order and find its corresponding exchange order ID. After sending the order, we immediately cancel it using the ID that we fetched from the previous step.
// Replace with a keypair that has SOL in it
// The corresponding pubkey must have a seat on the market
let payer = Keypair::new();
let client = EllipsisClient::from_rpc(
RpcClient::new_with_commitment(
"https://api.mainnet-beta.solana.com".to_string(),
CommitmentConfig::confirmed(),
),
&payer,
)?;
let mut sdk_client = SDKClient::new_from_ellipsis_client(client).await?;
let mut rng = StdRng::from_entropy();
let sol_market =
Pubkey::from_str("4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg")?;
sdk_client.add_market(&sol_market).await?;
// Pulls the fair price for SOL/USD from the Coinbase API
let response = reqwest::get("https://api.coinbase.com/v2/prices/SOL-USD/spot")
.await?
.json::<serde_json::Value>()
.await?;
let sol_price = f64::from_str(response["data"]["amount"].as_str().unwrap())?;
let client_order_id = sdk_client.get_next_client_order_id(&mut rng);
let tick_price =
sdk_client.float_price_to_ticks_rounded_down(&sol_market, sol_price)?;
let base_lots_to_quote =
sdk_client.raw_base_units_to_base_lots_rounded_down(&sol_market, 0.01)?;
let place_ix = sdk_client.get_post_only_ix_from_tick_price(
&sol_market, // Market pubkey
tick_price, // Price of the order to place in ticks
Side::Bid, // Side of the order
base_lots_to_quote, // Number of base lots to quote
client_order_id, // Client order id (used to lookup the exchange order id)
false, // Will fail the transaction if the order crosses
)?;
// Returns the signature from the transaction to place limit order
let txid = sdk_client
.client
.sign_send_instructions(vec![place_ix], vec![])
.await?;
// Fetches the emitted events from the place order transaction
let phoenix_events = sdk_client
.parse_events_from_transaction(&txid)
.await
.ok_or_else(|| anyhow::anyhow!("No events found"))?;
// Iterates through the events and finds the exchange order id
// that matches the client order id
let mut order_id = None;
phoenix_events.iter().for_each(|e| match e.details {
MarketEventDetails::Place(ref place) => {
if place.client_order_id == client_order_id {
if order_id.is_some() {
// If you encounter a duplicate in general, you should handle it
// more gracefully
panic!("Duplicate order id");
}
order_id = Some(FIFOOrderId::new_from_untyped(
place.price_in_ticks,
place.order_sequence_number,
));
}
}
_ => unreachable!(),
});
println!("Client order id: {}", client_order_id);
println!("Order placed: {:#?}", order_id,);
// Immediately cancel the order that was just placed
let (_, reduce_events) = sdk_client
.send_cancel_ids(
&sol_market,
order_id.map(|o_id| vec![o_id]).unwrap_or_default(),
)
.await
.ok_or_else(|| anyhow::anyhow!("Failed to send cancel"))?;
for event in reduce_events {
println!("Reduce event: {:#?}", event);
}
Last updated