use crate::order_id::compute_order_item_id_at_index;
use crate::sdk_compat::serialize_for_sdk_signing;
use crate::types::*;
use crate::{Error, Keypair, NonceManager, Result};
use ed25519_dalek::Signer as DalekSigner;
use rayon::prelude::*;
use serde_json::json;
const PARALLEL_THRESHOLD: usize = 10;
pub struct Signer {
keypair: Keypair,
signature_domain: SignatureDomain,
nonce_manager: Option<NonceManager>,
serializer: Vec<u8>,
compute_order_id: bool,
compute_batch_order_ids: bool,
}
impl Signer {
pub fn new(keypair: Keypair, signature_domain: SignatureDomain) -> Self {
Self {
keypair,
signature_domain,
nonce_manager: None,
serializer: Vec::with_capacity(512),
compute_order_id: true,
compute_batch_order_ids: false,
}
}
pub fn with_nonce_manager(
keypair: Keypair,
signature_domain: SignatureDomain,
nonce_manager: NonceManager,
) -> Self {
Self {
keypair,
signature_domain,
nonce_manager: Some(nonce_manager),
serializer: Vec::with_capacity(512),
compute_order_id: true,
compute_batch_order_ids: false,
}
}
pub fn without_order_id(mut self) -> Self {
self.compute_order_id = false;
self
}
pub fn with_order_id(mut self) -> Self {
self.compute_order_id = true;
self
}
pub fn computes_order_id(&self) -> bool {
self.compute_order_id
}
pub fn set_order_id(&mut self, enabled: bool) {
self.compute_order_id = enabled;
}
pub fn with_batch_order_ids(mut self) -> Self {
self.compute_batch_order_ids = true;
self
}
pub fn without_batch_order_ids(mut self) -> Self {
self.compute_batch_order_ids = false;
self
}
pub fn computes_batch_order_ids(&self) -> bool {
self.compute_batch_order_ids
}
pub fn set_batch_order_ids(&mut self, enabled: bool) {
self.compute_batch_order_ids = enabled;
}
pub fn pubkey(&self) -> Pubkey {
self.keypair.pubkey()
}
pub const fn signature_domain(&self) -> SignatureDomain {
self.signature_domain
}
pub fn sign_bytes(&self, message: &[u8]) -> String {
let signature = self.keypair.signing_key().sign(message);
bs58::encode(signature.to_bytes()).into_string()
}
fn next_nonce(&self) -> u64 {
self.nonce_manager
.as_ref()
.map(|m| m.next())
.unwrap_or_else(crate::nonce::current_timestamp_nanos)
}
pub fn sign_action(
&mut self,
action: &Action,
nonce: u64,
account: &Pubkey,
) -> Result<SignedTransaction> {
let signer_pubkey = self.keypair.pubkey();
serialize_for_sdk_signing(
action,
self.signature_domain,
nonce,
account,
&mut self.serializer,
)?;
let order_id = if self.compute_order_id {
self.compute_action_order_id(action, nonce, account)
} else {
None
};
let order_ids = if self.compute_batch_order_ids {
self.compute_action_order_ids(action, nonce, account)
} else {
None
};
let signature = self.sign_bytes(&self.serializer);
let actions = self.action_to_json(action)?;
Ok(SignedTransaction {
actions,
nonce,
account: account.to_base58(),
signer: signer_pubkey.to_base58(),
signature,
order_id,
order_ids,
})
}
pub fn sign_action_self(&mut self, action: &Action, nonce: u64) -> Result<SignedTransaction> {
let account = self.keypair.pubkey();
self.sign_action(action, nonce, &account)
}
pub fn sign(&mut self, item: OrderItem, nonce: Option<u64>) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::Order { orders: vec![item] };
self.sign_action_self(&action, nonce)
}
pub fn sign_all(
&self,
items: Vec<OrderItem>,
base_nonce: Option<u64>,
) -> Result<Vec<SignedTransaction>> {
if items.is_empty() {
return Ok(vec![]);
}
let base = base_nonce.unwrap_or_else(crate::nonce::current_timestamp_nanos);
if items.len() < PARALLEL_THRESHOLD {
items
.into_iter()
.enumerate()
.map(|(i, item)| self.sign_single_item(item, base + i as u64))
.collect()
} else {
items
.into_par_iter()
.enumerate()
.map(|(i, item)| self.sign_single_item(item, base + i as u64))
.collect()
}
}
pub fn sign_group(
&mut self,
items: Vec<OrderItem>,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
if items.is_empty() {
return Err(Error::EmptyOrders);
}
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::Order { orders: items };
self.sign_action_self(&action, nonce)
}
fn sign_single_item(&self, item: OrderItem, nonce: u64) -> Result<SignedTransaction> {
let account = self.keypair.pubkey();
let signer_pubkey = self.keypair.pubkey();
let order_id = if self.compute_order_id {
let mut scratch = Vec::with_capacity(96);
compute_order_item_id_at_index(&item, 0, nonce, &account, &mut scratch)
.map(|id| id.to_base58())
} else {
None
};
let action = Action::Order { orders: vec![item] };
let mut serializer = Vec::with_capacity(512);
serialize_for_sdk_signing(
&action,
self.signature_domain,
nonce,
&account,
&mut serializer,
)?;
let signature = self.sign_bytes(&serializer);
let actions = self.action_to_json(&action)?;
Ok(SignedTransaction {
actions,
nonce,
account: account.to_base58(),
signer: signer_pubkey.to_base58(),
signature,
order_id,
order_ids: None,
})
}
pub fn sign_faucet(&mut self, nonce: Option<u64>) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::Faucet(Faucet::new(self.keypair.pubkey()));
self.sign_action_self(&action, nonce)
}
pub fn sign_agent_wallet(
&mut self,
agent: Pubkey,
delete: bool,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::AgentWalletCreation(AgentWallet { agent, delete });
self.sign_action_self(&action, nonce)
}
pub fn sign_approve_commission_fee(
&mut self,
to: Pubkey,
fee: u8,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
if fee == 0 || fee > MAX_COMMISSION_FEE_BPS {
return Err(Error::InvalidOrder(
"builder-code fee must be 1..=15 bps".to_string(),
));
}
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::ApproveCommissionFee(ApproveCommissionFee { to, max_fee: fee });
self.sign_action_self(&action, nonce)
}
#[inline]
pub fn sign_approve_builder_code(
&mut self,
to: Pubkey,
fee: u8,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
self.sign_approve_commission_fee(to, fee, nonce)
}
pub fn sign_revoke_commission_fee(
&mut self,
to: Pubkey,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::RevokeCommissionFee(RevokeCommissionFee { to });
self.sign_action_self(&action, nonce)
}
#[inline]
pub fn sign_revoke_builder_code(
&mut self,
to: Pubkey,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
self.sign_revoke_commission_fee(to, nonce)
}
pub fn sign_update_liquidator_config(
&mut self,
config: LiquidatorConfig,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::UpdateLiquidatorConfig(config);
self.sign_action_self(&action, nonce)
}
pub fn sign_user_settings(
&mut self,
settings: UserSettings,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::UpdateUserSettings(settings);
self.sign_action_self(&action, nonce)
}
pub fn sign_oracle_prices(
&mut self,
oracles: Vec<OraclePrice>,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
if oracles.is_empty() {
return Err(Error::InvalidOrder(
"oracle prices array cannot be empty".to_string(),
));
}
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::Oracle { oracles };
self.sign_action_self(&action, nonce)
}
pub fn sign_pyth_oracle(
&mut self,
oracles: Vec<PythOraclePrice>,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
if oracles.is_empty() {
return Err(Error::InvalidOrder(
"pyth oracle array cannot be empty".to_string(),
));
}
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::PythOracle { oracles };
self.sign_action_self(&action, nonce)
}
pub fn sign_whitelist_faucet(
&mut self,
target: Pubkey,
whitelist: bool,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::WhitelistFaucet(WhitelistFaucet { target, whitelist });
self.sign_action_self(&action, nonce)
}
pub fn sign_create_sub_account(
&mut self,
sub_account: CreateSubAccount,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::CreateSubAccount(sub_account);
self.sign_action_self(&action, nonce)
}
pub fn sign_remove_sub_account(
&mut self,
to_remove: Pubkey,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::RemoveSubAccount(RemoveSubAccount { to_remove });
self.sign_action_self(&action, nonce)
}
pub fn sign_rename_sub_account(
&mut self,
rename: RenameSubAccount,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::RenameSubAccount(rename);
self.sign_action_self(&action, nonce)
}
pub fn sign_transfer(
&mut self,
transfer: Transfer,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::Transfer(transfer);
self.sign_action_self(&action, nonce)
}
pub fn sign_withdraw(
&mut self,
withdraw: Withdraw,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::Withdraw(withdraw);
self.sign_action_self(&action, nonce)
}
pub fn sign_withdraw_lock_recover(
&mut self,
recover: WithdrawLockRecover,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::WithdrawLockRecover(recover);
self.sign_action_self(&action, nonce)
}
pub fn sign_create_multisig(
&mut self,
create_multisig: CreateMultisig,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::CreateMultisig(create_multisig);
self.sign_action_self(&action, nonce)
}
pub fn sign_multisig_propose(
&mut self,
propose: MultisigPropose,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::MultisigPropose(propose);
self.sign_action_self(&action, nonce)
}
pub fn sign_multisig_approve(
&mut self,
approve: MultisigApprove,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::MultisigApprove(approve);
self.sign_action_self(&action, nonce)
}
pub fn sign_multisig_reject(
&mut self,
reject: MultisigReject,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::MultisigReject(reject);
self.sign_action_self(&action, nonce)
}
pub fn sign_multisig_cancel(
&mut self,
cancel: MultisigCancel,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::MultisigCancel(cancel);
self.sign_action_self(&action, nonce)
}
pub fn sign_multisig_execute(
&mut self,
execute: MultisigExecute,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::MultisigExecute(execute);
self.sign_action_self(&action, nonce)
}
pub fn sign_update_multisig_policy(
&mut self,
update: UpdateMultisigPolicy,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
let nonce = nonce.unwrap_or_else(|| self.next_nonce());
let action = Action::UpdateMultisigPolicy(update);
self.sign_action_self(&action, nonce)
}
#[deprecated(
since = "0.2.0",
note = "Use sign(), sign_all(), or sign_group() instead"
)]
pub fn sign_order(
&mut self,
orders: Vec<OrderItem>,
nonce: Option<u64>,
) -> Result<SignedTransaction> {
self.sign_group(orders, nonce)
}
#[deprecated(since = "0.2.0", note = "Use sign_all() for simple batches")]
pub fn sign_orders_batch(
&self,
order_batches: Vec<Vec<OrderItem>>,
base_nonce: Option<u64>,
) -> Result<Vec<SignedTransaction>> {
if order_batches.is_empty() {
return Ok(vec![]);
}
let base = base_nonce.unwrap_or_else(crate::nonce::current_timestamp_nanos);
if order_batches.len() < PARALLEL_THRESHOLD {
order_batches
.into_iter()
.enumerate()
.map(|(i, orders)| self.sign_single_order_batch(orders, base + i as u64))
.collect()
} else {
order_batches
.into_par_iter()
.enumerate()
.map(|(i, orders)| self.sign_single_order_batch(orders, base + i as u64))
.collect()
}
}
fn sign_single_order_batch(
&self,
orders: Vec<OrderItem>,
nonce: u64,
) -> Result<SignedTransaction> {
if orders.is_empty() {
return Err(Error::EmptyOrders);
}
let account = self.keypair.pubkey();
let signer_pubkey = self.keypair.pubkey();
let order_id = if self.compute_order_id && orders.len() == 1 {
let mut scratch = Vec::with_capacity(96);
compute_order_item_id_at_index(&orders[0], 0, nonce, &account, &mut scratch)
.map(|id| id.to_base58())
} else {
None
};
let order_ids = if self.compute_batch_order_ids && orders.len() > 1 {
let mut scratch = Vec::with_capacity(96);
let mut ids = Vec::with_capacity(orders.len());
for (idx, item) in orders.iter().enumerate() {
if let Some(id) =
compute_order_item_id_at_index(item, idx as u32, nonce, &account, &mut scratch)
{
ids.push(id.to_base58());
}
}
if ids.is_empty() {
None
} else {
Some(ids)
}
} else {
None
};
let action = Action::Order { orders };
let mut serializer = Vec::with_capacity(512);
serialize_for_sdk_signing(
&action,
self.signature_domain,
nonce,
&account,
&mut serializer,
)?;
let signature = self.sign_bytes(&serializer);
let actions = self.action_to_json(&action)?;
Ok(SignedTransaction {
actions,
nonce,
account: account.to_base58(),
signer: signer_pubkey.to_base58(),
signature,
order_id,
order_ids,
})
}
fn compute_action_order_id(
&self,
action: &Action,
nonce: u64,
account: &Pubkey,
) -> Option<String> {
match action {
Action::Order { orders } if orders.len() == 1 => {
let mut scratch = Vec::with_capacity(96);
compute_order_item_id_at_index(&orders[0], 0, nonce, account, &mut scratch)
.map(|id| id.to_base58())
}
_ => None,
}
}
fn compute_action_order_ids(
&self,
action: &Action,
nonce: u64,
account: &Pubkey,
) -> Option<Vec<String>> {
match action {
Action::Order { orders } if orders.len() > 1 => {
let mut scratch = Vec::with_capacity(96);
let mut ids = Vec::with_capacity(orders.len());
for (idx, item) in orders.iter().enumerate() {
if let Some(id) = compute_order_item_id_at_index(
item,
idx as u32,
nonce,
account,
&mut scratch,
) {
ids.push(id.to_base58());
}
}
if ids.is_empty() {
None
} else {
Some(ids)
}
}
_ => None,
}
}
fn action_to_json(&self, action: &Action) -> Result<Vec<serde_json::Value>> {
match action {
Action::Order { orders } => orders
.iter()
.map(|item| self.order_item_to_json(item))
.collect(),
Action::Faucet(faucet) => {
let mut faucet_obj = json!({ "u": faucet.user.to_base58() });
if let Some(amount) = faucet.amount {
faucet_obj["amount"] = json!(amount);
}
Ok(vec![json!({ "faucet": faucet_obj })])
}
Action::AgentWalletCreation(agent) => Ok(vec![json!({
"agentWalletCreation": {
"a": agent.agent.to_base58(),
"d": agent.delete
}
})]),
Action::UpdateUserSettings(settings) => {
let mut ordered = settings.max_leverage.clone();
ordered.sort_unstable_by(|left, right| left.0.cmp(&right.0));
let mut leverage = serde_json::Map::with_capacity(ordered.len());
for (symbol, lev) in ordered {
leverage.insert(symbol, json!(lev));
}
Ok(vec![json!({ "updateUserSettings": { "m": leverage } })])
}
Action::Oracle { oracles } => Ok(oracles
.iter()
.map(|o| {
json!({
"px": {
"t": o.timestamp,
"c": o.asset,
"px": o.price
}
})
})
.collect()),
Action::PythOracle { oracles } => {
let entries: Vec<_> = oracles
.iter()
.map(|o| {
json!({
"t": o.timestamp,
"fi": o.feed_index,
"px": o.price,
"e": o.exponent
})
})
.collect();
Ok(vec![json!({ "o": { "oracles": entries } })])
}
Action::WhitelistFaucet(action) => Ok(vec![json!({
"whitelistFaucet": {
"target": action.target.to_base58(),
"whitelist": action.whitelist
}
})]),
Action::CreateSubAccount(action) => {
let mut obj = json!({ "name": action.name });
if let Some(amount) = action.margin_amount {
obj["marginAmount"] = json!(amount);
}
Ok(vec![json!({ "createSubAccount": obj })])
}
Action::RemoveSubAccount(action) => Ok(vec![json!({
"removeSubAccount": {
"toRemove": action.to_remove.to_base58()
}
})]),
Action::RenameSubAccount(action) => Ok(vec![json!({
"renameSubAccount": {
"account": action.account.to_base58(),
"name": action.name
}
})]),
Action::Transfer(transfer) => Ok(vec![json!({
"transfer": {
"k": match transfer.kind {
TransferKind::Internal => "internal",
TransferKind::External => "external",
},
"from": transfer.from.to_base58(),
"to": transfer.to.to_base58(),
"marginAmount": transfer.margin_amount,
}
})]),
Action::Withdraw(_) | Action::WithdrawLockRecover(_) => Err(Error::LegacyWithdrawal),
Action::CreateMultisig(action) => Ok(vec![json!({
"createMultisig": {
"signers": action.signers.iter().map(Pubkey::to_base58).collect::<Vec<_>>(),
"threshold": action.threshold,
"timeLockSecs": action.time_lock_secs,
"proposalLifetimeSecs": action.proposal_lifetime_secs,
}
})]),
Action::MultisigPropose(action) => {
let mut inner_actions = Vec::new();
for inner in &action.actions {
inner_actions.extend(self.action_to_json(inner)?);
}
Ok(vec![json!({
"msp": {
"m": action.multisig.to_base58(),
"a": inner_actions,
}
})])
}
Action::MultisigApprove(action) => Ok(vec![json!({
"msa": {
"m": action.multisig.to_base58(),
"p": action.proposal_id,
}
})]),
Action::MultisigReject(action) => Ok(vec![json!({
"msr": {
"m": action.multisig.to_base58(),
"p": action.proposal_id,
}
})]),
Action::MultisigCancel(action) => Ok(vec![json!({
"msc": {
"m": action.multisig.to_base58(),
"p": action.proposal_id,
}
})]),
Action::MultisigExecute(action) => Ok(vec![json!({
"mse": {
"m": action.multisig.to_base58(),
"p": action.proposal_id,
}
})]),
Action::UpdateMultisigPolicy(action) => Ok(vec![json!({
"msu": {
"m": action.multisig.to_base58(),
"signers": action.signers.iter().map(Pubkey::to_base58).collect::<Vec<_>>(),
"threshold": action.threshold,
"timeLockSecs": action.time_lock_secs,
"proposalLifetimeSecs": action.proposal_lifetime_secs,
}
})]),
Action::ApproveCommissionFee(action) => Ok(vec![json!({
"abc": {
"to": action.to.to_base58(),
"fee": action.max_fee
}
})]),
Action::RevokeCommissionFee(action) => Ok(vec![json!({
"rbc": {
"to": action.to.to_base58()
}
})]),
Action::UpdateLiquidatorConfig(config) => {
Ok(vec![json!({ "liq": liquidator_config_to_json(config) })])
}
}
}
fn order_item_to_json(&self, item: &OrderItem) -> Result<serde_json::Value> {
match item {
OrderItem::Order(order) => match &order.order_type {
OrderType::Limit { tif } => {
let tif_str = match tif {
TimeInForce::Gtc => "GTC",
TimeInForce::Ioc => "IOC",
TimeInForce::Alo => "ALO",
};
let mut body = json!({
"c": order.symbol,
"b": order.is_buy,
"px": order.price,
"sz": order.size,
"tif": tif_str,
"r": order.reduce_only,
"i": order.iso
});
if let Some(commission) = order.commission {
body["builderCode"] = json!({
"to": commission.to.to_base58(),
"fee": commission.fee
});
}
Ok(json!({ "l": body }))
}
OrderType::Trigger {
is_market,
trigger_px: _,
} => {
if !is_market {
return Err(Error::InvalidOrder(
"trigger orders are not supported by BULK API; use market".to_string(),
));
}
let mut body = json!({
"c": order.symbol,
"b": order.is_buy,
"sz": order.size,
"r": order.reduce_only,
"i": order.iso
});
if let Some(commission) = order.commission {
body["builderCode"] = json!({
"to": commission.to.to_base58(),
"fee": commission.fee
});
}
if let Some(slippage) = order.slippage {
body["slippage"] = json!(slippage);
}
Ok(json!({ "m": body }))
}
},
OrderItem::Modify(modify) => Ok(json!({
"mod": {
"oid": modify.order_id.to_base58(),
"c": modify.symbol,
"sz": modify.amount
}
})),
OrderItem::Cancel(cancel) => Ok(json!({
"cx": {
"c": cancel.symbol,
"oid": cancel.order_id.to_base58()
}
})),
OrderItem::CancelAll(cancel_all) => Ok(json!({
"cxa": {
"c": cancel_all.symbols
}
})),
OrderItem::Stop(stop) => {
let mut body = json!({
"c": stop.symbol,
"d": stop.is_buy,
"sz": stop.size,
"tr": stop.trigger_price,
"lim": stop.limit_price,
"i": stop.iso
});
add_builder_code(&mut body, stop.commission);
Ok(json!({ "st": body }))
}
OrderItem::TakeProfit(tp) => {
let mut body = json!({
"c": tp.symbol,
"d": tp.is_buy,
"sz": tp.size,
"tr": tp.trigger_price,
"lim": tp.limit_price,
"i": tp.iso
});
add_builder_code(&mut body, tp.commission);
Ok(json!({ "tp": body }))
}
OrderItem::RangeOco(rng) => {
let mut body = json!({
"c": rng.symbol,
"d": rng.is_buy,
"sz": rng.size,
"pmin": rng.collar_min,
"pmax": rng.collar_max,
"lmin": rng.limit_min,
"lmax": rng.limit_max,
"i": rng.iso
});
add_builder_code(&mut body, rng.commission);
Ok(json!({ "rng": body }))
}
OrderItem::TriggerBasket(trig) => {
let nested: Result<Vec<_>> = trig
.actions
.iter()
.map(|a| self.order_item_to_json(a))
.collect();
Ok(json!({
"trig": {
"c": trig.symbol,
"d": trig.is_buy,
"tr": trig.trigger_price,
"actions": nested?
}
}))
}
OrderItem::OnFill(of) => {
let trigger = self.order_item_to_json(&of.trigger)?;
let actions: Result<Vec<_>> = of
.actions
.iter()
.map(|a| self.order_item_to_json(a))
.collect();
Ok(json!({
"of": {
"trigger": trigger,
"actions": actions?
}
}))
}
OrderItem::TrailingStop(trl) => {
let mut body = json!({
"c": trl.symbol,
"b": trl.is_buy,
"sz": trl.size,
"trb": trl.trail_bps,
"stb": trl.step_bps,
"lim": trl.limit_price,
"i": trl.iso
});
add_builder_code(&mut body, trl.commission);
Ok(json!({ "trl": body }))
}
}
}
}
fn add_builder_code(body: &mut serde_json::Value, commission: Option<Commission>) {
if let Some(commission) = commission {
body["builderCode"] = json!({ "to": commission.to.to_base58(), "fee": commission.fee });
}
}
#[cfg(test)]
mod tests {
use super::*;
fn devnet_signer(keypair: Keypair) -> Signer {
Signer::new(keypair, SignatureDomain::Devnet)
}
#[test]
fn test_sign_single() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
let signed = signer.sign(order.into(), Some(1234567890)).unwrap();
assert_eq!(signed.nonce, 1234567890);
assert_eq!(signed.actions.len(), 1);
assert!(signed.actions[0].get("l").is_some());
assert!(!signed.signature.is_empty());
}
#[test]
fn test_sign_modify_uses_compact_sdk_keys() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let modify = Modify::new(Hash::random(), "BTC-USD", 0.25);
let signed = signer
.sign(OrderItem::Modify(modify), Some(1234567890))
.unwrap();
let mod_obj = signed.actions[0].get("mod").unwrap();
assert!(mod_obj.get("c").is_some());
assert!(mod_obj.get("sz").is_some());
assert!(mod_obj.get("symbol").is_none());
assert!(mod_obj.get("amount").is_none());
}
#[test]
fn test_sign_all_parallel_nonce_increment() {
let keypair = Keypair::generate();
let signer = devnet_signer(keypair);
let orders: Vec<OrderItem> = (0..20)
.map(|i| {
Order::limit(
"BTC-USD",
i % 2 == 0,
100000.0 + i as f64,
0.1,
TimeInForce::Gtc,
)
.into()
})
.collect();
let signed = signer.sign_all(orders, Some(1000000)).unwrap();
assert_eq!(signed.len(), 20);
for (i, tx) in signed.iter().enumerate() {
assert_eq!(tx.nonce, 1000000 + i as u64);
assert!(tx.order_id.is_some());
assert!(tx.order_ids.is_none());
}
}
#[test]
fn test_sign_group_atomic() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let bracket: Vec<OrderItem> = vec![
Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc).into(),
Order::limit("BTC-USD", false, 99000.0, 0.1, TimeInForce::Gtc).into(),
Order::limit("BTC-USD", false, 110000.0, 0.1, TimeInForce::Gtc).into(),
];
let signed = signer.sign_group(bracket, Some(1234567890)).unwrap();
assert_eq!(signed.actions.len(), 3);
assert_eq!(signed.nonce, 1234567890);
assert!(signed.order_ids.is_none());
}
#[test]
fn test_sign_faucet() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let signed = signer.sign_faucet(Some(1234567890)).unwrap();
assert_eq!(signed.actions.len(), 1);
assert!(signed.actions[0].get("faucet").is_some());
}
#[test]
fn test_sign_agent_wallet() {
let keypair = Keypair::generate();
let agent_keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_agent_wallet(agent_keypair.pubkey(), false, Some(1234567890))
.unwrap();
assert!(signed.actions[0].get("agentWalletCreation").is_some());
}
#[test]
fn test_sign_group_empty_error() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let result = signer.sign_group(vec![], Some(1234567890));
assert!(matches!(result, Err(Error::EmptyOrders)));
}
#[test]
fn test_sign_oracle_prices() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_oracle_prices(
vec![OraclePrice {
timestamp: 1704067200000000000,
asset: "BTC".to_string(),
price: 102500.0,
}],
Some(1234567890),
)
.unwrap();
assert_eq!(signed.actions.len(), 1);
assert!(signed.actions[0].get("px").is_some());
}
#[test]
fn test_sign_pyth_oracle() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_pyth_oracle(
vec![PythOraclePrice {
timestamp: 1704067200000000000,
feed_index: 1,
price: 10250000000000,
exponent: -8,
}],
Some(1234567890),
)
.unwrap();
assert_eq!(signed.actions.len(), 1);
assert!(signed.actions[0].get("o").is_some());
}
#[test]
fn test_sign_user_settings_uses_sdk_map_shape() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_user_settings(
UserSettings::new(vec![("BTC".to_string(), 5.0), ("ETH".to_string(), 3.0)]),
Some(1234567890),
)
.unwrap();
let obj = signed.actions[0].get("updateUserSettings").unwrap();
let leverage = obj.get("m").and_then(|v| v.as_object()).unwrap();
assert_eq!(leverage.get("BTC").and_then(|v| v.as_f64()), Some(5.0));
assert_eq!(leverage.get("ETH").and_then(|v| v.as_f64()), Some(3.0));
}
#[test]
fn test_sign_user_settings_is_stable_across_input_order() {
let keypair = Keypair::generate();
let settings_a =
UserSettings::new(vec![("BTC".to_string(), 5.0), ("ETH".to_string(), 3.0)]);
let settings_b =
UserSettings::new(vec![("ETH".to_string(), 3.0), ("BTC".to_string(), 5.0)]);
let mut signer_a = devnet_signer(keypair.clone());
let mut signer_b = devnet_signer(keypair);
let signed_a = signer_a
.sign_user_settings(settings_a, Some(1234567890))
.unwrap();
let signed_b = signer_b
.sign_user_settings(settings_b, Some(1234567890))
.unwrap();
assert_eq!(signed_a.signature, signed_b.signature);
assert_eq!(signed_a.actions, signed_b.actions);
}
#[test]
fn test_sign_create_sub_account() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_create_sub_account(CreateSubAccount::new("desk-1"), Some(1234567890))
.unwrap();
assert_eq!(signed.actions.len(), 1);
let obj = signed.actions[0].get("createSubAccount").unwrap();
assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("desk-1"));
assert!(obj.get("marginSymbol").is_none());
assert!(obj.get("marginAmount").is_none());
}
#[test]
fn test_sign_create_sub_account_with_margin() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_create_sub_account(
CreateSubAccount::with_margin("desk-1", 1000.0),
Some(1234567890),
)
.unwrap();
let obj = signed.actions[0].get("createSubAccount").unwrap();
assert!(obj.get("marginSymbol").is_none());
assert_eq!(
obj.get("marginAmount").and_then(|v| v.as_f64()),
Some(1000.0)
);
}
#[test]
fn test_sign_remove_sub_account() {
let keypair = Keypair::generate();
let target = Keypair::generate().pubkey();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_remove_sub_account(target, Some(1234567890))
.unwrap();
let obj = signed.actions[0].get("removeSubAccount").unwrap();
assert_eq!(
obj.get("toRemove").and_then(|v| v.as_str()),
Some(target.to_base58().as_str())
);
}
#[test]
fn test_sign_rename_sub_account() {
let keypair = Keypair::generate();
let target = Keypair::generate().pubkey();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_rename_sub_account(RenameSubAccount::new(target, "desk-2"), Some(1234567890))
.unwrap();
let obj = signed.actions[0].get("renameSubAccount").unwrap();
assert_eq!(
obj.get("account").and_then(|v| v.as_str()),
Some(target.to_base58().as_str())
);
assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("desk-2"));
}
#[test]
fn test_sign_transfer_internal() {
let keypair = Keypair::generate();
let from = keypair.pubkey();
let to = Keypair::generate().pubkey();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_transfer(Transfer::internal(from, to, 10.0), Some(1234567890))
.unwrap();
let obj = signed.actions[0].get("transfer").unwrap();
assert_eq!(obj.get("k").and_then(|v| v.as_str()), Some("internal"));
assert!(obj.get("marginSymbol").is_none());
assert_eq!(obj.get("marginAmount").and_then(|v| v.as_f64()), Some(10.0));
}
#[test]
fn test_sign_whitelist_faucet() {
let keypair = Keypair::generate();
let target = Keypair::generate().pubkey();
let mut signer = devnet_signer(keypair);
let signed = signer
.sign_whitelist_faucet(target, true, Some(1234567890))
.unwrap();
assert_eq!(signed.actions.len(), 1);
assert!(signed.actions[0].get("whitelistFaucet").is_some());
}
#[test]
fn test_commission_order_json_and_order_id() {
let keypair = Keypair::generate();
let recipient = Pubkey::from_bytes([9u8; 32]);
let mut signer = devnet_signer(keypair);
let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
let plain = signer.sign(order.clone().into(), Some(1234567890)).unwrap();
let commissioned = signer
.sign(
order.with_builder_code(recipient, 5).unwrap().into(),
Some(1234567890),
)
.unwrap();
assert!(plain.actions[0]["l"].get("builderCode").is_none());
assert_eq!(commissioned.actions[0]["l"]["builderCode"]["fee"], 5);
assert_eq!(
commissioned.actions[0]["l"]["builderCode"]["to"],
recipient.to_base58()
);
assert_eq!(plain.actions[0]["l"]["i"], false);
assert_eq!(plain.order_id, commissioned.order_id);
assert_ne!(plain.signature, commissioned.signature);
}
#[test]
fn test_sign_commission_approval_actions() {
let keypair = Keypair::generate();
let recipient = Pubkey::from_bytes([7u8; 32]);
let mut signer = devnet_signer(keypair);
let approve = signer
.sign_approve_builder_code(recipient, 5, Some(1234567890))
.unwrap();
let revoke = signer
.sign_revoke_builder_code(recipient, Some(1234567891))
.unwrap();
assert_eq!(approve.actions[0]["abc"]["fee"], 5);
assert_eq!(approve.actions[0]["abc"]["to"], recipient.to_base58());
assert_eq!(revoke.actions[0]["rbc"]["to"], recipient.to_base58());
assert!(approve.order_id.is_none());
assert!(revoke.order_id.is_none());
assert!(signer
.sign_approve_builder_code(recipient, 0, Some(1234567892))
.is_err());
assert!(signer
.sign_approve_builder_code(recipient, 16, Some(1234567893))
.is_err());
}
#[test]
fn test_order_id_computed_by_default() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair);
let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
let signed = signer.sign(order.into(), Some(1234567890)).unwrap();
assert!(signed.order_id.is_some());
}
#[test]
fn test_order_id_disabled() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair).without_order_id();
let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
let signed = signer.sign(order.into(), Some(1234567890)).unwrap();
assert!(signed.order_id.is_none());
}
#[test]
fn test_batch_order_ids_optional_enabled() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair).with_batch_order_ids();
assert!(signer.computes_batch_order_ids());
let bracket: Vec<OrderItem> = vec![
Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc).into(),
Order::limit("BTC-USD", false, 99000.0, 0.1, TimeInForce::Gtc).into(),
Order::limit("BTC-USD", false, 110000.0, 0.1, TimeInForce::Gtc).into(),
];
let signed = signer.sign_group(bracket, Some(1234567890)).unwrap();
let ids = signed.order_ids.expect("order_ids should be present");
assert_eq!(ids.len(), 3);
assert!(signed.order_id.is_none());
}
#[test]
#[allow(deprecated)]
fn test_legacy_methods_still_work() {
let keypair = Keypair::generate();
let mut signer = devnet_signer(keypair.clone());
let order = Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc);
let one = signer
.sign_order(vec![order.clone().into()], Some(100))
.unwrap();
assert_eq!(one.actions.len(), 1);
let signer = devnet_signer(keypair);
let batches = vec![vec![order.into()]];
let many = signer.sign_orders_batch(batches, Some(200)).unwrap();
assert_eq!(many.len(), 1);
}
#[test]
#[allow(deprecated)]
fn test_legacy_batch_order_ids_enabled() {
let keypair = Keypair::generate();
let signer = devnet_signer(keypair).with_batch_order_ids();
let batches = vec![vec![
Order::limit("BTC-USD", true, 100000.0, 0.1, TimeInForce::Gtc).into(),
Order::limit("BTC-USD", false, 110000.0, 0.1, TimeInForce::Gtc).into(),
]];
let signed = signer.sign_orders_batch(batches, Some(300)).unwrap();
assert_eq!(signed.len(), 1);
assert_eq!(signed[0].order_ids.as_ref().map(Vec::len), Some(2));
}
#[test]
fn legacy_withdrawal_signing_fails_closed() {
let mut signer = Signer::new(Keypair::generate(), SignatureDomain::Devnet);
for action in [
Action::Withdraw(Withdraw {
user: signer.pubkey(),
vault: Pubkey::from_bytes([2; 32]),
recipient_token_account: Pubkey::from_bytes([3; 32]),
amount: 42,
blockhash: Hash::from_bytes([4; 32]),
}),
Action::WithdrawLockRecover(WithdrawLockRecover {
user: signer.pubkey(),
hash: Hash::from_bytes([5; 32]),
}),
] {
assert!(matches!(
match action.clone() {
Action::Withdraw(withdraw) => signer.sign_withdraw(withdraw, Some(1)),
Action::WithdrawLockRecover(recover) =>
signer.sign_withdraw_lock_recover(recover, Some(1)),
_ => unreachable!(),
},
Err(Error::LegacyWithdrawal)
));
assert!(matches!(
signer.sign_action_self(&action, 1),
Err(Error::LegacyWithdrawal)
));
assert!(matches!(
signer.sign_action_self(
&Action::MultisigPropose(MultisigPropose::new(
signer.pubkey(),
vec![Action::MultisigPropose(MultisigPropose::new(
signer.pubkey(),
vec![action]
)),]
)),
1,
),
Err(Error::LegacyWithdrawal)
));
}
assert!(signer
.sign_transfer(
Transfer::internal(signer.pubkey(), Pubkey::from_bytes([6; 32]), 1.0),
Some(2),
)
.is_ok());
}
}