Expand description
§Trading Guide
This guide covers common trading patterns using the O2 Rust SDK.
See also:
O2ClientAPI reference. Identifier reference: Identifiers and Wallet Types
Fuel-native wallets are best for broad interop with other Fuel ecosystem apps. EVM wallets are useful when reusing EVM accounts across chains and simplifying bridging from EVM chains. O2 owner identity is always B256; for EVM wallets, that B256 value is the EVM address zero-left-padded to 32 bytes.
§Order Types
The O2 Exchange supports six order types, specified via the OrderType enum
passed to O2Client::create_order.
use o2_sdk::{O2Client, Network, OrderType, Side};
let market = "fFUEL/fUSDC";§Spot (default)
A standard limit order that rests on the book if not immediately filled.
client.create_order(
&mut session, market, Side::Buy, "0.02", "100",
OrderType::Spot, true, true,
).await?;§PostOnly
Guaranteed to be a maker order. Rejected immediately if it would cross the spread and match an existing order.
client.create_order(
&mut session, market, Side::Buy, "0.02", "100",
OrderType::PostOnly, true, true,
).await?;§Market
Executes immediately at the best available price. Fails if the order book is empty.
client.create_order(
&mut session, market, Side::Buy, "0.03", "100",
OrderType::Market, true, true,
).await?;§FillOrKill
Must be filled entirely in a single match, or the entire order is rejected.
client.create_order(
&mut session, market, Side::Buy, "0.03", "100",
OrderType::FillOrKill, true, true,
).await?;§Limit
Like Spot, but includes a limit price and timestamp for time-in-force semantics:
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
client.create_order(
&mut session, market, Side::Buy, "0.02", "100",
OrderType::Limit { price: "0.025".parse()?, timestamp: now },
true, true,
).await?;§BoundedMarket
A market order with price bounds — executes at market price but only within the specified range:
client.create_order(
&mut session, market, Side::Buy, "0.025", "100",
OrderType::BoundedMarket { max_price: "0.03".parse()?, min_price: "0.01".parse()? },
true, true,
).await?;§Streamlined Batch Builder
For multi-action submissions in a single market, use
O2Client::actions_for to
build validated actions with the same flexible price/quantity input
types as create_order.
let actions = client
.actions_for("fFUEL/fUSDC")
.await?
.settle_balance()
.create_order(Side::Buy, "0.02", "100", OrderType::PostOnly)
.create_order(Side::Sell, "0.03", "100", OrderType::PostOnly)
.build()?;
let result = client
.batch_actions(&mut session, "fFUEL/fUSDC", actions, true)
.await?;§Cancel and Replace
Cancel an existing order:
// Cancel by order ID
client.cancel_order(&mut session, &"0xabc...".into(), market).await?;
// Cancel all open orders
client.cancel_all_orders(&mut session, market).await?;To atomically cancel-and-replace in a single transaction, use
O2Client::batch_actions with typed Action variants:
use o2_sdk::{Action, Side, OrderType};
let actions = vec![
Action::CancelOrder { order_id: old_order_id.into() },
Action::SettleBalance,
Action::CreateOrder {
side: Side::Buy,
price: new_price,
quantity: new_qty,
order_type: OrderType::Spot,
},
];
let result = client.batch_actions(&mut session, market, actions, true).await?;Important: Prices and quantities in
Action::CreateOrderare human-readable values. The SDK automatically scales them to on-chain integers using the market’s decimal configuration. For manual scaling, useMarket::scale_priceandMarket::scale_quantity.
§Settling Balances
When your orders are filled, the proceeds remain locked in the order book contract until they are settled back to your trading account.
O2Client::create_order handles this automatically when settle_first
is true (the default). You can also settle manually:
client.settle_balance(&mut session, market).await?;§Market Maker Pattern
A simple two-sided quoting loop using typed actions:
use o2_sdk::{O2Client, Network, Action, Side, OrderType};
use std::time::Duration;
let mut client = O2Client::new(Network::Testnet);
// ... setup wallet, account, session ...
let spread = 0.001;
let qty = 50.0;
let mut active_buy: Option<String> = None;
let mut active_sell: Option<String> = None;
loop {
// Get current mid price
let depth = client.get_depth(market, 1, None).await?;
let mid = if let (Some(bid), Some(ask)) = (depth.bids.first(), depth.asks.first()) {
let bid_price = market.format_price(bid.price);
let ask_price = market.format_price(ask.price);
(bid_price + ask_price) / 2.0
} else {
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
};
let buy_price = mid - spread / 2.0;
let sell_price = mid + spread / 2.0;
// Build batch: cancel old + settle + place new
let mut actions = Vec::new();
if let Some(ref id) = active_buy {
actions.push(Action::CancelOrder { order_id: id.clone().into() });
}
if let Some(ref id) = active_sell {
actions.push(Action::CancelOrder { order_id: id.clone().into() });
}
actions.push(Action::SettleBalance);
actions.push(Action::CreateOrder {
side: Side::Buy,
price: format!("{buy_price}").parse()?,
quantity: format!("{qty}").parse()?,
order_type: OrderType::PostOnly,
});
actions.push(Action::CreateOrder {
side: Side::Sell,
price: format!("{sell_price}").parse()?,
quantity: format!("{qty}").parse()?,
order_type: OrderType::PostOnly,
});
let result = client.batch_actions(&mut session, market, actions, true).await?;
if let Some(ref orders) = result.orders {
active_buy = orders.iter()
.find(|o| o.side == Side::Buy)
.map(|o| o.order_id.to_string());
active_sell = orders.iter()
.find(|o| o.side == Side::Sell)
.map(|o| o.order_id.to_string());
}
tokio::time::sleep(Duration::from_secs(15)).await;
}§Order Monitoring
Query order status:
// All orders for an account
let orders = client.get_orders(
&session.trade_account_id, market, None, 20,
).await?;
// Open orders only
let open_orders = client.get_orders(
&session.trade_account_id, market, Some(true), 20,
).await?;
// Single order by ID
let order = client.get_order(market, "0xabc...").await?;
println!("Status: {}", if !order.close { "open" } else { "closed" });
println!(
"Filled: {} / {}",
order.quantity_fill.unwrap_or(0),
order.quantity,
);For real-time order updates, use O2Client::stream_orders:
use o2_sdk::Identity;
use tokio_stream::StreamExt;
let identity = Identity::ContractId(session.trade_account_id.to_string());
let mut stream = client.stream_orders(&[identity]).await?;
while let Some(Ok(update)) = stream.next().await {
for order in &update.orders {
let status = if order.close { "closed" } else { "open" };
println!("Order {}: {}", order.order_id, status);
}
}§Withdrawals
Withdraw funds from the trading account to the owner wallet:
let result = client.withdraw(
&wallet,
&session,
&asset_id, // hex asset ID
"1000000000", // amount in chain integer string
None, // defaults to owner address
).await?;
println!("Withdrawal tx: {}", result.tx_id.unwrap_or_default());Note: Withdrawals require the owner wallet (not the session key) and use
personalSign.
§Nonce Management
The SDK automatically manages nonces during trading. If you encounter nonce errors after a failed transaction, refresh the nonce:
client.refresh_nonce(&mut session).await?;You can also fetch the current nonce directly:
let nonce = client.get_nonce(&session.trade_account_id).await?;