From 9e08dbeef600f94ef3b7d5e3ec8c2a4c2344a07d Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Mon, 21 Oct 2024 13:14:10 +0800 Subject: [PATCH] Trunk/swift (#61) * Generate swift IDL types (add Signature type for [u8;64]) Make get_market_accounts_with_fallback pub Add to/from_str for MarketType * add sign_message to wallet * bump drift-ffi-sys * use better mainnet endpoint --------- Co-authored-by: Nour Alharithi --- .github/workflows/build.yml | 1 + Cargo.lock | 2 +- Cargo.toml | 2 +- crates/drift-idl-gen/Cargo.toml | 2 +- crates/drift-idl-gen/src/lib.rs | 25 ++++- crates/src/account_map.rs | 9 +- crates/src/drift_idl.rs | 178 ++++++++++++++++++++++++++++-- crates/src/lib.rs | 9 +- crates/src/marketmap.rs | 2 +- crates/src/types.rs | 35 +++++- res/drift.json | 188 +++++++++++++++++++++++++++++++- tests/integration.rs | 2 +- 12 files changed, 430 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7472b39..9efb74f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,5 +58,6 @@ jobs: env: RUST_LOG: info TEST_DEVNET_RPC_ENDPOINT: ${{ secrets.DEVNET_RPC_ENDPOINT }} + TEST_MAINNET_RPC_ENDPOINT: ${{ secrets.MAINNET_RPC_ENDPOINT }} TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }} CARGO_DRIFT_FFI_PATH: "/usr/lib" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0723ef4..ae26ee1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1465,7 +1465,7 @@ dependencies = [ [[package]] name = "drift-idl-gen" -version = "0.1.0" +version = "0.1.1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5e3b3d2..edacafc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,4 +49,4 @@ hex = "0.4" hex-literal = "0.4" [build-dependencies] -drift-idl-gen = { version = "0.1.0", path = "crates/drift-idl-gen"} \ No newline at end of file +drift-idl-gen = { version = "0.1.1", path = "crates/drift-idl-gen"} \ No newline at end of file diff --git a/crates/drift-idl-gen/Cargo.toml b/crates/drift-idl-gen/Cargo.toml index 6bb5574..c6b8981 100644 --- a/crates/drift-idl-gen/Cargo.toml +++ b/crates/drift-idl-gen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift-idl-gen" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "Apache-2.0" repository = "https://github.com/drift-labs/drift-rs" diff --git a/crates/drift-idl-gen/src/lib.rs b/crates/drift-idl-gen/src/lib.rs index e2a1cc4..4d39251 100644 --- a/crates/drift-idl-gen/src/lib.rs +++ b/crates/drift-idl-gen/src/lib.rs @@ -69,7 +69,16 @@ impl ArgType { } } ArgType::Defined { defined } => defined.clone(), - ArgType::Array { array: (t, len) } => format!("[{}; {}]", t.to_rust_type(), len), + ArgType::Array { array: (t, len) } => { + let rust_type = t.to_rust_type(); + // this is a common signature representation + if *len == 64_usize && rust_type == "u8" { + // [u8; 64] does not have a Default impl + "Signature".into() + } else { + format!("[{}; {}]", t.to_rust_type(), len) + } + } ArgType::Option { option } => format!("Option<{}>", option.to_rust_type()), ArgType::Vec { vec } => format!("Vec<{}>", vec.to_rust_type()), } @@ -588,6 +597,20 @@ fn generate_idl_types(idl: &Idl) -> String { } } + #[repr(transparent)] + #[derive(AnchorDeserialize, AnchorSerialize, Copy, Clone, PartialEq, Debug)] + pub struct Signature(pub [u8; 64]); + + impl Default for Signature { + fn default() -> Self { + Self([0_u8; 64]) + } + } + + impl anchor_lang::Space for Signature { + const INIT_SPACE: usize = 8 * 64; + } + /// wrapper around fixed array types used for padding with `Default` implementation #[repr(transparent)] #[derive(AnchorDeserialize, AnchorSerialize, Copy, Clone, PartialEq)] diff --git a/crates/src/account_map.rs b/crates/src/account_map.rs index eb42bbb..8b87cc0 100644 --- a/crates/src/account_map.rs +++ b/crates/src/account_map.rs @@ -168,15 +168,14 @@ mod tests { use solana_sdk::pubkey; use super::*; - use crate::{accounts::User, constants::DEFAULT_PUBKEY, Wallet}; + use crate::{ + accounts::User, constants::DEFAULT_PUBKEY, utils::test_envs::mainnet_endpoint, Wallet, + }; #[tokio::test] async fn test_user_subscribe() { let _ = env_logger::try_init(); - let account_map = AccountMap::new( - "https://api.mainnet-beta.solana.com".into(), - CommitmentConfig::confirmed(), - ); + let account_map = AccountMap::new(mainnet_endpoint().into(), CommitmentConfig::confirmed()); let user_1 = Wallet::derive_user_account( &pubkey!("DxoRJ4f5XRMvXU9SGuM4ZziBFUxbhB3ubur5sVZEvue2"), 0, diff --git a/crates/src/drift_idl.rs b/crates/src/drift_idl.rs index e824aca..34fddd4 100644 --- a/crates/src/drift_idl.rs +++ b/crates/src/drift_idl.rs @@ -2,7 +2,6 @@ #![doc = r""] #![doc = r" Auto-generated IDL types, manual edits do not persist (see `crates/drift-idl-gen`)"] #![doc = r""] -use self::traits::ToAccountMetas; use anchor_lang::{ prelude::{ account, @@ -12,6 +11,8 @@ use anchor_lang::{ Discriminator, }; use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey}; + +use self::traits::ToAccountMetas; pub mod traits { use solana_sdk::instruction::AccountMeta; #[doc = r" This is distinct from the anchor version of the trait"] @@ -163,7 +164,7 @@ pub mod instructions { #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct PlaceAndTakePerpOrder { pub params: OrderParams, - pub maker_order_id: Option, + pub success_condition: Option, } #[automatically_derived] impl anchor_lang::Discriminator for PlaceAndTakePerpOrder { @@ -183,6 +184,18 @@ pub mod instructions { #[automatically_derived] impl anchor_lang::InstructionData for PlaceAndMakePerpOrder {} #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] + pub struct PlaceSwiftTakerOrder { + pub swift_message_bytes: Vec, + pub swift_order_params_message_bytes: Vec, + pub swift_message_signature: Signature, + } + #[automatically_derived] + impl anchor_lang::Discriminator for PlaceSwiftTakerOrder { + const DISCRIMINATOR: [u8; 8] = [50, 89, 120, 78, 254, 15, 104, 140]; + } + #[automatically_derived] + impl anchor_lang::InstructionData for PlaceSwiftTakerOrder {} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct PlaceSpotOrder { pub params: OrderParams, } @@ -1781,8 +1794,9 @@ pub mod instructions { impl anchor_lang::InstructionData for InitializePythPullOracle {} } pub mod types { - use super::*; use std::ops::Mul; + + use super::*; #[doc = r" backwards compatible u128 deserializing data from rust <=1.76.0 when u/i128 was 8-byte aligned"] #[doc = r" https://solana.stackexchange.com/questions/7720/using-u128-without-sacrificing-alignment-8"] #[derive( @@ -1835,6 +1849,17 @@ pub mod types { Self(value.to_le_bytes()) } } + #[repr(transparent)] + #[derive(AnchorDeserialize, AnchorSerialize, Copy, Clone, PartialEq, Debug)] + pub struct Signature(pub [u8; 64]); + impl Default for Signature { + fn default() -> Self { + Self([0_u8; 64]) + } + } + impl anchor_lang::Space for Signature { + const INIT_SPACE: usize = 8 * 64; + } #[doc = r" wrapper around fixed array types used for padding with `Default` implementation"] #[repr(transparent)] #[derive(AnchorDeserialize, AnchorSerialize, Copy, Clone, PartialEq)] @@ -2003,6 +2028,33 @@ pub mod types { #[derive( AnchorSerialize, AnchorDeserialize, InitSpace, Copy, Clone, Default, Debug, PartialEq, )] + pub struct SwiftServerMessage { + pub swift_order_signature: Signature, + pub slot: u64, + } + #[repr(C)] + #[derive( + AnchorSerialize, AnchorDeserialize, InitSpace, Copy, Clone, Default, Debug, PartialEq, + )] + pub struct SwiftOrderParamsMessage { + pub swift_order_params: OrderParams, + pub expected_order_id: i32, + pub sub_account_id: u16, + pub take_profit_order_params: Option, + pub stop_loss_order_params: Option, + } + #[repr(C)] + #[derive( + AnchorSerialize, AnchorDeserialize, InitSpace, Copy, Clone, Default, Debug, PartialEq, + )] + pub struct SwiftTriggerOrderParams { + pub trigger_price: u64, + pub base_asset_amount: u64, + } + #[repr(C)] + #[derive( + AnchorSerialize, AnchorDeserialize, InitSpace, Copy, Clone, Default, Debug, PartialEq, + )] pub struct ModifyOrderParams { pub direction: Option, pub base_asset_amount: Option, @@ -2558,6 +2610,14 @@ pub mod types { #[derive( AnchorSerialize, AnchorDeserialize, InitSpace, Copy, Clone, Default, Debug, PartialEq, )] + pub enum PlaceAndTakeOrderSuccessCondition { + #[default] + PartialFill, + FullFill, + } + #[derive( + AnchorSerialize, AnchorDeserialize, InitSpace, Copy, Clone, Default, Debug, PartialEq, + )] pub enum PerpOperation { #[default] UpdateFunding, @@ -4733,6 +4793,88 @@ pub mod accounts { } #[repr(C)] #[derive(Copy, Clone, Default, AnchorSerialize, AnchorDeserialize)] + pub struct PlaceSwiftTakerOrder { + pub state: Pubkey, + pub user: Pubkey, + pub user_stats: Pubkey, + pub authority: Pubkey, + pub ix_sysvar: Pubkey, + } + #[automatically_derived] + impl anchor_lang::Discriminator for PlaceSwiftTakerOrder { + const DISCRIMINATOR: [u8; 8] = [237, 23, 214, 85, 135, 68, 88, 236]; + } + #[automatically_derived] + unsafe impl anchor_lang::__private::bytemuck::Pod for PlaceSwiftTakerOrder {} + #[automatically_derived] + unsafe impl anchor_lang::__private::bytemuck::Zeroable for PlaceSwiftTakerOrder {} + #[automatically_derived] + impl anchor_lang::ZeroCopy for PlaceSwiftTakerOrder {} + #[automatically_derived] + impl anchor_lang::InstructionData for PlaceSwiftTakerOrder {} + #[automatically_derived] + impl ToAccountMetas for PlaceSwiftTakerOrder { + fn to_account_metas(&self) -> Vec { + vec![ + AccountMeta { + pubkey: self.state, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: self.user, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: self.user_stats, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: self.authority, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: self.ix_sysvar, + is_signer: false, + is_writable: false, + }, + ] + } + } + #[automatically_derived] + impl anchor_lang::AccountSerialize for PlaceSwiftTakerOrder { + fn try_serialize(&self, writer: &mut W) -> anchor_lang::Result<()> { + if writer.write_all(&Self::DISCRIMINATOR).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + if AnchorSerialize::serialize(self, writer).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + Ok(()) + } + } + #[automatically_derived] + impl anchor_lang::AccountDeserialize for PlaceSwiftTakerOrder { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + let given_disc = &buf[..8]; + if Self::DISCRIMINATOR != given_disc { + return Err(anchor_lang::error!( + anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + )); + } + Self::try_deserialize_unchecked(buf) + } + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let mut data: &[u8] = &buf[8..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into()) + } + } + #[repr(C)] + #[derive(Copy, Clone, Default, AnchorSerialize, AnchorDeserialize)] pub struct PlaceSpotOrder { pub state: Pubkey, pub user: Pubkey, @@ -6926,8 +7068,9 @@ pub mod accounts { #[repr(C)] #[derive(Copy, Clone, Default, AnchorSerialize, AnchorDeserialize)] pub struct SettleExpiredMarket { + pub admin: Pubkey, pub state: Pubkey, - pub authority: Pubkey, + pub perp_market: Pubkey, } #[automatically_derived] impl anchor_lang::Discriminator for SettleExpiredMarket { @@ -6945,15 +7088,20 @@ pub mod accounts { impl ToAccountMetas for SettleExpiredMarket { fn to_account_metas(&self) -> Vec { vec![ + AccountMeta { + pubkey: self.admin, + is_signer: true, + is_writable: false, + }, AccountMeta { pubkey: self.state, is_signer: false, is_writable: false, }, AccountMeta { - pubkey: self.authority, - is_signer: true, - is_writable: false, + pubkey: self.perp_market, + is_signer: false, + is_writable: true, }, ] } @@ -7432,6 +7580,7 @@ pub mod accounts { pub struct SetUserStatusToBeingLiquidated { pub state: Pubkey, pub user: Pubkey, + pub authority: Pubkey, } #[automatically_derived] impl anchor_lang::Discriminator for SetUserStatusToBeingLiquidated { @@ -7459,6 +7608,11 @@ pub mod accounts { is_signer: false, is_writable: true, }, + AccountMeta { + pubkey: self.authority, + is_signer: true, + is_writable: false, + }, ] } } @@ -16862,6 +17016,16 @@ pub mod errors { LiquidationOrderFailedToFill, #[msg("Invalid prediction market order")] InvalidPredictionMarketOrder, + #[msg("Ed25519 Ix must be before place and make swift order ix")] + InvalidVerificationIxIndex, + #[msg("Swift message verificaiton failed")] + SigVerificationFailed, + #[msg("Market index mismatched b/w taker and maker swift order params")] + MismatchedSwiftOrderParamsMarketIndex, + #[msg("Swift only available for market/oracle perp orders")] + InvalidSwiftOrderParam, + #[msg("Place and take order success condition failed")] + PlaceAndTakeOrderSuccessConditionFailed, } } pub mod events { diff --git a/crates/src/lib.rs b/crates/src/lib.rs index 4575d60..05cafd4 100644 --- a/crates/src/lib.rs +++ b/crates/src/lib.rs @@ -1343,6 +1343,7 @@ impl<'a> TransactionBuilder<'a> { maker_info: Option<(Pubkey, User)>, referrer: Option, fulfillment_type: Option, + success_condition: Option, ) -> Self { let mut user_accounts = vec![self.account_data.as_ref()]; if let Some((ref _maker_pubkey, ref maker)) = maker_info { @@ -1386,7 +1387,7 @@ impl<'a> TransactionBuilder<'a> { accounts, data: InstructionData::data(&drift_idl::instructions::PlaceAndTakePerpOrder { params: order, - maker_order_id: None, + success_condition, }), } } else { @@ -1616,6 +1617,12 @@ impl Wallet { VersionedTransaction::try_new(message, &[signer]).map_err(Into::into) } + /// Sign message with the wallet's signer + pub fn sign_message(&self, message: &[u8]) -> SdkResult { + let signer: &dyn Signer = self.signer.as_ref(); + Ok(signer.sign_message(message)) + } + /// Return the wallet authority address pub fn authority(&self) -> &Pubkey { &self.authority diff --git a/crates/src/marketmap.rs b/crates/src/marketmap.rs index 6570607..6abfd88 100644 --- a/crates/src/marketmap.rs +++ b/crates/src/marketmap.rs @@ -204,7 +204,7 @@ where /// getProgramAccounts, getMultipleAccounts, latstly multiple getAccountInfo /// /// Returns deserialized accounts and retreived slot -async fn get_market_accounts_with_fallback( +pub async fn get_market_accounts_with_fallback( rpc: &RpcClient, ) -> SdkResult<(Vec, Slot)> { let mut markets = Vec::::default(); diff --git a/crates/src/types.rs b/crates/src/types.rs index 4e02d8f..0abdb05 100644 --- a/crates/src/types.rs +++ b/crates/src/types.rs @@ -1,6 +1,7 @@ use std::{ cell::{BorrowError, BorrowMutError}, cmp::Ordering, + str::FromStr, }; use anchor_lang::AnchorDeserialize; @@ -484,8 +485,32 @@ impl ConfiguredMarkets { } } +impl ToString for MarketType { + fn to_string(&self) -> String { + match self { + MarketType::Perp => "perp".into(), + MarketType::Spot => "spot".into(), + } + } +} + +impl FromStr for MarketType { + type Err = (); + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("perp") { + Ok(Self::Perp) + } else if s.eq_ignore_ascii_case("spot") { + Ok(Self::Spot) + } else { + Err(()) + } + } +} + #[cfg(test)] mod tests { + use std::str::FromStr; + use solana_client::{ client_error::{ClientError, ClientErrorKind}, rpc_request::{RpcError, RpcRequest}, @@ -496,7 +521,15 @@ mod tests { }; use super::{RemainingAccount, SdkError}; - use crate::drift_idl::errors::ErrorCode; + use crate::{drift_idl::errors::ErrorCode, MarketType}; + + #[test] + fn market_type_str() { + assert_eq!(MarketType::from_str("PERP").unwrap(), MarketType::Perp,); + assert_eq!(MarketType::from_str("spot").unwrap(), MarketType::Spot,); + assert_eq!("perp", &MarketType::Perp.to_string(),); + assert_eq!("spot", &MarketType::Spot.to_string(),); + } #[test] fn extract_anchor_error() { diff --git a/res/drift.json b/res/drift.json index 9c8e4b5..81e2bdf 100644 --- a/res/drift.json +++ b/res/drift.json @@ -1,5 +1,5 @@ { - "version": "2.93.0", + "version": "2.95.0", "name": "drift", "instructions": [ { @@ -554,7 +554,7 @@ } }, { - "name": "makerOrderId", + "name": "successCondition", "type": { "option": "u32" } @@ -608,6 +608,60 @@ } ] }, + { + "name": "placeSwiftTakerOrder", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "ixSysvar", + "isMut": false, + "isSigner": false, + "docs": [ + "the supplied Sysvar could be anything else.", + "The Instruction Sysvar has not been implemented", + "in the Anchor framework yet, so this is the safe approach." + ] + } + ], + "args": [ + { + "name": "swiftMessageBytes", + "type": "bytes" + }, + { + "name": "swiftOrderParamsMessageBytes", + "type": "bytes" + }, + { + "name": "swiftMessageSignature", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + }, { "name": "placeSpotOrder", "accounts": [ @@ -1606,15 +1660,20 @@ { "name": "settleExpiredMarket", "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, { "name": "state", "isMut": false, "isSigner": false }, { - "name": "authority", - "isMut": false, - "isSigner": true + "name": "perpMarket", + "isMut": true, + "isSigner": false } ], "args": [ @@ -1893,6 +1952,11 @@ "name": "user", "isMut": true, "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true } ], "args": [] @@ -8040,6 +8104,81 @@ ] } }, + { + "name": "SwiftServerMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "swiftOrderSignature", + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "slot", + "type": "u64" + } + ] + } + }, + { + "name": "SwiftOrderParamsMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "swiftOrderParams", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "expectedOrderId", + "type": "i32" + }, + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "takeProfitOrderParams", + "type": { + "option": { + "defined": "SwiftTriggerOrderParams" + } + } + }, + { + "name": "stopLossOrderParams", + "type": { + "option": { + "defined": "SwiftTriggerOrderParams" + } + } + } + ] + } + }, + { + "name": "SwiftTriggerOrderParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "triggerPrice", + "type": "u64" + }, + { + "name": "baseAssetAmount", + "type": "u64" + } + ] + } + }, { "name": "ModifyOrderParams", "type": { @@ -10112,6 +10251,20 @@ ] } }, + { + "name": "PlaceAndTakeOrderSuccessCondition", + "type": { + "kind": "enum", + "variants": [ + { + "name": "PartialFill" + }, + { + "name": "FullFill" + } + ] + } + }, { "name": "PerpOperation", "type": { @@ -12948,6 +13101,31 @@ "code": 6284, "name": "InvalidPredictionMarketOrder", "msg": "Invalid prediction market order" + }, + { + "code": 6285, + "name": "InvalidVerificationIxIndex", + "msg": "Ed25519 Ix must be before place and make swift order ix" + }, + { + "code": 6286, + "name": "SigVerificationFailed", + "msg": "Swift message verificaiton failed" + }, + { + "code": 6287, + "name": "MismatchedSwiftOrderParamsMarketIndex", + "msg": "Market index mismatched b/w taker and maker swift order params" + }, + { + "code": 6288, + "name": "InvalidSwiftOrderParam", + "msg": "Swift only available for market/oracle perp orders" + }, + { + "code": 6289, + "name": "PlaceAndTakeOrderSuccessConditionFailed", + "msg": "Place and take order success condition failed" } ], "metadata": { diff --git a/tests/integration.rs b/tests/integration.rs index 88d11c6..b107118 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -101,7 +101,7 @@ async fn place_and_take() { .init_tx(&wallet.default_sub_account(), false) .await .unwrap() - .place_and_take(order, None, None, None) + .place_and_take(order, None, None, None, None) .build(); let result = client.sign_and_send(tx).await;