o2_sdk/
encoding.rs

1//! Fuel ABI encoding primitives for O2 Exchange.
2//!
3//! Implements the exact byte layouts from the O2 integration guide:
4//! - u64 big-endian encoding
5//! - Function selectors (NOT hash-based: u64(len) + utf8(name))
6//! - Identity encoding (discriminant + 32-byte address)
7//! - Option encoding (None = u64(0), Some = u64(1) + data)
8//! - OrderArgs struct encoding (tightly packed enum variants)
9//! - Session signing bytes
10//! - Action signing bytes
11
12/// Encode a u64 value as 8 bytes big-endian.
13pub fn u64_be(value: u64) -> [u8; 8] {
14    value.to_be_bytes()
15}
16
17/// Encode a Fuel ABI function selector: u64_be(len(name)) + utf8(name).
18/// These are NOT hash-based like Solidity selectors.
19pub fn function_selector(name: &str) -> Vec<u8> {
20    let name_bytes = name.as_bytes();
21    let mut result = Vec::with_capacity(8 + name_bytes.len());
22    result.extend_from_slice(&u64_be(name_bytes.len() as u64));
23    result.extend_from_slice(name_bytes);
24    result
25}
26
27/// Encode a Fuel Identity enum: u64(discriminant) + 32-byte address.
28/// discriminant: 0 = Address, 1 = ContractId
29pub fn encode_identity(discriminant: u64, address: &[u8; 32]) -> Vec<u8> {
30    let mut result = Vec::with_capacity(40);
31    result.extend_from_slice(&u64_be(discriminant));
32    result.extend_from_slice(address);
33    result
34}
35
36/// Encode Option::None: u64(0).
37pub fn encode_option_none() -> Vec<u8> {
38    u64_be(0).to_vec()
39}
40
41/// Encode Option::Some(data): u64(1) + data.
42pub fn encode_option_some(data: &[u8]) -> Vec<u8> {
43    let mut result = Vec::with_capacity(8 + data.len());
44    result.extend_from_slice(&u64_be(1));
45    result.extend_from_slice(data);
46    result
47}
48
49/// Encode Option for call_data in action signing bytes.
50/// None -> u64(0)
51/// Some -> u64(1) + u64(len(data)) + data
52pub fn encode_option_call_data(data: Option<&[u8]>) -> Vec<u8> {
53    match data {
54        None => u64_be(0).to_vec(),
55        Some(d) => {
56            let mut result = Vec::with_capacity(16 + d.len());
57            result.extend_from_slice(&u64_be(1));
58            result.extend_from_slice(&u64_be(d.len() as u64));
59            result.extend_from_slice(d);
60            result
61        }
62    }
63}
64
65/// Order type variants for encoding.
66#[derive(Debug, Clone)]
67pub enum OrderTypeEncoding {
68    Limit { price: u64, timestamp: u64 },
69    Spot,
70    FillOrKill,
71    PostOnly,
72    Market,
73    BoundedMarket { max_price: u64, min_price: u64 },
74}
75
76/// Encode OrderArgs struct for CreateOrder call_data.
77/// Layout: u64(price) + u64(quantity) + order_type_encoding (tightly packed)
78pub fn encode_order_args(price: u64, quantity: u64, order_type: &OrderTypeEncoding) -> Vec<u8> {
79    let mut result = Vec::with_capacity(40);
80    result.extend_from_slice(&u64_be(price));
81    result.extend_from_slice(&u64_be(quantity));
82
83    match order_type {
84        OrderTypeEncoding::Limit {
85            price: limit_price,
86            timestamp,
87        } => {
88            result.extend_from_slice(&u64_be(0));
89            result.extend_from_slice(&u64_be(*limit_price));
90            result.extend_from_slice(&u64_be(*timestamp));
91        }
92        OrderTypeEncoding::Spot => {
93            result.extend_from_slice(&u64_be(1));
94        }
95        OrderTypeEncoding::FillOrKill => {
96            result.extend_from_slice(&u64_be(2));
97        }
98        OrderTypeEncoding::PostOnly => {
99            result.extend_from_slice(&u64_be(3));
100        }
101        OrderTypeEncoding::Market => {
102            result.extend_from_slice(&u64_be(4));
103        }
104        OrderTypeEncoding::BoundedMarket {
105            max_price,
106            min_price,
107        } => {
108            result.extend_from_slice(&u64_be(5));
109            result.extend_from_slice(&u64_be(*max_price));
110            result.extend_from_slice(&u64_be(*min_price));
111        }
112    }
113
114    result
115}
116
117/// Build the signing bytes for set_session (Section 4.6 Step 3).
118///
119/// Layout:
120///   u64(nonce) + u64(chain_id) + u64(len("set_session")) + "set_session"
121///   + u64(1) [Option::Some] + u64(0) [Identity Address discriminant] + session_address(32)
122///   + u64(expiry) + u64(len(contract_ids)) + contract_ids(32 each)
123pub fn build_session_signing_bytes(
124    nonce: u64,
125    chain_id: u64,
126    session_address: &[u8; 32],
127    contract_ids: &[[u8; 32]],
128    expiry: u64,
129) -> Vec<u8> {
130    let func_name = b"set_session";
131
132    let mut result = Vec::with_capacity(128 + contract_ids.len() * 32);
133
134    // Nonce + chain_id
135    result.extend_from_slice(&u64_be(nonce));
136    result.extend_from_slice(&u64_be(chain_id));
137
138    // Function selector
139    result.extend_from_slice(&u64_be(func_name.len() as u64));
140    result.extend_from_slice(func_name);
141
142    // Option::Some
143    result.extend_from_slice(&u64_be(1));
144    // Identity::Address
145    result.extend_from_slice(&u64_be(0));
146    // Session address
147    result.extend_from_slice(session_address);
148    // Expiry
149    result.extend_from_slice(&u64_be(expiry));
150    // Contract IDs vec
151    result.extend_from_slice(&u64_be(contract_ids.len() as u64));
152    for cid in contract_ids {
153        result.extend_from_slice(cid);
154    }
155
156    result
157}
158
159/// A low-level contract call used in action signing.
160pub struct CallArg {
161    pub contract_id: [u8; 32],
162    pub function_selector: Vec<u8>,
163    pub amount: u64,
164    pub asset_id: [u8; 32],
165    pub gas: u64,
166    pub call_data: Option<Vec<u8>>,
167}
168
169/// Gas value: always u64::MAX. The API overrides with its own value.
170pub const GAS_MAX: u64 = u64::MAX;
171
172/// Build the signing bytes for session actions (Section 4.7 Step 2).
173///
174/// Layout:
175///   u64(nonce) + u64(num_calls) + for each call:
176///     contract_id(32) + u64(selector_len) + selector + u64(amount) + asset_id(32)
177///     + u64(gas) + encode_option_call_data(call_data)
178pub fn build_actions_signing_bytes(nonce: u64, calls: &[CallArg]) -> Vec<u8> {
179    let mut result = Vec::with_capacity(256);
180
181    result.extend_from_slice(&u64_be(nonce));
182    result.extend_from_slice(&u64_be(calls.len() as u64));
183
184    for call in calls {
185        result.extend_from_slice(&call.contract_id);
186        result.extend_from_slice(&u64_be(call.function_selector.len() as u64));
187        result.extend_from_slice(&call.function_selector);
188        result.extend_from_slice(&u64_be(call.amount));
189        result.extend_from_slice(&call.asset_id);
190        result.extend_from_slice(&u64_be(call.gas));
191        result.extend_from_slice(&encode_option_call_data(call.call_data.as_deref()));
192    }
193
194    result
195}
196
197/// Convert a high-level CreateOrder action to a low-level CallArg.
198#[allow(clippy::too_many_arguments)]
199pub fn create_order_to_call(
200    contract_id: &[u8; 32],
201    side: &str,
202    price: u64,
203    quantity: u64,
204    order_type: &OrderTypeEncoding,
205    base_decimals: u32,
206    base_asset: &[u8; 32],
207    quote_asset: &[u8; 32],
208) -> CallArg {
209    let call_data = encode_order_args(price, quantity, order_type);
210
211    let (amount, asset_id) = if side == "Buy" {
212        let amt = (price as u128 * quantity as u128) / 10u128.pow(base_decimals);
213        (amt as u64, *quote_asset)
214    } else {
215        (quantity, *base_asset)
216    };
217
218    CallArg {
219        contract_id: *contract_id,
220        function_selector: function_selector("create_order"),
221        amount,
222        asset_id,
223        gas: GAS_MAX,
224        call_data: Some(call_data),
225    }
226}
227
228/// Convert a CancelOrder action to a low-level CallArg.
229pub fn cancel_order_to_call(contract_id: &[u8; 32], order_id: &[u8; 32]) -> CallArg {
230    CallArg {
231        contract_id: *contract_id,
232        function_selector: function_selector("cancel_order"),
233        amount: 0,
234        asset_id: [0u8; 32],
235        gas: GAS_MAX,
236        call_data: Some(order_id.to_vec()),
237    }
238}
239
240/// Convert a SettleBalance action to a low-level CallArg.
241/// `to` is the destination identity (discriminant, address).
242pub fn settle_balance_to_call(
243    contract_id: &[u8; 32],
244    to_discriminant: u64,
245    to_address: &[u8; 32],
246) -> CallArg {
247    CallArg {
248        contract_id: *contract_id,
249        function_selector: function_selector("settle_balance"),
250        amount: 0,
251        asset_id: [0u8; 32],
252        gas: GAS_MAX,
253        call_data: Some(encode_identity(to_discriminant, to_address)),
254    }
255}
256
257/// Build the signing bytes for a withdrawal.
258///
259/// Layout:
260///   u64(nonce) + u64(chain_id) + u64(len("withdraw")) + "withdraw"
261///   + u64(to_discriminant) + to_address(32)
262///   + asset_id(32) + u64(amount)
263pub fn build_withdraw_signing_bytes(
264    nonce: u64,
265    chain_id: u64,
266    to_discriminant: u64,
267    to_address: &[u8; 32],
268    asset_id: &[u8; 32],
269    amount: u64,
270) -> Vec<u8> {
271    let func_name = b"withdraw";
272
273    let mut result = Vec::with_capacity(128);
274    result.extend_from_slice(&u64_be(nonce));
275    result.extend_from_slice(&u64_be(chain_id));
276    result.extend_from_slice(&u64_be(func_name.len() as u64));
277    result.extend_from_slice(func_name);
278    // to identity
279    result.extend_from_slice(&u64_be(to_discriminant));
280    result.extend_from_slice(to_address);
281    // asset_id
282    result.extend_from_slice(asset_id);
283    // amount
284    result.extend_from_slice(&u64_be(amount));
285
286    result
287}
288
289/// Convert a high-level `Action` to a low-level `CallArg` and JSON representation.
290///
291/// This is the typed counterpart to building calls manually. It handles
292/// price/quantity scaling internally using the `Market`.
293pub fn action_to_call(
294    action: &crate::models::Action,
295    market: &crate::models::Market,
296    trade_account_id: &str,
297    accounts_registry_id: Option<&[u8; 32]>,
298) -> Result<(CallArg, serde_json::Value), crate::errors::O2Error> {
299    use crate::crypto::parse_hex_32;
300    use crate::models::{Action, Identity};
301
302    let contract_id = parse_hex_32(&market.contract_id)?;
303
304    match action {
305        Action::CreateOrder {
306            side,
307            price,
308            quantity,
309            order_type,
310        } => {
311            let base_asset = parse_hex_32(market.base.asset.as_str())?;
312            let quote_asset = parse_hex_32(market.quote.asset.as_str())?;
313            let scaled_price = market.scale_price(price)?;
314            let scaled_quantity = market.scale_quantity(quantity)?;
315            let scaled_quantity = market.adjust_quantity(scaled_price, scaled_quantity)?;
316
317            market.validate_order(scaled_price, scaled_quantity)?;
318
319            let (ot_encoding, ot_json) = order_type.to_encoding(market)?;
320            let side_str = side.as_str();
321
322            let call = create_order_to_call(
323                &contract_id,
324                side_str,
325                scaled_price,
326                scaled_quantity,
327                &ot_encoding,
328                market.base.decimals,
329                &base_asset,
330                &quote_asset,
331            );
332
333            let json = serde_json::json!({
334                "CreateOrder": {
335                    "side": side_str,
336                    "price": scaled_price.to_string(),
337                    "quantity": scaled_quantity.to_string(),
338                    "order_type": ot_json
339                }
340            });
341
342            Ok((call, json))
343        }
344        Action::CancelOrder { order_id } => {
345            let order_id_bytes = parse_hex_32(order_id.as_str())?;
346            let call = cancel_order_to_call(&contract_id, &order_id_bytes);
347            let json = serde_json::json!({
348                "CancelOrder": { "order_id": order_id }
349            });
350            Ok((call, json))
351        }
352        Action::SettleBalance => {
353            let trade_account_bytes = parse_hex_32(trade_account_id)?;
354            let call = settle_balance_to_call(&contract_id, 1, &trade_account_bytes);
355            let json = serde_json::json!({
356                "SettleBalance": { "to": { "ContractId": trade_account_id } }
357            });
358            Ok((call, json))
359        }
360        Action::RegisterReferer { to } => {
361            let registry_id = accounts_registry_id.ok_or_else(|| {
362                crate::errors::O2Error::Other(
363                    "accounts_registry_id required for RegisterReferer".into(),
364                )
365            })?;
366            let (disc, addr_hex) = match to {
367                Identity::Address(a) => (0u64, a.as_str()),
368                Identity::ContractId(c) => (1u64, c.as_str()),
369            };
370            let addr_bytes = parse_hex_32(addr_hex)?;
371            let call = register_referer_to_call(registry_id, disc, &addr_bytes);
372            let json = serde_json::json!({
373                "RegisterReferer": { "to": serde_json::to_value(to).unwrap_or_default() }
374            });
375            Ok((call, json))
376        }
377    }
378}
379
380/// Convert a RegisterReferer action to a low-level CallArg.
381pub fn register_referer_to_call(
382    accounts_registry_id: &[u8; 32],
383    referer_discriminant: u64,
384    referer_address: &[u8; 32],
385) -> CallArg {
386    CallArg {
387        contract_id: *accounts_registry_id,
388        function_selector: function_selector("register_referer"),
389        amount: 0,
390        asset_id: [0u8; 32],
391        gas: GAS_MAX,
392        call_data: Some(encode_identity(referer_discriminant, referer_address)),
393    }
394}