1use crate::order_id::compute_order_item_id_at_index;
4use crate::sdk_compat::serialize_for_sdk_signing;
5use crate::types::*;
6use crate::{Error, Result};
7use rayon::prelude::*;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11const PARALLEL_THRESHOLD: usize = 10;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PreparedMessage {
17 #[serde(with = "serde_bytes")]
19 pub message_bytes: Vec<u8>,
20 pub order_id: Option<String>,
22 pub order_ids: Option<Vec<String>>,
24 pub actions: Vec<serde_json::Value>,
26 pub account: String,
28 pub signer: String,
30 #[serde(with = "crate::nonce::serde_decimal")]
32 pub nonce: u64,
33}
34
35impl PreparedMessage {
36 #[inline]
37 pub fn message_base58(&self) -> String {
38 bs58::encode(&self.message_bytes).into_string()
39 }
40
41 #[inline]
42 pub fn message_base64(&self) -> String {
43 use base64::{engine::general_purpose::STANDARD, Engine};
44 STANDARD.encode(&self.message_bytes)
45 }
46
47 #[inline]
48 pub fn message_hex(&self) -> String {
49 hex::encode(&self.message_bytes)
50 }
51}
52
53pub fn prepare_message(
55 item: OrderItem,
56 signature_domain: SignatureDomain,
57 account: &Pubkey,
58 signer: Option<&Pubkey>,
59 nonce: Option<u64>,
60) -> Result<PreparedMessage> {
61 let action = Action::Order { orders: vec![item] };
62 prepare_action(&action, signature_domain, account, signer, nonce)
63}
64
65pub fn prepare_group(
67 items: Vec<OrderItem>,
68 signature_domain: SignatureDomain,
69 account: &Pubkey,
70 signer: Option<&Pubkey>,
71 nonce: Option<u64>,
72) -> Result<PreparedMessage> {
73 if items.is_empty() {
74 return Err(Error::EmptyOrders);
75 }
76 let action = Action::Order { orders: items };
77 prepare_action(&action, signature_domain, account, signer, nonce)
78}
79
80pub fn prepare_faucet(
82 signature_domain: SignatureDomain,
83 account: &Pubkey,
84 signer: Option<&Pubkey>,
85 nonce: Option<u64>,
86) -> Result<PreparedMessage> {
87 let action = Action::Faucet(Faucet::new(*account));
88 prepare_action(&action, signature_domain, account, signer, nonce)
89}
90
91pub fn prepare_agent_wallet(
93 agent: &Pubkey,
94 delete: bool,
95 signature_domain: SignatureDomain,
96 account: &Pubkey,
97 signer: Option<&Pubkey>,
98 nonce: Option<u64>,
99) -> Result<PreparedMessage> {
100 let action = Action::AgentWalletCreation(AgentWallet {
101 agent: *agent,
102 delete,
103 });
104 prepare_action(&action, signature_domain, account, signer, nonce)
105}
106
107pub fn prepare_approve_commission_fee(
111 to: &Pubkey,
112 fee: u8,
113 signature_domain: SignatureDomain,
114 account: &Pubkey,
115 signer: Option<&Pubkey>,
116 nonce: Option<u64>,
117) -> Result<PreparedMessage> {
118 if fee == 0 || fee > MAX_COMMISSION_FEE_BPS {
119 return Err(Error::InvalidOrder(
120 "builder-code fee must be 1..=15 bps".to_string(),
121 ));
122 }
123 let action = Action::ApproveCommissionFee(ApproveCommissionFee {
124 to: *to,
125 max_fee: fee,
126 });
127 prepare_action(&action, signature_domain, account, signer, nonce)
128}
129
130#[inline]
132pub fn prepare_approve_builder_code(
133 to: &Pubkey,
134 fee: u8,
135 signature_domain: SignatureDomain,
136 account: &Pubkey,
137 signer: Option<&Pubkey>,
138 nonce: Option<u64>,
139) -> Result<PreparedMessage> {
140 prepare_approve_commission_fee(to, fee, signature_domain, account, signer, nonce)
141}
142
143pub fn prepare_revoke_commission_fee(
145 to: &Pubkey,
146 signature_domain: SignatureDomain,
147 account: &Pubkey,
148 signer: Option<&Pubkey>,
149 nonce: Option<u64>,
150) -> Result<PreparedMessage> {
151 let action = Action::RevokeCommissionFee(RevokeCommissionFee { to: *to });
152 prepare_action(&action, signature_domain, account, signer, nonce)
153}
154
155#[inline]
157pub fn prepare_revoke_builder_code(
158 to: &Pubkey,
159 signature_domain: SignatureDomain,
160 account: &Pubkey,
161 signer: Option<&Pubkey>,
162 nonce: Option<u64>,
163) -> Result<PreparedMessage> {
164 prepare_revoke_commission_fee(to, signature_domain, account, signer, nonce)
165}
166
167pub fn prepare_update_liquidator_config(
169 config: LiquidatorConfig,
170 signature_domain: SignatureDomain,
171 account: &Pubkey,
172 signer: Option<&Pubkey>,
173 nonce: Option<u64>,
174) -> Result<PreparedMessage> {
175 let action = Action::UpdateLiquidatorConfig(config);
176 prepare_action(&action, signature_domain, account, signer, nonce)
177}
178
179pub fn prepare_user_settings(
181 settings: UserSettings,
182 signature_domain: SignatureDomain,
183 account: &Pubkey,
184 signer: Option<&Pubkey>,
185 nonce: Option<u64>,
186) -> Result<PreparedMessage> {
187 let action = Action::UpdateUserSettings(settings);
188 prepare_action(&action, signature_domain, account, signer, nonce)
189}
190
191pub fn prepare_create_sub_account(
193 sub_account: CreateSubAccount,
194 signature_domain: SignatureDomain,
195 account: &Pubkey,
196 signer: Option<&Pubkey>,
197 nonce: Option<u64>,
198) -> Result<PreparedMessage> {
199 let action = Action::CreateSubAccount(sub_account);
200 prepare_action(&action, signature_domain, account, signer, nonce)
201}
202
203pub fn prepare_remove_sub_account(
205 to_remove: Pubkey,
206 signature_domain: SignatureDomain,
207 account: &Pubkey,
208 signer: Option<&Pubkey>,
209 nonce: Option<u64>,
210) -> Result<PreparedMessage> {
211 let action = Action::RemoveSubAccount(RemoveSubAccount { to_remove });
212 prepare_action(&action, signature_domain, account, signer, nonce)
213}
214
215pub fn prepare_rename_sub_account(
217 rename: RenameSubAccount,
218 signature_domain: SignatureDomain,
219 account: &Pubkey,
220 signer: Option<&Pubkey>,
221 nonce: Option<u64>,
222) -> Result<PreparedMessage> {
223 let action = Action::RenameSubAccount(rename);
224 prepare_action(&action, signature_domain, account, signer, nonce)
225}
226
227pub fn prepare_transfer(
229 transfer: Transfer,
230 signature_domain: SignatureDomain,
231 account: &Pubkey,
232 signer: Option<&Pubkey>,
233 nonce: Option<u64>,
234) -> Result<PreparedMessage> {
235 let action = Action::Transfer(transfer);
236 prepare_action(&action, signature_domain, account, signer, nonce)
237}
238
239pub fn prepare_withdraw(
241 withdraw: Withdraw,
242 signature_domain: SignatureDomain,
243 account: &Pubkey,
244 signer: Option<&Pubkey>,
245 nonce: Option<u64>,
246) -> Result<PreparedMessage> {
247 let action = Action::Withdraw(withdraw);
248 prepare_action(&action, signature_domain, account, signer, nonce)
249}
250
251pub fn prepare_withdraw_lock_recover(
253 recover: WithdrawLockRecover,
254 signature_domain: SignatureDomain,
255 account: &Pubkey,
256 signer: Option<&Pubkey>,
257 nonce: Option<u64>,
258) -> Result<PreparedMessage> {
259 let action = Action::WithdrawLockRecover(recover);
260 prepare_action(&action, signature_domain, account, signer, nonce)
261}
262
263pub fn prepare_create_multisig(
265 create_multisig: CreateMultisig,
266 signature_domain: SignatureDomain,
267 account: &Pubkey,
268 signer: Option<&Pubkey>,
269 nonce: Option<u64>,
270) -> Result<PreparedMessage> {
271 let action = Action::CreateMultisig(create_multisig);
272 prepare_action(&action, signature_domain, account, signer, nonce)
273}
274
275pub fn prepare_multisig_propose(
277 propose: MultisigPropose,
278 signature_domain: SignatureDomain,
279 account: &Pubkey,
280 signer: Option<&Pubkey>,
281 nonce: Option<u64>,
282) -> Result<PreparedMessage> {
283 let action = Action::MultisigPropose(propose);
284 prepare_action(&action, signature_domain, account, signer, nonce)
285}
286
287pub fn prepare_multisig_approve(
289 approve: MultisigApprove,
290 signature_domain: SignatureDomain,
291 account: &Pubkey,
292 signer: Option<&Pubkey>,
293 nonce: Option<u64>,
294) -> Result<PreparedMessage> {
295 let action = Action::MultisigApprove(approve);
296 prepare_action(&action, signature_domain, account, signer, nonce)
297}
298
299pub fn prepare_multisig_reject(
301 reject: MultisigReject,
302 signature_domain: SignatureDomain,
303 account: &Pubkey,
304 signer: Option<&Pubkey>,
305 nonce: Option<u64>,
306) -> Result<PreparedMessage> {
307 let action = Action::MultisigReject(reject);
308 prepare_action(&action, signature_domain, account, signer, nonce)
309}
310
311pub fn prepare_multisig_cancel(
313 cancel: MultisigCancel,
314 signature_domain: SignatureDomain,
315 account: &Pubkey,
316 signer: Option<&Pubkey>,
317 nonce: Option<u64>,
318) -> Result<PreparedMessage> {
319 let action = Action::MultisigCancel(cancel);
320 prepare_action(&action, signature_domain, account, signer, nonce)
321}
322
323pub fn prepare_multisig_execute(
325 execute: MultisigExecute,
326 signature_domain: SignatureDomain,
327 account: &Pubkey,
328 signer: Option<&Pubkey>,
329 nonce: Option<u64>,
330) -> Result<PreparedMessage> {
331 let action = Action::MultisigExecute(execute);
332 prepare_action(&action, signature_domain, account, signer, nonce)
333}
334
335pub fn prepare_update_multisig_policy(
337 update: UpdateMultisigPolicy,
338 signature_domain: SignatureDomain,
339 account: &Pubkey,
340 signer: Option<&Pubkey>,
341 nonce: Option<u64>,
342) -> Result<PreparedMessage> {
343 let action = Action::UpdateMultisigPolicy(update);
344 prepare_action(&action, signature_domain, account, signer, nonce)
345}
346
347pub fn prepare_action(
349 action: &Action,
350 signature_domain: SignatureDomain,
351 account: &Pubkey,
352 signer: Option<&Pubkey>,
353 nonce: Option<u64>,
354) -> Result<PreparedMessage> {
355 let signer_pubkey = signer.unwrap_or(account);
356 let nonce = nonce.unwrap_or_else(crate::nonce::current_timestamp_nanos);
357
358 let mut message_bytes = Vec::with_capacity(512);
359 serialize_for_sdk_signing(action, signature_domain, nonce, account, &mut message_bytes)?;
360
361 let actions = action_to_json(action)?;
362 let order_id = compute_action_order_id(action, nonce, account);
363 let order_ids = compute_action_order_ids(action, nonce, account);
364
365 Ok(PreparedMessage {
366 message_bytes,
367 order_id,
368 order_ids,
369 actions,
370 account: account.to_base58(),
371 signer: signer_pubkey.to_base58(),
372 nonce,
373 })
374}
375
376fn compute_action_order_id(action: &Action, nonce: u64, account: &Pubkey) -> Option<String> {
377 match action {
378 Action::Order { orders } if orders.len() == 1 => {
379 let mut scratch = Vec::with_capacity(96);
380 compute_order_item_id_at_index(&orders[0], 0, nonce, account, &mut scratch)
381 .map(|id| id.to_base58())
382 }
383 _ => None,
384 }
385}
386
387fn compute_action_order_ids(action: &Action, nonce: u64, account: &Pubkey) -> Option<Vec<String>> {
388 match action {
389 Action::Order { orders } if orders.len() > 1 => {
390 let mut scratch = Vec::with_capacity(96);
391 let mut ids = Vec::with_capacity(orders.len());
392 for (idx, item) in orders.iter().enumerate() {
393 if let Some(id) =
394 compute_order_item_id_at_index(item, idx as u32, nonce, account, &mut scratch)
395 {
396 ids.push(id.to_base58());
397 }
398 }
399 if ids.is_empty() {
400 None
401 } else {
402 Some(ids)
403 }
404 }
405 _ => None,
406 }
407}
408
409pub fn prepare_all(
411 items: Vec<OrderItem>,
412 signature_domain: SignatureDomain,
413 account: &Pubkey,
414 signer: Option<&Pubkey>,
415 base_nonce: Option<u64>,
416) -> Result<Vec<PreparedMessage>> {
417 if items.is_empty() {
418 return Ok(vec![]);
419 }
420
421 let base = base_nonce.unwrap_or_else(crate::nonce::current_timestamp_nanos);
422 let signer_pubkey = signer.unwrap_or(account);
423
424 if items.len() < PARALLEL_THRESHOLD {
425 items
426 .into_iter()
427 .enumerate()
428 .map(|(i, item)| {
429 prepare_single_item(
430 item,
431 signature_domain,
432 account,
433 signer_pubkey,
434 base + i as u64,
435 )
436 })
437 .collect()
438 } else {
439 items
440 .into_par_iter()
441 .enumerate()
442 .map(|(i, item)| {
443 prepare_single_item(
444 item,
445 signature_domain,
446 account,
447 signer_pubkey,
448 base + i as u64,
449 )
450 })
451 .collect()
452 }
453}
454
455fn prepare_single_item(
456 item: OrderItem,
457 signature_domain: SignatureDomain,
458 account: &Pubkey,
459 signer: &Pubkey,
460 nonce: u64,
461) -> Result<PreparedMessage> {
462 let mut scratch = Vec::with_capacity(96);
463 let order_id = compute_order_item_id_at_index(&item, 0, nonce, account, &mut scratch)
464 .map(|id| id.to_base58());
465 let action = Action::Order { orders: vec![item] };
466
467 let mut message_bytes = Vec::with_capacity(512);
468 serialize_for_sdk_signing(
469 &action,
470 signature_domain,
471 nonce,
472 account,
473 &mut message_bytes,
474 )?;
475 let actions = action_to_json(&action)?;
476
477 Ok(PreparedMessage {
478 message_bytes,
479 order_id,
480 order_ids: None,
481 actions,
482 account: account.to_base58(),
483 signer: signer.to_base58(),
484 nonce,
485 })
486}
487
488pub fn finalize_transaction(prepared: PreparedMessage, signature: &str) -> SignedTransaction {
490 SignedTransaction {
491 actions: prepared.actions,
492 nonce: prepared.nonce,
493 account: prepared.account,
494 signer: prepared.signer,
495 signature: signature.to_string(),
496 order_id: prepared.order_id,
497 order_ids: prepared.order_ids,
498 }
499}
500
501pub fn finalize_transaction_bytes(
503 prepared: PreparedMessage,
504 signature: &[u8],
505) -> SignedTransaction {
506 let signature_b58 = bs58::encode(signature).into_string();
507 finalize_transaction(prepared, &signature_b58)
508}
509
510pub fn finalize_all(
512 prepared: Vec<PreparedMessage>,
513 signatures: Vec<&str>,
514) -> Result<Vec<SignedTransaction>> {
515 if prepared.len() != signatures.len() {
516 return Err(Error::SignatureMismatch {
517 expected: prepared.len(),
518 got: signatures.len(),
519 });
520 }
521
522 Ok(prepared
523 .into_iter()
524 .zip(signatures)
525 .map(|(p, sig)| finalize_transaction(p, sig))
526 .collect())
527}
528
529fn action_to_json(action: &Action) -> Result<Vec<serde_json::Value>> {
530 match action {
531 Action::Order { orders } => orders.iter().map(order_item_to_json).collect(),
532 Action::Faucet(faucet) => {
533 let mut faucet_obj = json!({ "u": faucet.user.to_base58() });
534 if let Some(amount) = faucet.amount {
535 faucet_obj["amount"] = json!(amount);
536 }
537 Ok(vec![json!({ "faucet": faucet_obj })])
538 }
539 Action::AgentWalletCreation(agent) => Ok(vec![json!({
540 "agentWalletCreation": {
541 "a": agent.agent.to_base58(),
542 "d": agent.delete
543 }
544 })]),
545 Action::UpdateUserSettings(settings) => {
546 let mut ordered = settings.max_leverage.clone();
547 ordered.sort_unstable_by(|left, right| left.0.cmp(&right.0));
548 let mut m = serde_json::Map::with_capacity(ordered.len());
549 for (symbol, lev) in ordered {
550 m.insert(symbol, json!(lev));
551 }
552 Ok(vec![json!({ "updateUserSettings": { "m": m } })])
553 }
554 Action::Oracle { oracles } => Ok(oracles
555 .iter()
556 .map(|o| {
557 json!({
558 "px": {
559 "t": o.timestamp,
560 "c": o.asset,
561 "px": o.price
562 }
563 })
564 })
565 .collect()),
566 Action::PythOracle { oracles } => {
567 let entries: Vec<_> = oracles
568 .iter()
569 .map(|o| {
570 json!({
571 "t": o.timestamp,
572 "fi": o.feed_index,
573 "px": o.price,
574 "e": o.exponent
575 })
576 })
577 .collect();
578 Ok(vec![json!({ "o": { "oracles": entries } })])
579 }
580 Action::WhitelistFaucet(action) => Ok(vec![json!({
581 "whitelistFaucet": {
582 "target": action.target.to_base58(),
583 "whitelist": action.whitelist
584 }
585 })]),
586 Action::CreateSubAccount(action) => {
587 let mut obj = json!({ "name": action.name });
588 if let Some(amount) = action.margin_amount {
589 obj["marginAmount"] = json!(amount);
590 }
591 Ok(vec![json!({ "createSubAccount": obj })])
592 }
593 Action::RemoveSubAccount(action) => Ok(vec![json!({
594 "removeSubAccount": {
595 "toRemove": action.to_remove.to_base58()
596 }
597 })]),
598 Action::RenameSubAccount(action) => Ok(vec![json!({
599 "renameSubAccount": {
600 "account": action.account.to_base58(),
601 "name": action.name
602 }
603 })]),
604 Action::Transfer(transfer) => Ok(vec![json!({
605 "transfer": {
606 "k": match transfer.kind {
607 TransferKind::Internal => "internal",
608 TransferKind::External => "external",
609 },
610 "from": transfer.from.to_base58(),
611 "to": transfer.to.to_base58(),
612 "marginAmount": transfer.margin_amount,
613 }
614 })]),
615 Action::Withdraw(withdraw) => Ok(vec![json!({
616 "withdraw": {
617 "u": withdraw.user.to_base58(),
618 "v": withdraw.vault.to_base58(),
619 "rta": withdraw.recipient_token_account.to_base58(),
620 "a": withdraw.amount,
621 "b": withdraw.blockhash.to_base58(),
622 }
623 })]),
624 Action::WithdrawLockRecover(recover) => Ok(vec![json!({
625 "withdrawLockRecover": {
626 "u": recover.user.to_base58(),
627 "h": recover.hash.to_base58(),
628 }
629 })]),
630 Action::CreateMultisig(action) => Ok(vec![json!({
631 "createMultisig": {
632 "signers": action.signers.iter().map(Pubkey::to_base58).collect::<Vec<_>>(),
633 "threshold": action.threshold,
634 "timeLockSecs": action.time_lock_secs,
635 "proposalLifetimeSecs": action.proposal_lifetime_secs,
636 }
637 })]),
638 Action::MultisigPropose(action) => {
639 let mut inner_actions = Vec::new();
640 for inner in &action.actions {
641 inner_actions.extend(action_to_json(inner)?);
642 }
643 Ok(vec![json!({
644 "msp": {
645 "m": action.multisig.to_base58(),
646 "a": inner_actions,
647 }
648 })])
649 }
650 Action::MultisigApprove(action) => Ok(vec![json!({
651 "msa": {
652 "m": action.multisig.to_base58(),
653 "p": action.proposal_id,
654 }
655 })]),
656 Action::MultisigReject(action) => Ok(vec![json!({
657 "msr": {
658 "m": action.multisig.to_base58(),
659 "p": action.proposal_id,
660 }
661 })]),
662 Action::MultisigCancel(action) => Ok(vec![json!({
663 "msc": {
664 "m": action.multisig.to_base58(),
665 "p": action.proposal_id,
666 }
667 })]),
668 Action::MultisigExecute(action) => Ok(vec![json!({
669 "mse": {
670 "m": action.multisig.to_base58(),
671 "p": action.proposal_id,
672 }
673 })]),
674 Action::UpdateMultisigPolicy(action) => Ok(vec![json!({
675 "msu": {
676 "m": action.multisig.to_base58(),
677 "signers": action.signers.iter().map(Pubkey::to_base58).collect::<Vec<_>>(),
678 "threshold": action.threshold,
679 "timeLockSecs": action.time_lock_secs,
680 "proposalLifetimeSecs": action.proposal_lifetime_secs,
681 }
682 })]),
683 Action::ApproveCommissionFee(action) => Ok(vec![json!({
684 "abc": {
685 "to": action.to.to_base58(),
686 "fee": action.max_fee
687 }
688 })]),
689 Action::RevokeCommissionFee(action) => Ok(vec![json!({
690 "rbc": {
691 "to": action.to.to_base58()
692 }
693 })]),
694 Action::UpdateLiquidatorConfig(config) => {
695 Ok(vec![json!({ "liq": liquidator_config_to_json(config) })])
696 }
697 }
698}
699
700fn order_item_to_json(item: &OrderItem) -> Result<serde_json::Value> {
701 match item {
702 OrderItem::Order(order) => match &order.order_type {
703 OrderType::Limit { tif } => {
704 let tif_str = match tif {
705 TimeInForce::Gtc => "GTC",
706 TimeInForce::Ioc => "IOC",
707 TimeInForce::Alo => "ALO",
708 };
709 let mut body = json!({
710 "c": order.symbol,
711 "b": order.is_buy,
712 "px": order.price,
713 "sz": order.size,
714 "tif": tif_str,
715 "r": order.reduce_only,
716 "i": order.iso
717 });
718 if let Some(commission) = order.commission {
719 body["builderCode"] = json!({
720 "to": commission.to.to_base58(),
721 "fee": commission.fee
722 });
723 }
724 Ok(json!({ "l": body }))
725 }
726 OrderType::Trigger {
727 is_market,
728 trigger_px: _,
729 } => {
730 if !is_market {
731 return Err(Error::InvalidOrder(
732 "trigger orders are not supported by BULK API; use market".to_string(),
733 ));
734 }
735 let mut body = json!({
736 "c": order.symbol,
737 "b": order.is_buy,
738 "sz": order.size,
739 "r": order.reduce_only,
740 "i": order.iso,
741 });
742 if let Some(commission) = order.commission {
743 body["builderCode"] = json!({
744 "to": commission.to.to_base58(),
745 "fee": commission.fee
746 });
747 }
748 Ok(json!({ "m": body }))
749 }
750 },
751 OrderItem::Modify(modify) => Ok(json!({
752 "mod": {
753 "oid": modify.order_id.to_base58(),
754 "c": modify.symbol,
755 "sz": modify.amount
756 }
757 })),
758 OrderItem::Cancel(cancel) => Ok(json!({
759 "cx": {
760 "c": cancel.symbol,
761 "oid": cancel.order_id.to_base58()
762 }
763 })),
764 OrderItem::CancelAll(cancel_all) => Ok(json!({
765 "cxa": {
766 "c": cancel_all.symbols
767 }
768 })),
769 OrderItem::Stop(stop) => Ok(json!({
770 "st": {
771 "c": stop.symbol,
772 "d": stop.is_buy,
773 "sz": stop.size,
774 "tr": stop.trigger_price,
775 "lim": stop.limit_price,
776 "i": stop.iso
777 }
778 })),
779 OrderItem::TakeProfit(tp) => Ok(json!({
780 "tp": {
781 "c": tp.symbol,
782 "d": tp.is_buy,
783 "sz": tp.size,
784 "tr": tp.trigger_price,
785 "lim": tp.limit_price,
786 "i": tp.iso
787 }
788 })),
789 OrderItem::RangeOco(rng) => Ok(json!({
790 "rng": {
791 "c": rng.symbol,
792 "d": rng.is_buy,
793 "sz": rng.size,
794 "pmin": rng.collar_min,
795 "pmax": rng.collar_max,
796 "lmin": rng.limit_min,
797 "lmax": rng.limit_max,
798 "i": rng.iso
799 }
800 })),
801 OrderItem::TriggerBasket(trig) => {
802 let nested: Result<Vec<_>> = trig.actions.iter().map(order_item_to_json).collect();
803 Ok(json!({
804 "trig": {
805 "c": trig.symbol,
806 "d": trig.is_buy,
807 "tr": trig.trigger_price,
808 "actions": nested?
809 }
810 }))
811 }
812 OrderItem::OnFill(of) => {
813 let trigger = order_item_to_json(&of.trigger)?;
814 let actions: Result<Vec<_>> = of.actions.iter().map(order_item_to_json).collect();
815 Ok(json!({
816 "of": {
817 "trigger": trigger,
818 "actions": actions?
819 }
820 }))
821 }
822 OrderItem::TrailingStop(trl) => Ok(json!({
823 "trl": {
824 "c": trl.symbol,
825 "b": trl.is_buy,
826 "sz": trl.size,
827 "trb": trl.trail_bps,
828 "stb": trl.step_bps,
829 "lim": trl.limit_price,
830 "i": trl.iso
831 }
832 })),
833 }
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839 use crate::Keypair;
840
841 #[test]
842 fn test_prepare_message() {
843 let keypair = Keypair::generate();
844 let account = keypair.pubkey();
845 let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
846 let prepared = prepare_message(
847 order.into(),
848 SignatureDomain::Devnet,
849 &account,
850 None,
851 Some(1234567890),
852 )
853 .unwrap();
854
855 assert!(!prepared.message_bytes.is_empty());
856 assert_eq!(prepared.nonce, 1234567890);
857 assert_eq!(prepared.actions.len(), 1);
858 assert!(prepared.actions[0].get("l").is_some());
859 }
860
861 #[test]
862 fn test_prepare_modify_uses_compact_sdk_keys() {
863 let keypair = Keypair::generate();
864 let account = keypair.pubkey();
865 let modify = Modify::new(Hash::random(), "BTC-USD", 0.25);
866 let prepared = prepare_message(
867 OrderItem::Modify(modify),
868 SignatureDomain::Devnet,
869 &account,
870 None,
871 Some(1234567890),
872 )
873 .unwrap();
874
875 let mod_obj = prepared.actions[0].get("mod").unwrap();
876 assert!(mod_obj.get("c").is_some());
877 assert!(mod_obj.get("sz").is_some());
878 assert!(mod_obj.get("symbol").is_none());
879 assert!(mod_obj.get("amount").is_none());
880 }
881
882 #[test]
883 fn test_prepare_liquidator_config_includes_current_schema_fields() {
884 let keypair = Keypair::generate();
885 let config = LiquidatorConfig::new(15e6, 0.5, 0.0, 0.25, 2.0).with_instrument(
886 LiquidatorInstrumentConfig {
887 symbol: "BTC-USD".to_string(),
888 max_exposure: 10e6,
889 reserve: 49.0,
890 rfactor: 0.25,
891 volume_percent: 25.0,
892 volume_min: 0.5,
893 volume_rampup: 0,
894 max_sweep_bps: 100.0,
895 max_adl_notional: 0.0,
896 max_adl_percent: 0.0,
897 },
898 );
899 let prepared = prepare_update_liquidator_config(
900 config,
901 SignatureDomain::Devnet,
902 &keypair.pubkey(),
903 None,
904 Some(42),
905 )
906 .unwrap();
907
908 let liq = prepared.actions[0]["liq"].as_object().unwrap();
909 assert_eq!(liq["urgency_size_fraction"], 0.25);
910 assert_eq!(liq["sweep_sds"], 2.0);
911 let instrument = liq["instruments"][0].as_object().unwrap();
912 for field in [
913 "max_exposure",
914 "reserve",
915 "rfactor",
916 "volume_percent",
917 "volume_min",
918 "volume_rampup",
919 "max_sweep_bps",
920 "max_adl_notional",
921 "max_adl_percent",
922 ] {
923 assert!(instrument.contains_key(field), "missing {field}");
924 }
925 assert!(!instrument.contains_key("premium_min"));
926 assert!(!instrument.contains_key("fee"));
927 assert!(!liq.contains_key("price_to_sweep"));
928 }
929
930 #[test]
931 fn test_prepare_group() {
932 let keypair = Keypair::generate();
933 let account = keypair.pubkey();
934 let items: Vec<OrderItem> = vec![
935 Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc).into(),
936 Order::limit("BTC-USD", false, 99000.0, 0.1, TimeInForce::Gtc).into(),
937 ];
938 let prepared = prepare_group(
939 items,
940 SignatureDomain::Devnet,
941 &account,
942 None,
943 Some(1234567890),
944 )
945 .unwrap();
946 assert_eq!(prepared.actions.len(), 2);
947 assert!(prepared.order_id.is_none());
948 assert_eq!(prepared.order_ids.as_ref().map(Vec::len), Some(2));
949 }
950
951 #[test]
952 fn test_prepare_on_fill_emits_one_inline_trigger_action() {
953 let account = Pubkey::from_bytes([7u8; 32]);
954 let item = OrderItem::OnFill(OnFill {
955 trigger: Box::new(
956 Order::limit("BTC-USD", true, 100_000.0, 0.1, TimeInForce::Gtc).into(),
957 ),
958 actions: vec![Order::market("ETH-USD", false, 1.25).into()],
959 });
960
961 let prepared = prepare_message(
962 item,
963 SignatureDomain::Devnet,
964 &account,
965 None,
966 Some(1_234_567_890),
967 )
968 .unwrap();
969
970 assert_eq!(prepared.actions.len(), 1);
971 assert!(prepared.actions[0]["of"].get("p").is_none());
972 assert!(prepared.actions[0]["of"]["trigger"].get("l").is_some());
973 assert!(prepared.actions[0]["of"]["actions"][0].get("m").is_some());
974 }
975
976 #[test]
977 fn trigger_basket_json_has_no_top_level_iso() {
978 let account = Pubkey::from_bytes([7u8; 32]);
979 let prepared = prepare_message(
980 OrderItem::TriggerBasket(TriggerBasket {
981 symbol: "BTC-USD".to_string(),
982 is_buy: true,
983 trigger_price: 100_000.0,
984 actions: vec![Order::market("BTC-USD", false, 0.1).into()],
985 }),
986 SignatureDomain::Devnet,
987 &account,
988 None,
989 Some(1_234_567_890),
990 )
991 .unwrap();
992
993 assert!(prepared.actions[0]["trig"].get("i").is_none());
994 }
995
996 #[test]
997 fn test_prepare_commission_order_omits_absent_and_includes_present() {
998 let keypair = Keypair::generate();
999 let account = keypair.pubkey();
1000 let recipient = Pubkey::from_bytes([9u8; 32]);
1001 let plain = prepare_message(
1002 Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc).into(),
1003 SignatureDomain::Devnet,
1004 &account,
1005 None,
1006 Some(1234567890),
1007 )
1008 .unwrap();
1009 let commissioned = prepare_message(
1010 Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc)
1011 .with_builder_code(recipient, 5)
1012 .unwrap()
1013 .into(),
1014 SignatureDomain::Devnet,
1015 &account,
1016 None,
1017 Some(1234567890),
1018 )
1019 .unwrap();
1020
1021 assert!(plain.actions[0]["l"].get("builderCode").is_none());
1022 assert_eq!(commissioned.actions[0]["l"]["builderCode"]["fee"], 5);
1023 assert_eq!(
1024 commissioned.actions[0]["l"]["builderCode"]["to"],
1025 recipient.to_base58()
1026 );
1027 assert_eq!(plain.order_id, commissioned.order_id);
1028 assert_ne!(plain.message_bytes, commissioned.message_bytes);
1029 }
1030
1031 #[test]
1032 fn test_prepare_commission_approval_actions() {
1033 let keypair = Keypair::generate();
1034 let account = keypair.pubkey();
1035 let recipient = Pubkey::from_bytes([7u8; 32]);
1036 let approve = prepare_approve_builder_code(
1037 &recipient,
1038 5,
1039 SignatureDomain::Devnet,
1040 &account,
1041 None,
1042 Some(1234567890),
1043 )
1044 .unwrap();
1045 let revoke = prepare_revoke_builder_code(
1046 &recipient,
1047 SignatureDomain::Devnet,
1048 &account,
1049 None,
1050 Some(1234567891),
1051 )
1052 .unwrap();
1053
1054 assert_eq!(approve.actions[0]["abc"]["fee"], 5);
1055 assert_eq!(approve.actions[0]["abc"]["to"], recipient.to_base58());
1056 assert_eq!(revoke.actions[0]["rbc"]["to"], recipient.to_base58());
1057 assert!(approve.order_id.is_none());
1058 assert!(revoke.order_id.is_none());
1059 assert!(prepare_approve_builder_code(
1060 &recipient,
1061 0,
1062 SignatureDomain::Devnet,
1063 &account,
1064 None,
1065 None
1066 )
1067 .is_err());
1068 assert!(prepare_approve_builder_code(
1069 &recipient,
1070 16,
1071 SignatureDomain::Devnet,
1072 &account,
1073 None,
1074 None
1075 )
1076 .is_err());
1077 }
1078
1079 #[test]
1080 fn test_prepare_all_parallel() {
1081 let keypair = Keypair::generate();
1082 let account = keypair.pubkey();
1083 let orders: Vec<OrderItem> = (0..20)
1084 .map(|i| {
1085 Order::limit(
1086 "BTC-USD",
1087 i % 2 == 0,
1088 100000.0 + i as f64,
1089 0.1,
1090 TimeInForce::Gtc,
1091 )
1092 .into()
1093 })
1094 .collect();
1095
1096 let prepared = prepare_all(
1097 orders,
1098 SignatureDomain::Devnet,
1099 &account,
1100 None,
1101 Some(1000000),
1102 )
1103 .unwrap();
1104 assert_eq!(prepared.len(), 20);
1105 for (i, p) in prepared.iter().enumerate() {
1106 assert_eq!(p.nonce, 1000000 + i as u64);
1107 }
1108 }
1109
1110 #[test]
1111 fn test_finalize_transaction() {
1112 let keypair = Keypair::generate();
1113 let account = keypair.pubkey();
1114 let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
1115 let prepared = prepare_message(
1116 order.into(),
1117 SignatureDomain::Devnet,
1118 &account,
1119 None,
1120 Some(1234567890),
1121 )
1122 .unwrap();
1123 let signed = finalize_transaction(prepared.clone(), "sig");
1124
1125 assert_eq!(signed.nonce, prepared.nonce);
1126 assert_eq!(signed.actions, prepared.actions);
1127 assert_eq!(signed.signature, "sig");
1128 assert_eq!(signed.order_ids, prepared.order_ids);
1129 }
1130
1131 #[test]
1132 fn test_large_nonce_prepare_finalize_and_json_roundtrip() {
1133 const NONCE: u64 = 9_007_199_254_740_993;
1134 const NONCE_DECIMAL: &str = "9007199254740993";
1135
1136 let keypair = Keypair::generate();
1137 let account = keypair.pubkey();
1138 let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
1139 let prepared = prepare_message(
1140 order.into(),
1141 SignatureDomain::Devnet,
1142 &account,
1143 None,
1144 Some(NONCE),
1145 )
1146 .unwrap();
1147
1148 let nonce_offset = prepared.message_bytes.len() - 41;
1149 assert_eq!(
1150 &prepared.message_bytes[nonce_offset..nonce_offset + 8],
1151 &NONCE.to_le_bytes()
1152 );
1153
1154 let prepared_json = serde_json::to_value(&prepared).unwrap();
1155 assert_eq!(prepared_json["nonce"], NONCE_DECIMAL);
1156 let restored: PreparedMessage = serde_json::from_value(prepared_json).unwrap();
1157 assert_eq!(restored.nonce, NONCE);
1158
1159 let signed = finalize_transaction(restored, "sig");
1160 let signed_json = signed.to_json().unwrap();
1161 let signed_value: serde_json::Value = serde_json::from_str(&signed_json).unwrap();
1162 assert_eq!(signed_value["nonce"], NONCE_DECIMAL);
1163 assert_eq!(signed.nonce, NONCE);
1164 }
1165
1166 #[test]
1167 fn test_prepare_rename_sub_account() {
1168 let keypair = Keypair::generate();
1169 let account = keypair.pubkey();
1170 let subaccount = Keypair::generate().pubkey();
1171 let prepared = prepare_rename_sub_account(
1172 RenameSubAccount::new(subaccount, "desk-2"),
1173 SignatureDomain::Devnet,
1174 &account,
1175 None,
1176 Some(1234567890),
1177 )
1178 .unwrap();
1179
1180 assert_eq!(prepared.actions.len(), 1);
1181 let obj = prepared.actions[0].get("renameSubAccount").unwrap();
1182 assert_eq!(
1183 obj.get("account").and_then(|v| v.as_str()),
1184 Some(subaccount.to_base58().as_str())
1185 );
1186 assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("desk-2"));
1187 }
1188
1189 #[test]
1190 fn test_prepare_create_sub_account_with_margin_uses_current_sdk_shape() {
1191 let keypair = Keypair::generate();
1192 let account = keypair.pubkey();
1193 let prepared = prepare_create_sub_account(
1194 CreateSubAccount::with_margin("desk-1", 1000.0),
1195 SignatureDomain::Devnet,
1196 &account,
1197 None,
1198 Some(1234567890),
1199 )
1200 .unwrap();
1201
1202 let obj = prepared.actions[0].get("createSubAccount").unwrap();
1203 assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("desk-1"));
1204 assert!(obj.get("marginSymbol").is_none());
1205 assert_eq!(
1206 obj.get("marginAmount").and_then(|v| v.as_f64()),
1207 Some(1000.0)
1208 );
1209 }
1210
1211 #[test]
1212 fn test_prepare_transfer_uses_current_sdk_shape() {
1213 let keypair = Keypair::generate();
1214 let account = keypair.pubkey();
1215 let to = Keypair::generate().pubkey();
1216 let prepared = prepare_transfer(
1217 Transfer::internal(account, to, 10.0),
1218 SignatureDomain::Devnet,
1219 &account,
1220 None,
1221 Some(1234567890),
1222 )
1223 .unwrap();
1224
1225 let obj = prepared.actions[0].get("transfer").unwrap();
1226 assert_eq!(obj.get("k").and_then(|v| v.as_str()), Some("internal"));
1227 assert!(obj.get("marginSymbol").is_none());
1228 assert_eq!(obj.get("marginAmount").and_then(|v| v.as_f64()), Some(10.0));
1229 }
1230
1231 #[test]
1232 fn test_prepare_withdraw_matches_client_wire_shape() {
1233 let account = Pubkey::from_bytes([1; 32]);
1234 let vault = Pubkey::from_bytes([2; 32]);
1235 let recipient_token_account = Pubkey::from_bytes([3; 32]);
1236 let blockhash = Hash::from_bytes([4; 32]);
1237 let amount = 42u64;
1238 let nonce = 1234567890u64;
1239 let withdraw = Withdraw {
1240 user: account,
1241 vault,
1242 recipient_token_account,
1243 amount,
1244 blockhash,
1245 };
1246 let prepared = prepare_withdraw(
1247 withdraw,
1248 SignatureDomain::Devnet,
1249 &account,
1250 None,
1251 Some(nonce),
1252 )
1253 .unwrap();
1254
1255 let mut expected = Vec::new();
1256 expected.extend_from_slice(&1u64.to_le_bytes());
1257 expected.extend_from_slice(&45u32.to_le_bytes());
1258 expected.extend_from_slice(account.as_bytes());
1259 expected.extend_from_slice(vault.as_bytes());
1260 expected.extend_from_slice(recipient_token_account.as_bytes());
1261 expected.extend_from_slice(&amount.to_le_bytes());
1262 expected.extend_from_slice(blockhash.as_bytes());
1263 expected.extend_from_slice(&nonce.to_le_bytes());
1264 expected.extend_from_slice(account.as_bytes());
1265 expected.push(SignatureDomain::Devnet as u8);
1266 assert_eq!(prepared.message_bytes, expected);
1267
1268 let obj = prepared.actions[0].get("withdraw").unwrap();
1269 assert_eq!(
1270 obj.get("u").and_then(|v| v.as_str()),
1271 Some(account.to_base58().as_str())
1272 );
1273 assert_eq!(
1274 obj.get("v").and_then(|v| v.as_str()),
1275 Some(vault.to_base58().as_str())
1276 );
1277 assert_eq!(obj.get("a").and_then(|v| v.as_u64()), Some(amount));
1278 assert!(obj.get("recipientTokenAccount").is_none());
1279 }
1280
1281 #[test]
1282 fn test_prepare_withdraw_lock_recover_matches_client_wire_shape() {
1283 let account = Pubkey::from_bytes([1; 32]);
1284 let user = Pubkey::from_bytes([2; 32]);
1285 let hash = Hash::from_bytes([3; 32]);
1286 let nonce = 1234567890u64;
1287 let prepared = prepare_withdraw_lock_recover(
1288 WithdrawLockRecover { user, hash },
1289 SignatureDomain::Devnet,
1290 &account,
1291 None,
1292 Some(nonce),
1293 )
1294 .unwrap();
1295
1296 let mut expected = Vec::new();
1297 expected.extend_from_slice(&1u64.to_le_bytes());
1298 expected.extend_from_slice(&54u32.to_le_bytes());
1299 expected.extend_from_slice(user.as_bytes());
1300 expected.extend_from_slice(hash.as_bytes());
1301 expected.extend_from_slice(&nonce.to_le_bytes());
1302 expected.extend_from_slice(account.as_bytes());
1303 expected.push(SignatureDomain::Devnet as u8);
1304 assert_eq!(prepared.message_bytes, expected);
1305
1306 let obj = prepared.actions[0].get("withdrawLockRecover").unwrap();
1307 assert_eq!(
1308 obj.get("u").and_then(|v| v.as_str()),
1309 Some(user.to_base58().as_str())
1310 );
1311 assert_eq!(
1312 obj.get("h").and_then(|v| v.as_str()),
1313 Some(hash.to_base58().as_str())
1314 );
1315 }
1316}