Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add indicative price #398

Merged
merged 6 commits into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion apps/swap/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,19 @@ export default function Home() {
{
inputAmount: quote.inputToken.amount.toString(),
outputAmount: quote.outputToken.amount.toString(),
expirationTime: quote.expirationTime.toISOString(),
expirationTime: quote.expirationTime
? quote.expirationTime.toISOString()
: "undefined",
},
null,
2,
),
]);
if (!quote.transaction) {
throw new Error(
"Transaction not found due to wallet not being connected",
);
}
const signedTransaction = await signTransaction(quote.transaction);
// Do not call getAccountKeys() in case there is an unresolved lookup table
const accountPosition =
Expand Down
2 changes: 1 addition & 1 deletion auction-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "auction-server"
version = "0.18.1"
version = "0.19.0"
edition = "2021"
license-file = "license.txt"

Expand Down
30 changes: 19 additions & 11 deletions auction-server/api-types/src/opportunity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,10 +524,10 @@ pub struct OpportunityBidEvm {
#[serde_as]
#[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)]
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,
/// The user wallet address which requested the quote from the wallet. If not provided, an indicative price without a transaction will be returned.
#[schema(example = "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", value_type = Option<String>)]
#[serde_as(as = "Option<DisplayFromStr>")]
pub user_wallet_address: Option<Pubkey>,
/// The mint address of the token the user will provide in the swap.
#[schema(example = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", value_type = String)]
#[serde_as(as = "DisplayFromStr")]
Expand Down Expand Up @@ -591,15 +591,23 @@ pub enum QuoteCreate {
Svm(QuoteCreateSvm),
}

impl QuoteCreate {
pub fn get_user_wallet_address(&self) -> Option<Pubkey> {
match self {
QuoteCreate::Svm(QuoteCreateSvm::V1(params)) => params.user_wallet_address,
}
}
}

#[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug)]
pub struct QuoteV1Svm {
/// The 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,
/// 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,
/// The transaction for the quote to be executed on chain which is valid until the expiration time. Not provided if the quote to return is only an indicative price.
#[schema(example = "SGVsbG8sIFdvcmxkIQ==", value_type = Option<String>)]
#[serde(with = "crate::serde::nullable_transaction_svm")]
pub transaction: Option<VersionedTransaction>,
/// The expiration time of the quote (in seconds since the Unix epoch). Not provided if indicative price.
#[schema(example = 1_700_000_000_000_000i64, value_type = Option<i64>)]
pub expiration_time: Option<i64>,
/// The token and amount that the user needs to send to fulfill the swap transaction.
pub input_token: TokenAmountSvm,
/// The token and amount that the user will receive when the swap is complete.
Expand Down
50 changes: 50 additions & 0 deletions auction-server/api-types/src/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,56 @@ pub mod transaction_svm {
}
}

pub mod nullable_transaction_svm {
use {
base64::{
engine::general_purpose::STANDARD,
Engine as _,
},
serde::{
de::Error as _,
ser::Error,
Deserialize,
Deserializer,
Serializer,
},
solana_sdk::transaction::VersionedTransaction,
};

pub fn serialize<S>(t: &Option<VersionedTransaction>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match t {
Some(t) => {
let serialized =
bincode::serialize(t).map_err(|e| S::Error::custom(e.to_string()))?;
let base64_encoded = STANDARD.encode(serialized);
s.serialize_str(base64_encoded.as_str())
}
None => s.serialize_none(),
}
}

pub fn deserialize<'de, D>(d: D) -> Result<Option<VersionedTransaction>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Deserialize::deserialize(d)?;
match s {
Some(s) => {
let base64_decoded = STANDARD
.decode(s)
.map_err(|e| D::Error::custom(e.to_string()))?;
let transaction: VersionedTransaction = bincode::deserialize(&base64_decoded)
.map_err(|e| D::Error::custom(e.to_string()))?;
Ok(Some(transaction))
}
None => Ok(None),
}
}
}

pub mod nullable_signature_svm {
use {
serde::{
Expand Down
19 changes: 17 additions & 2 deletions auction-server/src/opportunity/api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use {
super::{
entities::QuoteCreate as QuoteCreateEntity,
repository::OPPORTUNITY_PAGE_SIZE_CAP,
service::{
add_opportunity::AddOpportunityInput,
Expand Down Expand Up @@ -45,6 +46,7 @@ use {
},
ErrorBodyResponse,
},
solana_sdk::pubkey::Pubkey,
std::sync::Arc,
time::OffsetDateTime,
};
Expand Down Expand Up @@ -198,6 +200,12 @@ pub async fn get_opportunities(
}
}

/// This corresponds to the base58 pubkey "Price11111111111111111111111111111111111112"
pub const INDICATIVE_PRICE_TAKER: Pubkey = Pubkey::new_from_array([
0x05, 0xda, 0xfe, 0x58, 0xfc, 0xc9, 0x54, 0xbe, 0x96, 0xc9, 0x32, 0xae, 0x8e, 0x9a, 0x17, 0x68,
0x9d, 0x10, 0x17, 0xf8, 0xc9, 0xe1, 0xb0, 0x7c, 0x86, 0x32, 0x71, 0xc0, 0x00, 0x00, 0x00, 0x01,
]);

/// Submit a quote request.
///
/// The server will create an opportunity and receive searcher bids
Expand All @@ -211,11 +219,18 @@ pub async fn post_quote(
State(store): State<Arc<StoreNew>>,
Json(params): Json<QuoteCreate>,
) -> Result<Json<Quote>, RestError> {
if params.get_user_wallet_address() == Some(INDICATIVE_PRICE_TAKER) {
return Err(RestError::BadParameters(
"Invalid user wallet address".to_string(),
));
}
let quote_create: QuoteCreateEntity = params.into();

let quote = store
.opportunity_service_svm
.get_quote(GetQuoteInput {
quote_create: params.into(),
program: ProgramSvm::Swap,
quote_create,
program: ProgramSvm::Swap,
})
.await?;

Expand Down
6 changes: 3 additions & 3 deletions auction-server/src/opportunity/entities/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ use {

#[derive(Debug, Clone, PartialEq)]
pub struct Quote {
pub transaction: VersionedTransaction,
pub transaction: Option<VersionedTransaction>,
// The expiration time of the quote (in seconds since the Unix epoch)
pub expiration_time: i64,
pub expiration_time: Option<i64>,
pub searcher_token: TokenAmountSvm,
pub user_token: TokenAmountSvm,
pub referrer_fee: TokenAmountSvm,
Expand All @@ -36,7 +36,7 @@ pub struct ReferralFeeInfo {

#[derive(Debug, Clone, PartialEq)]
pub struct QuoteCreate {
pub user_wallet_address: Pubkey,
pub user_wallet_address: Option<Pubkey>,
pub tokens: QuoteTokens,
pub referral_fee_info: Option<ReferralFeeInfo>,
pub chain_id: ChainId,
Expand Down
53 changes: 36 additions & 17 deletions auction-server/src/opportunity/service/get_quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use {
Svm,
},
opportunity::{
api::INDICATIVE_PRICE_TAKER,
entities::{
self,
TokenAmountSvm,
Expand Down Expand Up @@ -255,9 +256,13 @@ impl Service<ChainTypeSvm> {
},
},
};
let user_wallet_address = match quote_create.user_wallet_address {
Some(address) => address,
None => INDICATIVE_PRICE_TAKER,
};
let permission_account = get_quote_virtual_permission_account(
&tokens_for_permission,
&quote_create.user_wallet_address,
&user_wallet_address,
&router_token_account,
referral_fee_info.referral_fee_bps,
);
Expand All @@ -282,7 +287,7 @@ impl Service<ChainTypeSvm> {
let program_opportunity = match program {
ProgramSvm::Swap => {
entities::OpportunitySvmProgram::Swap(entities::OpportunitySvmProgramSwap {
user_wallet_address: quote_create.user_wallet_address,
user_wallet_address,
fee_token,
referral_fee_bps: referral_fee_info.referral_fee_bps,
platform_fee_bps: metadata.swap_platform_fee_bps,
Expand Down Expand Up @@ -401,18 +406,24 @@ impl Service<ChainTypeSvm> {
bids.sort_by(|a, b| a.amount.cmp(&b.amount));
}
}

let mut bids_filtered: Vec<Bid<Svm>> = Vec::new();
// here optimize_bids is used to batch the bid simulation.
// the first bid in the returned vector is the best bid that passes simulation.
if !bids.is_empty() {
bids_filtered = auction_service
.optimize_bids(&bids)
.await
.map(|x| x.value)
.map_err(|e| {
tracing::error!("Failed to simulate swap bids: {:?}", e);
RestError::TemporarilyUnavailable
})?;
if input.quote_create.user_wallet_address.is_none() {
// TODO: we may want to filter out bids by simulation later, but for now we just take the best bid as an indicative price
bids_filtered = bids.clone();
} else {
bids_filtered = auction_service
.optimize_bids(&bids)
.await
.map(|x| x.value)
.map_err(|e| {
tracing::error!("Failed to simulate swap bids: {:?}", e);
RestError::TemporarilyUnavailable
})?;
}
}

if bids_filtered.is_empty() {
Expand Down Expand Up @@ -535,28 +546,36 @@ impl Service<ChainTypeSvm> {
),
};

let (transaction, expiration_time) = match input.quote_create.user_wallet_address {
None => (None, None),
Some(_) => (
Some(winner_bid.chain_data.transaction.clone()),
Some(deadline),
),
};

Ok(entities::Quote {
transaction: winner_bid.chain_data.transaction.clone(),
expiration_time: deadline,
transaction,
expiration_time,

searcher_token: TokenAmountSvm {
token: searcher_token.token,
amount: searcher_amount,
},
user_token: TokenAmountSvm {
user_token: TokenAmountSvm {
token: user_token.token,
amount: user_amount,
},
referrer_fee: TokenAmountSvm {
referrer_fee: TokenAmountSvm {
token: fee_token,
amount: fees.relayer_fee,
},
platform_fee: TokenAmountSvm {
platform_fee: TokenAmountSvm {
token: fee_token,
amount: fees.express_relay_fee + fees.relayer_fee,
},
chain_id: input.quote_create.chain_id,
reference_id: auction.id,
chain_id: input.quote_create.chain_id,
reference_id: auction.id,
})
}
}
2 changes: 1 addition & 1 deletion sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-js",
"version": "0.21.2",
"version": "0.22.0",
"description": "Utilities for interacting with the express relay protocol",
"homepage": "https://github.com/pyth-network/per/tree/main/sdk/js",
"author": "Douro Labs",
Expand Down
16 changes: 11 additions & 5 deletions sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,9 @@ export class Client {
}
: null,
specified_token_amount: quoteRequest.specifiedTokenAmount,
user_wallet_address: quoteRequest.userWallet.toBase58(),
user_wallet_address: quoteRequest.userWallet
? quoteRequest.userWallet.toBase58()
: null,
version: "v1" as const,
};
// TODO: we may want to wrap all the GET/POST calls in a try/catch block to handle errors
Expand Down Expand Up @@ -753,7 +755,9 @@ export class Client {
): QuoteResponse {
return {
chainId: quoteResponse.chain_id,
expirationTime: new Date(quoteResponse.expiration_time * 1000),
expirationTime: quoteResponse.expiration_time
? new Date(quoteResponse.expiration_time * 1000)
: undefined,
inputToken: {
token: new PublicKey(quoteResponse.input_token.token),
amount: BigInt(quoteResponse.input_token.amount),
Expand All @@ -762,9 +766,11 @@ export class Client {
token: new PublicKey(quoteResponse.output_token.token),
amount: BigInt(quoteResponse.output_token.amount),
},
transaction: VersionedTransaction.deserialize(
new Uint8Array(base64.decode(quoteResponse.transaction)),
),
transaction: quoteResponse.transaction
? VersionedTransaction.deserialize(
new Uint8Array(base64.decode(quoteResponse.transaction)),
)
: undefined,
referenceId: quoteResponse.reference_id,
};
}
Expand Down
Loading
Loading