diff --git a/auction-server/api-types/src/opportunity.rs b/auction-server/api-types/src/opportunity.rs index c5814c9e..75b6c7bb 100644 --- a/auction-server/api-types/src/opportunity.rs +++ b/auction-server/api-types/src/opportunity.rs @@ -58,9 +58,8 @@ pub struct OpportunityBidResult { } #[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)] -#[serde(rename_all = "lowercase")] pub enum ProgramSvm { - Phantom, + Swap, Limo, } @@ -206,18 +205,18 @@ pub enum OpportunityCreateProgramParamsV1Svm { #[serde_as(as = "DisplayFromStr")] order_address: Pubkey, }, - /// Phantom program specific parameters for the opportunity. - #[serde(rename = "phantom")] - #[schema(title = "phantom")] - Phantom { + /// Swap program specific parameters for the opportunity. + #[serde(rename = "swap")] + #[schema(title = "swap")] + Swap { /// The user wallet address which requested the quote from the wallet. #[schema(example = "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", value_type = String)] #[serde_as(as = "DisplayFromStr")] user_wallet_address: Pubkey, - /// The maximum slippage percentage that the user is willing to accept. - #[schema(example = 0.5, value_type = f64)] - maximum_slippage_percentage: f64, + /// The maximum slippage in basis points that the user is willing to accept. + #[schema(example = 50, value_type = u16)] + maximum_slippage_bps: u16, }, } @@ -311,18 +310,18 @@ pub enum OpportunityParamsV1ProgramSvm { #[serde_as(as = "DisplayFromStr")] order_address: Pubkey, }, - /// Phantom program specific parameters for the opportunity. - #[serde(rename = "phantom")] - #[schema(title = "phantom")] - Phantom { + /// Swap program specific parameters for the opportunity. + #[serde(rename = "swap")] + #[schema(title = "swap")] + Swap { /// The user wallet address which requested the quote from the wallet. #[schema(example = "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", value_type = String)] #[serde_as(as = "DisplayFromStr")] user_wallet_address: Pubkey, - /// The maximum slippage percentage that the user is willing to accept. - #[schema(example = 0.5, value_type = f64)] - maximum_slippage_percentage: f64, + /// The maximum slippage in basis points that the user is willing to accept. + #[schema(example = 50, value_type = u16)] + maximum_slippage_bps: u16, /// The permission account to be permitted by the ER contract for the opportunity execution of the protocol. #[schema(example = "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", value_type = String)] @@ -334,11 +333,21 @@ pub enum OpportunityParamsV1ProgramSvm { #[serde_as(as = "DisplayFromStr")] router_account: Pubkey, - /// The token searcher will send. - sell_token: TokenAmountSvm, + /// Details about the tokens to be swapped. Either the input token amount or the output token amount must be specified. + #[schema(inline)] + tokens: QuoteTokens, + }, +} - /// The token searcher will receive. - buy_token: TokenAmountSvm, +#[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug, ToResponse)] +pub enum QuoteTokens { + InputTokenSpecified { + input_token: TokenAmountSvm, + output_token: Pubkey, + }, + OutputTokenSpecified { + input_token: Pubkey, + output_token: TokenAmountSvm, }, } @@ -466,40 +475,53 @@ pub struct OpportunityBidEvm { pub signature: Signature, } -/// Parameters needed to create a new opportunity from the Phantom wallet. +/// Parameters needed to create a new opportunity from the swap request. /// Auction server will extract the output token price for the auction. #[serde_as] #[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)] -pub struct QuoteCreatePhantomV1Svm { +pub struct QuoteCreateV1SvmParams { /// The user wallet address which requested the quote from the wallet. #[schema(example = "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", value_type = String)] #[serde_as(as = "DisplayFromStr")] - pub user_wallet_address: Pubkey, + pub user_wallet_address: Pubkey, /// The token mint address of the input token. #[schema(example = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", value_type = String)] #[serde_as(as = "DisplayFromStr")] - pub input_token_mint: Pubkey, + pub input_token_mint: Pubkey, /// The token mint address of the output token. #[schema(example = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", value_type = String)] #[serde_as(as = "DisplayFromStr")] - pub output_token_mint: Pubkey, - /// The input token amount that the user wants to swap. - #[schema(example = 100)] - pub input_token_amount: u64, - /// The maximum slippage percentage that the user is willing to accept. - #[schema(example = 0.5)] - pub maximum_slippage_percentage: f64, + pub output_token_mint: Pubkey, + /// The token amount that the user wants to swap out of/into. + #[schema(inline)] + pub specified_token_amount: SpecifiedTokenAmount, + /// The maximum slippage in basis points that the user is willing to accept. + #[schema(example = 50)] + pub maximum_slippage_bps: u16, + /// The router account to send referral fees to. + #[schema(example = "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", value_type = String)] + #[serde_as(as = "DisplayFromStr")] + pub router: Pubkey, /// The chain id for creating the quote. #[schema(example = "solana", value_type = String)] - pub chain_id: ChainId, + pub chain_id: ChainId, } #[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)] -#[serde(tag = "program")] -pub enum QuoteCreateV1Svm { - #[serde(rename = "phantom")] - #[schema(title = "phantom")] - Phantom(QuoteCreatePhantomV1Svm), +#[serde(tag = "side")] +pub enum SpecifiedTokenAmount { + #[serde(rename = "input")] + #[schema(title = "input")] + InputToken { + #[schema(example = 100)] + amount: u64, + }, + #[serde(rename = "output")] + #[schema(title = "output")] + OutputToken { + #[schema(example = 50)] + amount: u64, + }, } #[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)] @@ -507,7 +529,7 @@ pub enum QuoteCreateV1Svm { pub enum QuoteCreateSvm { #[serde(rename = "v1")] #[schema(title = "v1")] - V1(QuoteCreateV1Svm), + V1(QuoteCreateV1SvmParams), } #[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)] @@ -522,20 +544,20 @@ pub struct QuoteV1Svm { /// The signed transaction for the quote to be executed on chain which is valid until the expiration time. #[schema(example = "SGVsbG8sIFdvcmxkIQ==", value_type = String)] #[serde(with = "crate::serde::transaction_svm")] - pub transaction: VersionedTransaction, + pub transaction: VersionedTransaction, /// The expiration time of the quote (in seconds since the Unix epoch). #[schema(example = 1_700_000_000_000_000i64, value_type = i64)] - pub expiration_time: i64, + pub expiration_time: i64, /// The input token amount that the user wants to swap. - pub input_token: TokenAmountSvm, + pub input_token: TokenAmountSvm, /// The output token amount that the user will receive. - pub output_token: TokenAmountSvm, - /// The maximum slippage percentage that the user is willing to accept. - #[schema(example = 0.5)] - pub maximum_slippage_percentage: f64, + pub output_token: TokenAmountSvm, + /// The maximum slippage in basis points that the user is willing to accept. + #[schema(example = 50)] + pub maximum_slippage_bps: u16, /// The chain id for the quote. #[schema(example = "solana", value_type = String)] - pub chain_id: ChainId, + pub chain_id: ChainId, } #[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)] @@ -558,7 +580,7 @@ impl OpportunityCreateSvm { match self { OpportunityCreateSvm::V1(params) => match ¶ms.program_params { OpportunityCreateProgramParamsV1Svm::Limo { .. } => ProgramSvm::Limo, - OpportunityCreateProgramParamsV1Svm::Phantom { .. } => ProgramSvm::Phantom, + OpportunityCreateProgramParamsV1Svm::Swap { .. } => ProgramSvm::Swap, }, } } diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index ce2a1423..7c82eb45 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -340,6 +340,7 @@ pub async fn start_api(run_options: RunOptions, store: Arc) -> Result< api_types::bid::Bids, api_types::SvmChainUpdate, + api_types::opportunity::SpecifiedTokenAmount, api_types::opportunity::OpportunityBidEvm, api_types::opportunity::OpportunityBidResult, api_types::opportunity::OpportunityMode, @@ -360,10 +361,10 @@ pub async fn start_api(run_options: RunOptions, store: Arc) -> Result< api_types::opportunity::OpportunityParamsV1Evm, api_types::opportunity::QuoteCreate, api_types::opportunity::QuoteCreateSvm, - api_types::opportunity::QuoteCreateV1Svm, - api_types::opportunity::QuoteCreatePhantomV1Svm, + api_types::opportunity::QuoteCreateV1SvmParams, api_types::opportunity::Quote, api_types::opportunity::QuoteSvm, + api_types::opportunity::QuoteTokens, api_types::opportunity::QuoteV1Svm, api_types::opportunity::OpportunityDelete, api_types::opportunity::OpportunityDeleteSvm, diff --git a/auction-server/src/auction/entities/auction.rs b/auction-server/src/auction/entities/auction.rs index 51ebbc56..ccaa6786 100644 --- a/auction-server/src/auction/entities/auction.rs +++ b/auction-server/src/auction/entities/auction.rs @@ -34,6 +34,7 @@ pub struct Auction { pub bids: Vec>, } +#[derive(PartialEq)] pub enum SubmitType { ByServer, ByOther, diff --git a/auction-server/src/auction/service/verification.rs b/auction-server/src/auction/service/verification.rs index f5e17cbd..cac80cd4 100644 --- a/auction-server/src/auction/service/verification.rs +++ b/auction-server/src/auction/service/verification.rs @@ -13,6 +13,7 @@ use { entities::{ self, BidChainData, + SubmitType, }, service::get_live_bids::GetLiveBidsInput, }, @@ -269,6 +270,7 @@ struct BidDataSvm { router: Pubkey, permission_account: Pubkey, deadline: OffsetDateTime, + submit_type: SubmitType, } const BID_MINIMUM_LIFE_TIME_SVM_SERVER: Duration = Duration::from_secs(5); @@ -429,12 +431,61 @@ impl Service { } } + // // TODO*: implement this once Swap instruction is implemented + // pub fn extract_swap_data( + // instruction: &CompiledInstruction, + // ) -> Result { + // let discriminator = express_relay_svm::instruction::Swap::discriminator(); + // express_relay_svm::SwapArgs::try_from_slice( + // &instruction.data.as_slice()[discriminator.len()..], + // ) + // .map_err(|e| { + // RestError::BadParameters(format!("Invalid submit_bid instruction data: {}", e)) + // }) + // } + + // TODO*: implement this once Swap instruction is implemented + // Checks that the transaction includes exactly one swap instruction to the Express Relay on-chain program + pub fn verify_swap_instruction( + &self, + transaction: VersionedTransaction, + ) -> Result { + // let swap_instructions: Vec = transaction + // .message + // .instructions() + // .iter() + // .filter(|instruction| { + // let program_id = instruction.program_id(transaction.message.static_account_keys()); + // if *program_id != self.config.chain_config.express_relay.program_id { + // return false; + // } + + // instruction + // .data + // .starts_with(&express_relay_svm::instruction::Swap::discriminator()) + // }) + // .cloned() + // .collect(); + + // match swap_instructions.len() { + // 1 => Ok(swap_instructions[0].clone()), + // _ => Err(RestError::BadParameters( + // "Bid has to include exactly one swap instruction to Express Relay program" + // .to_string(), + // )), + // } + + Err(RestError::BadParameters( + "Swap instruction not implemented".to_string(), + )) + } + async fn check_deadline( &self, - permission_key: &PermissionKeySvm, + submit_type: &SubmitType, deadline: OffsetDateTime, ) -> Result<(), RestError> { - let minimum_bid_life_time = match self.get_submission_state(permission_key).await { + let minimum_bid_life_time = match submit_type { entities::SubmitType::ByServer => Some(BID_MINIMUM_LIFE_TIME_SVM_SERVER), entities::SubmitType::ByOther => Some(BID_MINIMUM_LIFE_TIME_SVM_OTHER), entities::SubmitType::Invalid => None, @@ -463,42 +514,95 @@ impl Service { &self, transaction: VersionedTransaction, ) -> Result { - let submit_bid_instruction = self.verify_submit_bid_instruction(transaction.clone())?; - let submit_bid_data = Self::extract_submit_bid_data(&submit_bid_instruction)?; - - let permission_account = self - .extract_account( - &transaction, - &submit_bid_instruction, - self.config - .chain_config - .express_relay - .permission_account_position, - ) - .await?; - let router = self - .extract_account( - &transaction, - &submit_bid_instruction, - self.config - .chain_config - .express_relay - .router_account_position, - ) - .await?; - Ok(BidDataSvm { - amount: submit_bid_data.bid_amount, - permission_account, - router, - deadline: OffsetDateTime::from_unix_timestamp(submit_bid_data.deadline).map_err( - |e| { - RestError::BadParameters(format!( - "Invalid deadline: {:?} {:?}", - submit_bid_data.deadline, e - )) - }, - )?, - }) + let submit_bid_instruction_result = self.verify_submit_bid_instruction(transaction.clone()); + let swap_instruction_result = self.verify_swap_instruction(transaction.clone()); + + match ( + submit_bid_instruction_result.clone(), + swap_instruction_result.clone(), + ) { + (Ok(_), Err(_)) => { + let submit_bid_instruction = submit_bid_instruction_result?; + let submit_bid_data = Self::extract_submit_bid_data(&submit_bid_instruction)?; + + let permission_account = self + .extract_account( + &transaction, + &submit_bid_instruction, + self.config + .chain_config + .express_relay + .permission_account_position, + ) + .await?; + let router = self + .extract_account( + &transaction, + &submit_bid_instruction, + self.config + .chain_config + .express_relay + .router_account_position, + ) + .await?; + if router == self.config.chain_config.wallet_program_router_account { + return Err(RestError::BadParameters( + "Using swap router account is not allowed for submit_bid instruction" + .to_string(), + )); + } + Ok(BidDataSvm { + amount: submit_bid_data.bid_amount, + permission_account, + router, + deadline: OffsetDateTime::from_unix_timestamp(submit_bid_data.deadline) + .map_err(|e| { + RestError::BadParameters(format!( + "Invalid deadline: {:?} {:?}", + submit_bid_data.deadline, e + )) + })?, + submit_type: SubmitType::ByServer, + }) + } + (Err(_), Ok(_)) => { + let swap_instruction = swap_instruction_result?; + // TODO*: implement this once Swap instruction is implemented + Err(RestError::BadParameters( + "Swap instruction not implemented".to_string(), + )) + // TODO*: calculate the permission key here from all the relevant seeds + // let swap_data = Self::extract_swap_data(&swap_instruction)?; + + // let router = self + // .extract_account( + // &transaction, + // &submit_bid_instruction, + // self.config + // .chain_config + // .express_relay + // .router_account_position, + // ) + // .await?; + // Ok(BidDataSvm { + // amount: swap_data.bid_amount, + // permission_account: swap_data.permission_account, + // router: swap_data.router, + // deadline: OffsetDateTime::from_unix_timestamp(swap_data.deadline).map_err( + // |e| { + // RestError::BadParameters(format!( + // "Invalid deadline: {:?} {:?}", + // swap_data.deadline, e + // )) + // }, + // )?, + // submit_type: SubmitType::ByServer, + // }) + } + _ => Err(RestError::BadParameters( + "Either submit_bid or swap must be present, but not both".to_string(), + )), + } } fn all_signatures_exists( @@ -538,12 +642,13 @@ impl Service { &self, bid: &entities::BidCreate, chain_data: &entities::BidChainDataSvm, + submit_type: &entities::SubmitType, ) -> Result<(), RestError> { let message_bytes = chain_data.transaction.message.serialize(); let signatures = chain_data.transaction.signatures.clone(); let accounts = chain_data.transaction.message.static_account_keys(); let permission_key = chain_data.get_permission_key(); - match self.get_submission_state(&permission_key).await { + match submit_type { entities::SubmitType::Invalid => { // TODO Look at the todo comment in get_quote.rs file in opportunity module Err(RestError::BadParameters(format!( @@ -674,10 +779,10 @@ impl Verification for Service { }; let permission_key = bid_chain_data.get_permission_key(); tracing::Span::current().record("permission_key", bid_data.permission_account.to_string()); - self.check_deadline(&permission_key, bid_data.deadline) + self.check_deadline(&bid_data.submit_type, bid_data.deadline) + .await?; + self.verify_signatures(&bid, &bid_chain_data, &bid_data.submit_type) .await?; - self.verify_signatures(&bid, &bid_chain_data).await?; - // TODO we should verify that the wallet bids also include another instruction to the swap program with the appropriate accounts and fields self.simulate_bid(&bid).await?; // Check if the bid is not duplicate diff --git a/auction-server/src/config.rs b/auction-server/src/config.rs index cde529b3..c4a504aa 100644 --- a/auction-server/src/config.rs +++ b/auction-server/src/config.rs @@ -164,7 +164,7 @@ pub struct ConfigSvm { /// Timeout for RPC requests in seconds. #[serde(default = "default_rpc_timeout_svm")] pub rpc_timeout: u64, - /// The router account for Phantom. + /// The router account for swap program. // TODO: we should work to remove this and fully identify swaps by ix type instead of router account #[serde_as(as = "DisplayFromStr")] pub wallet_program_router_account: Pubkey, #[serde(default)] diff --git a/auction-server/src/opportunity/api.rs b/auction-server/src/opportunity/api.rs index 6ef3c3a8..a6cfbe3a 100644 --- a/auction-server/src/opportunity/api.rs +++ b/auction-server/src/opportunity/api.rs @@ -58,7 +58,7 @@ fn get_program(auth: &Auth) -> Result { match profile.name.as_str() { "limo" => Ok(ProgramSvm::Limo), - "phantom" => Ok(ProgramSvm::Phantom), + "Kamino Market" => Ok(ProgramSvm::Swap), _ => Err(RestError::Forbidden), } } @@ -211,7 +211,7 @@ pub async fn post_quote( State(store): State>, Json(params): Json, ) -> Result, RestError> { - if get_program(&auth)? != ProgramSvm::Phantom { + if get_program(&auth)? != ProgramSvm::Swap { return Err(RestError::Forbidden); } diff --git a/auction-server/src/opportunity/entities/opportunity_svm.rs b/auction-server/src/opportunity/entities/opportunity_svm.rs index baf9ffdc..5f61da40 100644 --- a/auction-server/src/opportunity/entities/opportunity_svm.rs +++ b/auction-server/src/opportunity/entities/opportunity_svm.rs @@ -33,14 +33,14 @@ pub struct OpportunitySvmProgramLimo { #[derive(Debug, Clone, PartialEq)] pub struct OpportunitySvmProgramWallet { - pub user_wallet_address: Pubkey, - pub maximum_slippage_percentage: f64, + pub user_wallet_address: Pubkey, + pub maximum_slippage_bps: u16, } #[derive(Debug, Clone, PartialEq)] pub enum OpportunitySvmProgram { Limo(OpportunitySvmProgramLimo), - Phantom(OpportunitySvmProgramWallet), + Swap(OpportunitySvmProgramWallet), } #[derive(Debug, Clone, PartialEq)] @@ -93,11 +93,11 @@ impl Opportunity for OpportunitySvm { }, ) } - OpportunitySvmProgram::Phantom(program) => { - repository::OpportunityMetadataSvmProgram::Phantom( + OpportunitySvmProgram::Swap(program) => { + repository::OpportunityMetadataSvmProgram::Swap( repository::OpportunityMetadataSvmProgramWallet { - user_wallet_address: program.user_wallet_address, - maximum_slippage_percentage: program.maximum_slippage_percentage, + user_wallet_address: program.user_wallet_address, + maximum_slippage_bps: program.maximum_slippage_bps, }, ) } @@ -166,33 +166,46 @@ impl From for api::Opportunity { impl From for api::OpportunitySvm { fn from(val: OpportunitySvm) -> Self { let program = match val.program.clone() { - OpportunitySvmProgram::Limo(prgoram) => api::OpportunityParamsV1ProgramSvm::Limo { - order: prgoram.order, - order_address: prgoram.order_address, + OpportunitySvmProgram::Limo(program) => api::OpportunityParamsV1ProgramSvm::Limo { + order: program.order, + order_address: program.order_address, }, - OpportunitySvmProgram::Phantom(program) => { - api::OpportunityParamsV1ProgramSvm::Phantom { - user_wallet_address: program.user_wallet_address, - maximum_slippage_percentage: program.maximum_slippage_percentage, - permission_account: val.permission_account, - router_account: val.router, + OpportunitySvmProgram::Swap(program) => { + let buy_token = val + .buy_tokens + .first() + .ok_or(anyhow::anyhow!( + "Failed to get buy token from opportunity svm" + )) + .expect("Failed to get buy token from opportunity svm"); + let sell_token = val + .sell_tokens + .first() + .ok_or(anyhow::anyhow!( + "Failed to get sell token from opportunity svm" + )) + .expect("Failed to get sell token from opportunity svm"); + let tokens = if buy_token.amount == 0 { + api::QuoteTokens::OutputTokenSpecified { + input_token: buy_token.token, + output_token: sell_token.clone().into(), + } + } else { + if sell_token.amount != 0 { + tracing::error!(opportunity = ?val, "Both token amounts are specified for swap opportunity"); + } + api::QuoteTokens::InputTokenSpecified { + input_token: buy_token.clone().into(), + output_token: sell_token.token, + } + }; + api::OpportunityParamsV1ProgramSvm::Swap { + user_wallet_address: program.user_wallet_address, + maximum_slippage_bps: program.maximum_slippage_bps, + permission_account: val.permission_account, + router_account: val.router, // TODO can we make it type safe? - sell_token: val - .sell_tokens - .first() - .map(|t| t.clone().into()) - .ok_or(anyhow::anyhow!( - "Failed to get sell token from opportunity svm" - )) - .expect("Failed to get sell token from opportunity svm"), - buy_token: val - .sell_tokens - .first() - .map(|t| t.clone().into()) - .ok_or(anyhow::anyhow!( - "Failed to get sell token from opportunity svm" - )) - .expect("Failed to get sell token from opportunity svm"), + tokens, } } }; @@ -237,10 +250,10 @@ impl TryFrom> for Op order_address: program.order_address, }) } - repository::OpportunityMetadataSvmProgram::Phantom(program) => { - OpportunitySvmProgram::Phantom(OpportunitySvmProgramWallet { - user_wallet_address: program.user_wallet_address, - maximum_slippage_percentage: program.maximum_slippage_percentage, + repository::OpportunityMetadataSvmProgram::Swap(program) => { + OpportunitySvmProgram::Swap(OpportunitySvmProgramWallet { + user_wallet_address: program.user_wallet_address, + maximum_slippage_bps: program.maximum_slippage_bps, }) } }; @@ -273,12 +286,12 @@ impl From for OpportunityCreateSvm { order, order_address, }), - api::OpportunityCreateProgramParamsV1Svm::Phantom { + api::OpportunityCreateProgramParamsV1Svm::Swap { user_wallet_address, - maximum_slippage_percentage, - } => OpportunitySvmProgram::Phantom(OpportunitySvmProgramWallet { + maximum_slippage_bps, + } => OpportunitySvmProgram::Swap(OpportunitySvmProgramWallet { user_wallet_address, - maximum_slippage_percentage, + maximum_slippage_bps, }), }; @@ -322,7 +335,7 @@ impl From for OpportunityCreateSvm { impl OpportunitySvm { pub fn get_missing_signers(&self) -> Vec { match self.program.clone() { - OpportunitySvmProgram::Phantom(data) => vec![data.user_wallet_address], + OpportunitySvmProgram::Swap(data) => vec![data.user_wallet_address], OpportunitySvmProgram::Limo(_) => vec![], } } @@ -336,7 +349,7 @@ impl From for api::ProgramSvm { fn from(val: OpportunitySvmProgram) -> Self { match val { OpportunitySvmProgram::Limo(_) => api::ProgramSvm::Limo, - OpportunitySvmProgram::Phantom(_) => api::ProgramSvm::Phantom, + OpportunitySvmProgram::Swap(_) => api::ProgramSvm::Swap, } } } diff --git a/auction-server/src/opportunity/entities/quote.rs b/auction-server/src/opportunity/entities/quote.rs index 7d4ffe11..27a1d771 100644 --- a/auction-server/src/opportunity/entities/quote.rs +++ b/auction-server/src/opportunity/entities/quote.rs @@ -10,38 +10,65 @@ use { #[derive(Debug, Clone, PartialEq)] pub struct Quote { - pub transaction: VersionedTransaction, + pub transaction: VersionedTransaction, // The expiration time of the quote (in seconds since the Unix epoch) - pub expiration_time: i64, - pub input_token: TokenAmountSvm, - pub output_token: TokenAmountSvm, - pub maximum_slippage_percentage: f64, - pub chain_id: ChainId, + pub expiration_time: i64, + pub input_token: TokenAmountSvm, + pub output_token: TokenAmountSvm, + pub maximum_slippage_bps: u16, + pub chain_id: ChainId, } #[derive(Debug, Clone, PartialEq)] pub struct QuoteCreate { - pub user_wallet_address: Pubkey, - pub input_token: TokenAmountSvm, - pub output_mint_token: Pubkey, - pub maximum_slippage_percentage: f64, - pub chain_id: ChainId, + pub user_wallet_address: Pubkey, + pub tokens: QuoteTokens, + pub maximum_slippage_bps: u16, + pub router: Pubkey, + pub chain_id: ChainId, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum QuoteTokens { + InputTokenSpecified { + input_token: TokenAmountSvm, + output_token: Pubkey, + }, + OutputTokenSpecified { + input_token: Pubkey, + output_token: TokenAmountSvm, + }, } impl From for QuoteCreate { fn from(quote_create: api::QuoteCreate) -> Self { - let api::QuoteCreate::Svm(api::QuoteCreateSvm::V1(api::QuoteCreateV1Svm::Phantom(params))) = - quote_create; + let api::QuoteCreate::Svm(api::QuoteCreateSvm::V1(params)) = quote_create; - Self { - user_wallet_address: params.user_wallet_address, - input_token: TokenAmountSvm { - token: params.input_token_mint, - amount: params.input_token_amount, + let tokens = match params.specified_token_amount { + api::SpecifiedTokenAmount::InputToken { amount } => QuoteTokens::InputTokenSpecified { + input_token: TokenAmountSvm { + token: params.input_token_mint, + amount: amount, + }, + output_token: params.output_token_mint, }, - output_mint_token: params.output_token_mint, - maximum_slippage_percentage: params.maximum_slippage_percentage, - chain_id: params.chain_id, + api::SpecifiedTokenAmount::OutputToken { amount } => { + QuoteTokens::OutputTokenSpecified { + input_token: params.input_token_mint, + output_token: TokenAmountSvm { + token: params.output_token_mint, + amount: amount, + }, + } + } + }; + + Self { + user_wallet_address: params.user_wallet_address, + tokens, + maximum_slippage_bps: params.maximum_slippage_bps, + router: params.router, + chain_id: params.chain_id, } } } @@ -49,12 +76,12 @@ impl From for QuoteCreate { impl From for api::Quote { fn from(quote: Quote) -> Self { api::Quote::Svm(api::QuoteSvm::V1(api::QuoteV1Svm { - transaction: quote.transaction, - expiration_time: quote.expiration_time, - input_token: quote.input_token.into(), - output_token: quote.output_token.into(), - maximum_slippage_percentage: quote.maximum_slippage_percentage, - chain_id: quote.chain_id, + transaction: quote.transaction, + expiration_time: quote.expiration_time, + input_token: quote.input_token.into(), + output_token: quote.output_token.into(), + maximum_slippage_bps: quote.maximum_slippage_bps, + chain_id: quote.chain_id, })) } } diff --git a/auction-server/src/opportunity/repository/models.rs b/auction-server/src/opportunity/repository/models.rs index aabcd116..f7ae5dbb 100644 --- a/auction-server/src/opportunity/repository/models.rs +++ b/auction-server/src/opportunity/repository/models.rs @@ -59,15 +59,15 @@ pub struct OpportunityMetadataSvmProgramLimo { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct OpportunityMetadataSvmProgramWallet { #[serde_as(as = "DisplayFromStr")] - pub user_wallet_address: Pubkey, - pub maximum_slippage_percentage: f64, + pub user_wallet_address: Pubkey, + pub maximum_slippage_bps: u16, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "program", rename_all = "lowercase")] pub enum OpportunityMetadataSvmProgram { Limo(OpportunityMetadataSvmProgramLimo), - Phantom(OpportunityMetadataSvmProgramWallet), + Swap(OpportunityMetadataSvmProgramWallet), } #[serde_as] diff --git a/auction-server/src/opportunity/service/estimate_price.rs b/auction-server/src/opportunity/service/estimate_price.rs deleted file mode 100644 index 81f342f4..00000000 --- a/auction-server/src/opportunity/service/estimate_price.rs +++ /dev/null @@ -1,23 +0,0 @@ -use { - super::{ - ChainTypeSvm, - Service, - }, - crate::{ - api::RestError, - opportunity::entities, - }, -}; - -pub struct EstimatePriceInput { - #[allow(dead_code)] - pub quote_create: entities::QuoteCreate, -} - -impl Service { - #[tracing::instrument(skip_all)] - pub async fn estimate_price(&self, _input: EstimatePriceInput) -> Result { - // TODO implement - return Ok(0); - } -} diff --git a/auction-server/src/opportunity/service/get_quote.rs b/auction-server/src/opportunity/service/get_quote.rs index 6f7e2f79..3f9d88b8 100644 --- a/auction-server/src/opportunity/service/get_quote.rs +++ b/auction-server/src/opportunity/service/get_quote.rs @@ -8,6 +8,7 @@ use { auction::{ entities::{ Auction, + BidStatus, BidStatusAuction, }, service::{ @@ -19,21 +20,14 @@ use { Service as AuctionService, }, }, - kernel::entities::{ - PermissionKeySvm, - Svm, - }, + kernel::entities::PermissionKeySvm, opportunity::{ entities, - service::{ - add_opportunity::AddOpportunityInput, - estimate_price::EstimatePriceInput, - }, + service::add_opportunity::AddOpportunityInput, }, }, axum_prometheus::metrics, futures::future::join_all, - rand::Rng, solana_sdk::{ clock::Slot, pubkey::Pubkey, @@ -50,15 +44,69 @@ pub struct GetQuoteInput { pub quote_create: entities::QuoteCreate, } +pub fn get_quote_permission_key( + tokens: &entities::QuoteTokens, + user_wallet_address: &Pubkey, +) -> Pubkey { + // get pda seeded by user_wallet_address, mints, and token amount + let input_token_amount: [u8; 8]; + let output_token_amount: [u8; 8]; + let seeds = match tokens { + entities::QuoteTokens::InputTokenSpecified { + input_token, + output_token, + } => { + let input_token_mint = input_token.token.as_ref(); + let output_token_mint = output_token.as_ref(); + input_token_amount = input_token.amount.to_le_bytes(); + [ + user_wallet_address.as_ref(), + input_token_mint, + input_token_amount.as_ref(), + output_token_mint, + ] + } + entities::QuoteTokens::OutputTokenSpecified { + input_token, + output_token, + } => { + let input_token_mint = input_token.as_ref(); + let output_token_mint = output_token.token.as_ref(); + output_token_amount = output_token.amount.to_le_bytes(); + [ + user_wallet_address.as_ref(), + input_token_mint, + output_token_mint, + output_token_amount.as_ref(), + ] + } + }; + // since this permission key will not be used on-chain, we don't need to use the express relay program_id. + // we can use a distinctive bytes object for the program_id + Pubkey::find_program_address(&seeds, &Pubkey::default()).0 +} + impl Service { async fn get_opportunity_create_for_quote( &self, quote_create: entities::QuoteCreate, - output_amount: u64, ) -> Result { - let chain_config = self.get_config("e_create.chain_id)?; - let router = chain_config.wallet_program_router_account; - let permission_account = Pubkey::new_from_array(rand::thread_rng().gen()); + let router = quote_create.router; + let permission_account = + get_quote_permission_key("e_create.tokens, "e_create.user_wallet_address); + + // TODO*: we should fix the Opportunity struct (or create a new format) to more clearly distinguish Swap opps from traditional opps + // currently, we are using the same struct and just setting the unspecified token amount to 0 + let (input_mint, input_amount, output_mint, output_amount) = match quote_create.tokens { + entities::QuoteTokens::InputTokenSpecified { + input_token, + output_token, + } => (input_token.token, input_token.amount, output_token, 0), + entities::QuoteTokens::OutputTokenSpecified { + input_token, + output_token, + } => (input_token, 0, output_token.token, output_token.amount), + }; let core_fields = entities::OpportunityCoreFieldsCreate { permission_key: entities::OpportunitySvm::get_permission_key( @@ -66,9 +114,12 @@ impl Service { permission_account, ), chain_id: quote_create.chain_id, - sell_tokens: vec![quote_create.input_token], + sell_tokens: vec![entities::TokenAmountSvm { + token: input_mint, + amount: input_amount, + }], buy_tokens: vec![entities::TokenAmountSvm { - token: quote_create.output_mint_token, + token: output_mint, amount: output_amount, }], }; @@ -77,12 +128,10 @@ impl Service { core_fields, router, permission_account, - program: entities::OpportunitySvmProgram::Phantom( - entities::OpportunitySvmProgramWallet { - user_wallet_address: quote_create.user_wallet_address, - maximum_slippage_percentage: quote_create.maximum_slippage_percentage, - }, - ), + program: entities::OpportunitySvmProgram::Swap(entities::OpportunitySvmProgramWallet { + user_wallet_address: quote_create.user_wallet_address, + maximum_slippage_bps: quote_create.maximum_slippage_bps, + }), // TODO extract latest slot slot: Slot::default(), }) @@ -93,22 +142,24 @@ impl Service { let config = self.get_config(&input.quote_create.chain_id)?; let auction_service = config.get_auction_service().await; - // TODO Check for the input amount tracing::info!(quote_create = ?input.quote_create, "Received request to get quote"); - let output_amount = self - .estimate_price(EstimatePriceInput { - quote_create: input.quote_create.clone(), - }) - .await?; let opportunity_create = self - .get_opportunity_create_for_quote(input.quote_create.clone(), output_amount) + .get_opportunity_create_for_quote(input.quote_create.clone()) .await?; let opportunity = self .add_opportunity(AddOpportunityInput { opportunity: opportunity_create, }) .await?; + let input_token = opportunity.buy_tokens[0].clone(); + let output_token = opportunity.sell_tokens[0].clone(); + if input_token.amount == 0 && output_token.amount == 0 { + tracing::error!(opportunity = ?opportunity, "Both token amounts are zero for swap opportunity"); + return Err(RestError::BadParameters( + "Both token amounts are zero for swap opportunity".to_string(), + )); + } // NOTE: This part will be removed after refactoring the permission key type let slice: [u8; 64] = opportunity @@ -123,7 +174,7 @@ impl Service { let bid_collection_time = OffsetDateTime::now_utc(); let mut bids = auction_service .get_live_bids(GetLiveBidsInput { - permission_key: permission_key_svm, + permission_key: permission_key_svm.clone(), }) .await; @@ -135,7 +186,7 @@ impl Service { // Add metrics let labels = [ ("chain_id", input.quote_create.chain_id.to_string()), - ("wallet", "phantom".to_string()), + ("router", input.quote_create.router.to_string()), ("total_bids", total_bids), ]; metrics::counter!("get_quote_total_bids", &labels).increment(1); @@ -146,24 +197,36 @@ impl Service { return Err(RestError::QuoteNotFound); } - // Find winner bid: the bid with the highest bid amount - bids.sort_by(|a, b| b.amount.cmp(&a.amount)); + // Find winner bid: + match input.quote_create.tokens { + entities::QuoteTokens::InputTokenSpecified { .. } => { + // highest bid = best (most output token returned) + bids.sort_by(|a, b| b.amount.cmp(&a.amount)); + } + entities::QuoteTokens::OutputTokenSpecified { .. } => { + // lowest bid = best (least input token consumed) + bids.sort_by(|a, b| a.amount.cmp(&b.amount)); + } + } + let winner_bid = bids.first().expect("failed to get first bid"); - // Find the submit bid instruction from bid transaction to extract the deadline - let submit_bid_instruction = auction_service - .verify_submit_bid_instruction(winner_bid.chain_data.transaction.clone()) - .map_err(|e| { - tracing::error!("Failed to verify submit bid instruction: {:?}", e); - RestError::TemporarilyUnavailable - })?; - let submit_bid_data = AuctionService::::extract_submit_bid_data( - &submit_bid_instruction, - ) - .map_err(|e| { - tracing::error!("Failed to extract submit bid data: {:?}", e); - RestError::TemporarilyUnavailable - })?; + // // TODO: uncomment this once Swap instruction is implemented + // // Find the swap instruction from bid transaction to extract the deadline + // let swap_instruction = auction_service + // .verify_swap_instruction(winner_bid.chain_data.transaction.clone()) + // .map_err(|e| { + // tracing::error!("Failed to verify swap instruction: {:?}", e); + // RestError::TemporarilyUnavailable + // })?; + // let swap_data = AuctionService::::extract_swap_data( + // &swap_instruction, + // ) + // .map_err(|e| { + // tracing::error!("Failed to extract swap data: {:?}", e); + // RestError::TemporarilyUnavailable + // })?; + let deadline = i64::MAX; // Bids is not empty let auction = Auction::try_new(bids.clone(), bid_collection_time) @@ -184,55 +247,64 @@ impl Service { }) .await?; - self.task_tracker.spawn({ - let (repo, db, winner_bid) = (self.repo.clone(), self.db.clone(), winner_bid.clone()); - let auction_service = auction_service.clone(); - async move { - join_all(auction.bids.iter().map(|bid| { - auction_service.update_bid_status(UpdateBidStatusInput { - new_status: AuctionService::get_new_status( - bid, - &vec![winner_bid.clone()], - BidStatusAuction { - tx_hash: signature, - id: auction.id, - }, - ), - bid: bid.clone(), - }) - })) - .await; - // Remove opportunity to prevent further bids - // The handle auction loop will take care of the bids that were submitted late - - // TODO - // Maybe we should add state for opportunity. - // Right now logic for removing halted/expired bids, checks if opportunity exists. - // We should remove opportunity only after the auction bid result is broadcasted. - // This is to make sure we are not gonna remove the bids that are currently in the auction in the handle_auction loop. - let removal_reason = - entities::OpportunityRemovalReason::Invalid(RestError::InvalidOpportunity( - "Auction finished for the opportunity".to_string(), - )); - if let Err(e) = repo - .remove_opportunity(&db, &opportunity, removal_reason) - .await - { - tracing::error!("Failed to remove opportunity: {:?}", e); - } - } - }); + join_all(auction.bids.iter().map(|bid| { + auction_service.update_bid_status(UpdateBidStatusInput { + new_status: AuctionService::get_new_status( + bid, + &vec![winner_bid.clone()], + BidStatusAuction { + tx_hash: signature, + id: auction.id, + }, + ), + bid: bid.clone(), + }) + })) + .await; + // Remove opportunity to prevent further bids + // The handle auction loop will take care of the bids that were submitted late + + // TODO + // Maybe we should add state for opportunity. + // Right now logic for removing halted/expired bids, checks if opportunity exists. + // We should remove opportunity only after the auction bid result is broadcasted. + // This is to make sure we are not gonna remove the bids that are currently in the auction in the handle_auction loop. + let removal_reason = entities::OpportunityRemovalReason::Invalid( + RestError::InvalidOpportunity("Auction finished for the opportunity".to_string()), + ); + if let Err(e) = self + .repo + .remove_opportunity(&self.db, &opportunity, removal_reason) + .await + { + tracing::error!("Failed to remove opportunity: {:?}", e); + } + + // we check the winner bid status here to make sure the winner bid was successfully entered into the db as submitted + // this is because: if the winner bid was not successfully entered as submitted, that could indicate the presence of + // duplicate auctions for the same quote. in such a scenario, one auction will conclude first and update the bid status + // of its winner bid, and we want to ensure that a winner bid whose status is already updated is not further updated + // to prevent a new status update from being broadcast + let live_bids = auction_service + .get_live_bids(GetLiveBidsInput { + permission_key: permission_key_svm, + }) + .await; + if !live_bids + .iter() + .any(|bid| bid.id == winner_bid.id && bid.status.is_submitted()) + { + tracing::error!(winner_bid = ?winner_bid, opportunity = ?opportunity, "Failed to update winner bid status"); + return Err(RestError::TemporarilyUnavailable); + } Ok(entities::Quote { - transaction: bid.chain_data.transaction.clone(), - expiration_time: submit_bid_data.deadline, - input_token: input.quote_create.input_token, - output_token: entities::TokenAmountSvm { - token: input.quote_create.output_mint_token, - amount: output_amount, - }, - maximum_slippage_percentage: input.quote_create.maximum_slippage_percentage, - chain_id: input.quote_create.chain_id, + transaction: bid.chain_data.transaction.clone(), + expiration_time: deadline, + input_token, + output_token, // TODO*: incorporate fees + maximum_slippage_bps: input.quote_create.maximum_slippage_bps, + chain_id: input.quote_create.chain_id, }) } } diff --git a/auction-server/src/opportunity/service/mod.rs b/auction-server/src/opportunity/service/mod.rs index 06e600c4..fa3852ab 100644 --- a/auction-server/src/opportunity/service/mod.rs +++ b/auction-server/src/opportunity/service/mod.rs @@ -31,13 +31,11 @@ use { types::Address, }, futures::future::try_join_all, - solana_sdk::pubkey::Pubkey, std::{ collections::HashMap, sync::Arc, }, tokio::sync::RwLock, - tokio_util::task::TaskTracker, }; pub mod add_opportunity; @@ -50,7 +48,6 @@ pub mod remove_invalid_or_expired_opportunities; pub mod remove_opportunities; pub mod verification; -mod estimate_price; mod get_spoof_info; mod make_adapter_calldata; mod make_opportunity_execution_params; @@ -84,8 +81,7 @@ impl ConfigEvm { // NOTE: Do not implement debug here. it has a circular reference to auction_service pub struct ConfigSvm { - pub wallet_program_router_account: Pubkey, - pub auction_service: RwLock>>, + pub auction_service: RwLock>>, } impl ConfigSvm { @@ -198,12 +194,11 @@ impl ConfigSvm { ) -> anyhow::Result> { Ok(chains .iter() - .map(|(chain_id, config)| { + .map(|(chain_id, _)| { ( chain_id.clone(), Self { - wallet_program_router_account: config.wallet_program_router_account, - auction_service: RwLock::new(None), + auction_service: RwLock::new(None), }, ) }) @@ -241,18 +236,16 @@ impl ChainType for ChainTypeSvm { // TODO maybe just create a service per chain_id? pub struct Service { - store: Arc, - db: DB, + store: Arc, + db: DB, // TODO maybe after adding state for opportunity we can remove the arc - repo: Arc>, - config: HashMap, - task_tracker: TaskTracker, + repo: Arc>, + config: HashMap, } impl Service { pub fn new(store: Arc, db: DB, config: HashMap) -> Self { Self { - task_tracker: store.task_tracker.clone(), store, db, repo: Arc::new(Repository::::new()), diff --git a/auction-server/src/server.rs b/auction-server/src/server.rs index 6fe2c9c0..0fadb078 100644 --- a/auction-server/src/server.rs +++ b/auction-server/src/server.rs @@ -251,7 +251,6 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { broadcast_sender, broadcast_receiver, }, - task_tracker: task_tracker.clone(), secret_key: run_options.secret_key.clone(), access_tokens: RwLock::new(access_tokens), metrics_recorder: setup_metrics_recorder()?, diff --git a/auction-server/src/state.rs b/auction-server/src/state.rs index 1469e1d1..30a2bd44 100644 --- a/auction-server/src/state.rs +++ b/auction-server/src/state.rs @@ -31,7 +31,6 @@ use { Response, RpcLogsResponse, }, - solana_sdk::pubkey::Pubkey, std::{ collections::HashMap, sync::Arc, @@ -45,7 +44,6 @@ use { }, RwLock, }, - tokio_util::task::TaskTracker, uuid::Uuid, }; @@ -105,11 +103,10 @@ impl ChainStoreEvm { } pub struct ChainStoreSvm { - pub log_sender: Sender>, + pub log_sender: Sender>, // only to avoid closing the channel - pub _dummy_log_receiver: Receiver>, - pub config: ConfigSvm, - pub wallet_program_router_account: Pubkey, + pub _dummy_log_receiver: Receiver>, + pub config: ConfigSvm, } impl ChainStoreSvm { @@ -119,8 +116,6 @@ impl ChainStoreSvm { Self { log_sender: tx, _dummy_log_receiver: rx, - - wallet_program_router_account: config.wallet_program_router_account, config, } } @@ -131,7 +126,6 @@ pub struct Store { pub chains_svm: HashMap>, pub ws: WsState, pub db: sqlx::PgPool, - pub task_tracker: TaskTracker, pub secret_key: String, pub access_tokens: RwLock>, pub metrics_recorder: PrometheusHandle,