From 9132085097e0845508efb0b332080820a2c11413 Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Wed, 2 Apr 2025 15:12:13 +0900 Subject: [PATCH] Add generalized decoding of swift delegated and authority signed orders --- Cargo.lock | 1 + Cargo.toml | 1 + crates/src/lib.rs | 65 +----- crates/src/swift_order_subscriber.rs | 292 +++++++++++++++------------ 4 files changed, 168 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18c18bf..10a1c18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1233,6 +1233,7 @@ dependencies = [ "abi_stable", "ahash", "anchor-lang", + "arrayvec", "base64 0.22.1", "bytemuck", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 88d3aa1..9bb9aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tokio-stream = "0.1.17" tokio-tungstenite = { version = "0.26", features = ["native-tls"] } drift-pubsub-client = { version = "0.1.1", path = "crates/pubsub-client" } +arrayvec = "0.7.6" [dev-dependencies] bytes = "1" diff --git a/crates/src/lib.rs b/crates/src/lib.rs index 96f5a3a..49ca4d1 100644 --- a/crates/src/lib.rs +++ b/crates/src/lib.rs @@ -18,7 +18,6 @@ use solana_sdk::{ signature::Signature, }; pub use solana_sdk::{address_lookup_table::AddressLookupTableAccount, pubkey::Pubkey}; -use swift_order_subscriber::SignedDelegateOrderInfo; use crate::{ account_map::AccountMap, @@ -1665,10 +1664,7 @@ impl<'a> TransactionBuilder<'a> { authority: self.authority, user: self.sub_account, user_stats: Wallet::derive_stats_account(&self.authority), - taker: Wallet::derive_user_account( - &taker_account.authority, - signed_order_info.taker_subaccount_id(), - ), + taker: signed_order_info.taker_subaccount(&taker_account.authority), taker_stats: Wallet::derive_stats_account(&taker_account.authority), taker_signed_msg_user_orders: Wallet::derive_swift_order_account( &taker_account.authority, @@ -1708,7 +1704,7 @@ impl<'a> TransactionBuilder<'a> { /// or see `place_and_make_swift_order` /// /// * `signed_order_info` - the signed swift order info - /// * `taker_account` - taker account data (authority of the swift order) + /// * `taker_account` - taker subaccount data /// pub fn place_swift_order( mut self, @@ -1727,62 +1723,7 @@ impl<'a> TransactionBuilder<'a> { types::accounts::PlaceSignedMsgTakerOrder { state: *state_account(), authority: self.authority, - user: Wallet::derive_user_account( - &taker_account.authority, - signed_order_info.taker_subaccount_id(), - ), - user_stats: Wallet::derive_stats_account(&taker_account.authority), - signed_msg_user_orders: Wallet::derive_swift_order_account( - &taker_account.authority, - ), - ix_sysvar: SYSVAR_INSTRUCTIONS_PUBKEY, - }, - &[taker_account], - self.force_markets.readable.iter(), - perp_writable - .iter() - .chain(self.force_markets.writeable.iter()), - ); - - let swift_taker_ix_data = signed_order_info.to_ix_data(); - let ed25519_verify_ix = crate::utils::new_ed25519_ix_ptr( - swift_taker_ix_data.as_slice(), - self.ixs.len() as u16 + 1, - ); - - let place_swift_ix = Instruction { - program_id: constants::PROGRAM_ID, - accounts, - data: InstructionData::data(&drift_idl::instructions::PlaceSignedMsgTakerOrder { - signed_msg_order_params_message_bytes: swift_taker_ix_data, - is_delegate_signer: signed_order_info.using_delegate_signing(), - }), - }; - - self.ixs - .extend_from_slice(&[ed25519_verify_ix, place_swift_ix]); - self - } - - /// Place a swift delegate order. Same as above but with a different account type provided - pub fn place_swift_delegate_order( - mut self, - signed_order_info: &SignedDelegateOrderInfo, - taker_account: &User, - ) -> Self { - let order_params = signed_order_info.order_params(); - assert!( - order_params.market_type == MarketType::Perp, - "only swift perps are supported" - ); - - let perp_writable = [MarketId::perp(order_params.market_index)]; - let accounts = build_accounts( - self.program_data, - types::accounts::PlaceSignedMsgTakerOrder { - state: *state_account(), - authority: self.authority, - user: signed_order_info.taker_pubkey(), + user: signed_order_info.taker_subaccount(&taker_account.authority), user_stats: Wallet::derive_stats_account(&taker_account.authority), signed_msg_user_orders: Wallet::derive_swift_order_account( &taker_account.authority, diff --git a/crates/src/swift_order_subscriber.rs b/crates/src/swift_order_subscriber.rs index f261d98..fea4d93 100644 --- a/crates/src/swift_order_subscriber.rs +++ b/crates/src/swift_order_subscriber.rs @@ -3,7 +3,11 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use anchor_lang::{AnchorDeserialize, AnchorSerialize, Space}; +use anchor_lang::{ + prelude::borsh::{self}, + AnchorDeserialize, AnchorSerialize, InitSpace, Space, +}; +use arrayvec::ArrayVec; use base64::Engine; use futures_util::{SinkExt, StreamExt}; use serde::Deserialize; @@ -17,7 +21,7 @@ pub use crate::types::SignedMsgOrderParamsMessage as SignedOrder; use crate::{ constants::MarketExt, types::{Context, MarketId, OrderParams, SdkError, SdkResult}, - DriftClient, + DriftClient, Wallet, }; /// Swift message discriminator (Anchor) @@ -39,15 +43,53 @@ pub const SWIFT_MAINNET_WS_URL: &str = "wss://swift.drift.trade"; const LOG_TARGET: &str = "swift"; +/// Wrapper for a signed order message (aka swift order) +/// +/// It can be either signed by the authority keypair or an authorized delegate +#[derive(Clone, Debug, PartialEq, AnchorSerialize, AnchorDeserialize, InitSpace)] +pub enum SignedOrderType { + /// Swift order signed by authority keypair + Authority(SignedOrder), + /// Swift order signed by a delegated keypair + Delegated(SignedDelegateOrder), +} + +impl SignedOrderType { + /// Returns true if this is a delegated signed msg order + pub fn is_delegated(&self) -> bool { + matches!(self, Self::Delegated(_)) + } + /// Serialize as a borsh buffer + /// This differs from `AnchorSerialize` as it does _not_ encode the enum byte + /// Swift clients do not encode or decode the enum byte + pub fn to_borsh(&self) -> ArrayVec { + // SignedOrderType::INIT_SPACE (max variant size) -1 (no enum byte) +8 (anchor discriminator len) + let mut buf = ArrayVec::new(); + match self { + Self::Authority(ref x) => { + (*SWIFT_MSG_PREFIX).serialize(&mut buf).unwrap(); + x.serialize(&mut buf).unwrap(); + } + Self::Delegated(ref x) => { + (*SWIFT_DELEGATE_MSG_PREFIX).serialize(&mut buf).unwrap(); + x.serialize(&mut buf).unwrap(); + } + } + + buf + } +} + +/// Order notification from Websocket #[derive(Clone, Deserialize)] -pub struct OrderNotification<'a> { +struct OrderNotification<'a> { #[allow(dead_code)] channel: &'a str, order: SignedOrderInfo, } #[derive(Deserialize)] -pub struct Heartbeat { +struct Heartbeat { #[serde(deserialize_with = "deser_int_str", rename = "message")] ts: u64, } @@ -72,8 +114,8 @@ pub struct SignedOrderInfo { pub signer: Pubkey, /// hex-ified, borsh encoded signed order message /// this is the signed/verified payload for onchain use - #[serde(rename = "order_message", deserialize_with = "deser_order_message")] - order: SignedOrder, + #[serde(rename = "order_message", deserialize_with = "deser_signed_msg_type")] + order: SignedOrderType, /// Signature over the serialized `order` payload #[serde(rename = "order_signature", deserialize_with = "deser_signature")] pub signature: Signature, @@ -86,114 +128,32 @@ impl SignedOrderInfo { } /// The order's UUID (raw) pub fn order_uuid(&self) -> [u8; 8] { - self.order.uuid + match self.order { + SignedOrderType::Authority(inner) => inner.uuid, + SignedOrderType::Delegated(inner) => inner.uuid, + } } /// The drift order params of the message pub fn order_params(&self) -> OrderParams { - self.order.signed_msg_order_params - } - /// The taker sub-account_id of the order - pub fn taker_subaccount_id(&self) -> u16 { - self.order.sub_account_id - } - /// serialize the order message for onchain use e.g. signature verification - pub fn encode_for_signing(&self) -> Vec { - let mut buf = Vec::with_capacity(SignedOrder::INIT_SPACE + 8); - buf.extend_from_slice(SWIFT_MSG_PREFIX.as_slice()); - self.order - .serialize(&mut buf) - .expect("swift msg serialized"); - hex::encode(buf).into_bytes() - } - /// convert swift order into anchor ix data - pub fn to_ix_data(&self) -> Vec { - let signed_msg = self.encode_for_signing(); - [ - self.signature.as_ref(), - self.signer.as_ref(), - &(signed_msg.len() as u16).to_le_bytes(), - signed_msg.as_ref(), - ] - .concat() - } - /// True if the message was signed by an identity other than the authority i.e a delegated - pub fn using_delegate_signing(&self) -> bool { - self.taker_authority != self.signer - } - - pub fn new( - uuid: String, - taker_authority: Pubkey, - signer: Pubkey, - order: SignedOrder, - signature: Signature, - ) -> Self { - Self { - uuid, - ts: unix_now_ms(), - taker_authority, - signer, - order, - signature, + match self.order { + SignedOrderType::Authority(inner) => inner.signed_msg_order_params, + SignedOrderType::Delegated(inner) => inner.signed_msg_order_params, } } -} - -/// Swift order and metadata fresh from the Websocket -/// -/// This is an off-chain authorization for a taker order. -/// It may be placed and filled by any willing counter-party, ensuring the time-price bounds -/// are respected. -#[derive(Clone, Debug, Deserialize)] -pub struct SignedDelegateOrderInfo { - /// Swift order uuid - uuid: String, - /// Order creation timestamp (unix ms) - pub ts: u64, - /// The taker authority pubkey - #[serde(deserialize_with = "deser_pubkey")] - pub taker_authority: Pubkey, - /// The authority pubkey that verifies `signature` - /// it is either the taker authority or a sub-account delegate - #[serde(rename = "signing_authority", deserialize_with = "deser_pubkey")] - pub signer: Pubkey, - /// hex-ified, borsh encoded signed order message - /// this is the signed/verified payload for onchain use - #[serde( - rename = "order_message", - deserialize_with = "deser_order_delegate_message" - )] - order: SignedDelegateOrder, - /// Signature over the serialized `order` payload - #[serde(rename = "order_signature", deserialize_with = "deser_signature")] - pub signature: Signature, -} - -impl SignedDelegateOrderInfo { - /// The taker pubkey that delegate is signing for - pub fn taker_pubkey(&self) -> Pubkey { - self.order.taker_pubkey - } - /// The order's UUID (stringified) - pub fn order_uuid_str(&self) -> &str { - self.uuid.as_ref() - } - /// The order's UUID (raw) - pub fn order_uuid(&self) -> [u8; 8] { - self.order.uuid - } - /// The drift order params of the message - pub fn order_params(&self) -> OrderParams { - self.order.signed_msg_order_params + /// Get the taker sub-account for the order + /// + /// `taker_authority` - the Authority pubkey of the taker's sub-account + pub fn taker_subaccount(&self, taker_authority: &Pubkey) -> Pubkey { + match self.order { + SignedOrderType::Authority(inner) => { + Wallet::derive_user_account(taker_authority, inner.sub_account_id) + } + SignedOrderType::Delegated(inner) => inner.taker_pubkey, + } } /// serialize the order message for onchain use e.g. signature verification pub fn encode_for_signing(&self) -> Vec { - let mut buf = Vec::with_capacity(SignedDelegateOrder::INIT_SPACE + 8); - buf.extend_from_slice(SWIFT_DELEGATE_MSG_PREFIX.as_slice()); - self.order - .serialize(&mut buf) - .expect("swift msg serialized"); - hex::encode(buf).into_bytes() + hex::encode(self.order.to_borsh()).into_bytes() } /// convert swift order into anchor ix data pub fn to_ix_data(&self) -> Vec { @@ -206,16 +166,17 @@ impl SignedDelegateOrderInfo { ] .concat() } - /// True if the message was signed by an identity other than the authority i.e a delegated + + /// Returns true if the order was signed using delegated authority pub fn using_delegate_signing(&self) -> bool { - self.taker_authority != self.signer + self.order.is_delegated() } - pub fn new( + pub(crate) fn new( uuid: String, taker_authority: Pubkey, signer: Pubkey, - order: SignedDelegateOrder, + order: SignedOrderType, signature: Signature, ) -> Self { Self { @@ -391,38 +352,49 @@ where Ok(Signature::try_from(base64::engine::general_purpose::STANDARD.decode(s).unwrap()).unwrap()) } -fn deser_order_message<'de, D>(deserializer: D) -> Result +fn deser_int_str<'de, D>(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { - let order_message: &str = serde::de::Deserialize::deserialize(deserializer)?; - let order_message_buf = hex::decode(order_message).expect("valid hex"); - Ok(AnchorDeserialize::deserialize(&mut &order_message_buf[8..]) - .expect("SignedMsgOrderParams deser")) + let s: &str = serde::de::Deserialize::deserialize(deserializer)?; + Ok(s.parse().unwrap()) } -fn deser_order_delegate_message<'de, D>(deserializer: D) -> Result +/// Deserialize hex-ified, borsh bytes as a `SignedOrderType` +pub fn deser_signed_msg_type<'de, D>(deserializer: D) -> Result where - D: serde::de::Deserializer<'de>, + D: serde::Deserializer<'de>, { - let order_message: &str = serde::de::Deserialize::deserialize(deserializer)?; - let order_message_buf = hex::decode(order_message).expect("valid hex"); - Ok(AnchorDeserialize::deserialize(&mut &order_message_buf[8..]) - .expect("SignedMsgOrderDelegateParams deser")) -} + let payload: &[u8] = serde::Deserialize::deserialize(deserializer)?; + if payload.len() % 2 != 0 { + return Err(serde::de::Error::custom("Hex string length must be even")); + } -fn deser_int_str<'de, D>(deserializer: D) -> Result -where - D: serde::de::Deserializer<'de>, -{ - let s: &str = serde::de::Deserialize::deserialize(deserializer)?; - Ok(s.parse().unwrap()) + // decode expecting the largest possible variant + let mut borsh_buf = [0u8; SignedDelegateOrder::INIT_SPACE + 8]; + + hex::decode_to_slice(payload, &mut borsh_buf[..payload.len() / 2]) + .map_err(serde::de::Error::custom)?; + + // this is basically the same as if we derived AnchorDeserialize on `SignedOrderType` _expect_ it does not + // add a u8 to distinguish the enum + if borsh_buf[..8] == *SWIFT_DELEGATE_MSG_PREFIX { + AnchorDeserialize::deserialize(&mut &borsh_buf[8..]) + .map(SignedOrderType::Delegated) + .map_err(serde::de::Error::custom) + } else { + AnchorDeserialize::deserialize(&mut &borsh_buf[8..]) + .map(SignedOrderType::Authority) + .map_err(serde::de::Error::custom) + } } #[cfg(test)] mod tests { use super::*; - use crate::types::MarketType; + use crate::types::{ + MarketType, OrderTriggerCondition, OrderType, PositionDirection, PostOnlyParam, + }; #[test] fn test_swift_order_deser() { @@ -469,4 +441,66 @@ mod tests { b"c8d5a65e2234f55d0001010080841e0000000000000000000000000002000000000000000001320124c6aa950000000001786b2f94000000000000bb64a9150000000074735730364f6d380000" ); } + + #[test] + fn deserialize_incoming_signed_message_delegated() { + let payload = serde_json::json!({ + "channel": "swift_orders_perp_2", + "order": { + "market_index": 2, + "market_type": "perp", + "order_message": "42656638c7259e230001010080841e00000000000000000000000000020000000000000000013201bb60507d000000000117c0127c00000000395311d51c1b87fd56c3b5872d1041111e51f399b12d291d981a0ea383407295272108160000000073386c754a4c5a650000", + "order_signature": "9G8luwFfeAc25HwXCgaUjrKv6yJHcMFDq4Z4uPXqom5mhwZ63YU5g7p07Kxe/AKSt5A/9OPDh3nN/c9IHjkCDA==", + "taker_authority": "4rmhwytmKH1XsgGAUyUUH7U64HS5FtT6gM8HGKAfwcFE", + "signing_authority": "GiMXQkJXLVjScmQDkoLJShBJpTh9SDPvT2AZQq8NyEBf", + "ts": 1739518796400_u64, + "uuid":"s8luJLZe" + } + }) + .to_string(); + let actual: OrderNotification<'_> = + serde_json::from_str(payload.as_str()).expect("deserializes"); + + assert_eq!( + actual.order.signer, + solana_sdk::pubkey!("GiMXQkJXLVjScmQDkoLJShBJpTh9SDPvT2AZQq8NyEBf") + ); + assert_eq!( + actual.order.taker_authority, + solana_sdk::pubkey!("4rmhwytmKH1XsgGAUyUUH7U64HS5FtT6gM8HGKAfwcFE") + ); + assert_eq!(actual.order.order_uuid_str(), "s8luJLZe"); + + if let SignedOrderType::Delegated(signed_msg) = actual.order.order { + let expected = SignedDelegateOrder { + signed_msg_order_params: OrderParams { + order_type: OrderType::Market, + market_type: MarketType::Perp, + direction: PositionDirection::Short, + user_order_id: 0, + base_asset_amount: 2000000, + price: 0, + market_index: 2, + reduce_only: false, + post_only: PostOnlyParam::None, + immediate_or_cancel: false, + max_ts: None, + trigger_price: None, + trigger_condition: OrderTriggerCondition::Above, + oracle_price_offset: None, + auction_duration: Some(50), + auction_start_price: Some(2102419643), + auction_end_price: Some(2081603607), + }, + taker_pubkey: solana_sdk::pubkey!("4rmhwytmKH1XsgGAUyUUH7U64HS5FtT6gM8HGKAfwcFE"), + slot: 369631527, + uuid: [115, 56, 108, 117, 74, 76, 90, 101], + take_profit_order_params: None, + stop_loss_order_params: None, + }; + assert_eq!(signed_msg, expected); + } else { + assert!(false, "unexpected variant"); + } + } }