Files
khadhroony-bobobot/kb_lib/src/dex_pool_materialization.rs
2026-05-11 11:02:47 +02:00

663 lines
25 KiB
Rust

// file: kb_lib/src/dex_pool_materialization.rs
//! Shared DEX pool materialization helpers.
//!
//! This module persists normalized pool, pair, pool token and pool listing
//! records from decoded DEX events.
//!
//! It intentionally does not decode instructions and does not record analysis
//! signals. Detection services remain responsible for deciding which signals
//! to emit.
/// Token ordering strategy used when materializing a decoded DEX pool.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum DexPoolTokenOrder {
/// `token_a_mint` is already the base token and `token_b_mint` is already the quote token.
AlreadyBaseQuote,
/// Base/quote order should be chosen from token A/B using known quote-token hints.
ChooseBaseQuoteFromTokenAB,
}
/// Input required to materialize a normalized DEX pool.
#[derive(Debug, Clone)]
pub(crate) struct DexPoolMaterializationInput {
/// Parent decoded event id.
pub(crate) decoded_event_id: i64,
/// Internal DEX id.
pub(crate) dex_id: i64,
/// Pool account address.
pub(crate) pool_address: std::string::String,
/// Token A mint, or already-base mint depending on `token_order`.
pub(crate) token_a_mint: std::string::String,
/// Token B mint, or already-quote mint depending on `token_order`.
pub(crate) token_b_mint: std::string::String,
/// Optional LP mint.
pub(crate) lp_mint: std::option::Option<std::string::String>,
/// Optional token A vault address, or base vault when `token_order` is `AlreadyBaseQuote`.
pub(crate) token_a_vault_address: std::option::Option<std::string::String>,
/// Optional token B vault address, or quote vault when `token_order` is `AlreadyBaseQuote`.
pub(crate) token_b_vault_address: std::option::Option<std::string::String>,
/// Pool kind to persist.
pub(crate) pool_kind: crate::PoolKind,
/// Pool status to persist.
pub(crate) pool_status: crate::PoolStatus,
/// Token ordering strategy.
pub(crate) token_order: crate::dex_pool_materialization::DexPoolTokenOrder,
/// Listing source kind.
pub(crate) listing_source_kind: crate::ObservationSourceKind,
/// Optional source endpoint logical name.
pub(crate) source_endpoint_name: std::option::Option<std::string::String>,
}
impl DexPoolMaterializationInput {
/// Creates a materialization input from a decoded event requiring pool and two token mints.
pub(crate) fn from_decoded_event(
decoded_event: &crate::DexDecodedEventDto,
dex_id: i64,
pool_kind: crate::PoolKind,
pool_status: crate::PoolStatus,
token_order: crate::dex_pool_materialization::DexPoolTokenOrder,
token_a_vault_address: std::option::Option<std::string::String>,
token_b_vault_address: std::option::Option<std::string::String>,
source_endpoint_name: std::option::Option<std::string::String>,
) -> Result<Self, crate::Error> {
let decoded_event_id_result =
crate::dex_pool_materialization::required_decoded_event_id(decoded_event);
let decoded_event_id = match decoded_event_id_result {
Ok(decoded_event_id) => decoded_event_id,
Err(error) => return Err(error),
};
let pool_address_result =
crate::dex_pool_materialization::required_pool_account(decoded_event);
let pool_address = match pool_address_result {
Ok(pool_address) => pool_address,
Err(error) => return Err(error),
};
let token_a_mint_result =
crate::dex_pool_materialization::required_token_a_mint(decoded_event);
let token_a_mint = match token_a_mint_result {
Ok(token_a_mint) => token_a_mint,
Err(error) => return Err(error),
};
let token_b_mint_result =
crate::dex_pool_materialization::required_token_b_mint(decoded_event);
let token_b_mint = match token_b_mint_result {
Ok(token_b_mint) => token_b_mint,
Err(error) => return Err(error),
};
return Ok(Self {
decoded_event_id,
dex_id,
pool_address,
token_a_mint,
token_b_mint,
lp_mint: decoded_event.lp_mint.clone(),
token_a_vault_address,
token_b_vault_address,
pool_kind,
pool_status,
token_order,
listing_source_kind: crate::ObservationSourceKind::Dex,
source_endpoint_name,
});
}
/// Creates a materialization input from a decoded event and explicit token mints.
///
/// This is used by launch or bonding-curve events where the decoded event
/// may expose only the launched token mint and the quote mint is inferred
/// by the detector.
pub(crate) fn from_decoded_event_with_mints(
decoded_event: &crate::DexDecodedEventDto,
dex_id: i64,
token_a_mint: std::string::String,
token_b_mint: std::string::String,
lp_mint: std::option::Option<std::string::String>,
pool_kind: crate::PoolKind,
pool_status: crate::PoolStatus,
token_order: crate::dex_pool_materialization::DexPoolTokenOrder,
token_a_vault_address: std::option::Option<std::string::String>,
token_b_vault_address: std::option::Option<std::string::String>,
source_endpoint_name: std::option::Option<std::string::String>,
) -> Result<Self, crate::Error> {
let decoded_event_id_result =
crate::dex_pool_materialization::required_decoded_event_id(decoded_event);
let decoded_event_id = match decoded_event_id_result {
Ok(decoded_event_id) => decoded_event_id,
Err(error) => return Err(error),
};
let pool_address_result =
crate::dex_pool_materialization::required_pool_account(decoded_event);
let pool_address = match pool_address_result {
Ok(pool_address) => pool_address,
Err(error) => return Err(error),
};
return Ok(Self {
decoded_event_id,
dex_id,
pool_address,
token_a_mint,
token_b_mint,
lp_mint,
token_a_vault_address,
token_b_vault_address,
pool_kind,
pool_status,
token_order,
listing_source_kind: crate::ObservationSourceKind::Dex,
source_endpoint_name,
});
}
}
/// Returns the decoded event id or fails with a stable diagnostic.
pub(crate) fn required_decoded_event_id(
decoded_event: &crate::DexDecodedEventDto,
) -> Result<i64, crate::Error> {
match decoded_event.id {
Some(decoded_event_id) => return Ok(decoded_event_id),
None => {
return Err(crate::Error::InvalidState(
"decoded dex event has no internal id".to_string(),
));
},
}
}
/// Returns the pool account or fails with a stable diagnostic.
pub(crate) fn required_pool_account(
decoded_event: &crate::DexDecodedEventDto,
) -> Result<std::string::String, crate::Error> {
let decoded_event_id_result =
crate::dex_pool_materialization::required_decoded_event_id(decoded_event);
let decoded_event_id = match decoded_event_id_result {
Ok(decoded_event_id) => decoded_event_id,
Err(error) => return Err(error),
};
match decoded_event.pool_account.clone() {
Some(pool_account) => return Ok(pool_account),
None => {
return Err(crate::Error::InvalidState(format!(
"decoded event '{}' has no pool_account",
decoded_event_id
)));
},
}
}
/// Returns token A mint or fails with a stable diagnostic.
pub(crate) fn required_token_a_mint(
decoded_event: &crate::DexDecodedEventDto,
) -> Result<std::string::String, crate::Error> {
let decoded_event_id_result =
crate::dex_pool_materialization::required_decoded_event_id(decoded_event);
let decoded_event_id = match decoded_event_id_result {
Ok(decoded_event_id) => decoded_event_id,
Err(error) => return Err(error),
};
match decoded_event.token_a_mint.clone() {
Some(token_a_mint) => return Ok(token_a_mint),
None => {
return Err(crate::Error::InvalidState(format!(
"decoded event '{}' has no token_a_mint",
decoded_event_id
)));
},
}
}
/// Returns token B mint or fails with a stable diagnostic.
pub(crate) fn required_token_b_mint(
decoded_event: &crate::DexDecodedEventDto,
) -> Result<std::string::String, crate::Error> {
let decoded_event_id_result =
crate::dex_pool_materialization::required_decoded_event_id(decoded_event);
let decoded_event_id = match decoded_event_id_result {
Ok(decoded_event_id) => decoded_event_id,
Err(error) => return Err(error),
};
match decoded_event.token_b_mint.clone() {
Some(token_b_mint) => return Ok(token_b_mint),
None => {
return Err(crate::Error::InvalidState(format!(
"decoded event '{}' has no token_b_mint",
decoded_event_id
)));
},
}
}
/// Persists pool, pair, pool tokens and listing, returning the materialized ids.
pub(crate) async fn materialize_dex_pool(
database: &crate::Database,
input: &crate::dex_pool_materialization::DexPoolMaterializationInput,
) -> Result<crate::DexPoolDetectionResult, crate::Error> {
let ordered_tokens = crate::dex_pool_materialization::ordered_pool_tokens_from_input(input);
let base_token_id_result =
crate::dex_pool_materialization::ensure_token(database, ordered_tokens.base_mint.as_str())
.await;
let base_token_id = match base_token_id_result {
Ok(base_token_id) => base_token_id,
Err(error) => return Err(error),
};
let quote_token_id_result =
crate::dex_pool_materialization::ensure_token(database, ordered_tokens.quote_mint.as_str())
.await;
let quote_token_id = match quote_token_id_result {
Ok(quote_token_id) => quote_token_id,
Err(error) => return Err(error),
};
let lp_token_id = match input.lp_mint.clone() {
Some(lp_mint) => {
let lp_token_id_result =
crate::dex_pool_materialization::ensure_token(database, lp_mint.as_str()).await;
match lp_token_id_result {
Ok(lp_token_id) => Some(lp_token_id),
Err(error) => return Err(error),
}
},
None => None,
};
let pool_result = crate::dex_pool_materialization::ensure_pool(database, input).await;
let pool_materialization = match pool_result {
Ok(pool_materialization) => pool_materialization,
Err(error) => return Err(error),
};
let pair_result = crate::dex_pool_materialization::ensure_pair(
database,
input.dex_id,
pool_materialization.pool_id,
base_token_id,
quote_token_id,
ordered_tokens.base_mint.as_str(),
ordered_tokens.quote_mint.as_str(),
)
.await;
let pair_materialization = match pair_result {
Ok(pair_materialization) => pair_materialization,
Err(error) => return Err(error),
};
let upsert_base_pool_token_result = crate::query_pool_tokens_upsert(
database,
&crate::PoolTokenDto::new(
pool_materialization.pool_id,
base_token_id,
crate::PoolTokenRole::Base,
ordered_tokens.base_vault_address,
Some(0),
),
)
.await;
if let Err(error) = upsert_base_pool_token_result {
return Err(error);
}
let upsert_quote_pool_token_result = crate::query_pool_tokens_upsert(
database,
&crate::PoolTokenDto::new(
pool_materialization.pool_id,
quote_token_id,
crate::PoolTokenRole::Quote,
ordered_tokens.quote_vault_address,
Some(1),
),
)
.await;
if let Err(error) = upsert_quote_pool_token_result {
return Err(error);
}
if let Some(lp_token_id) = lp_token_id {
let upsert_lp_pool_token_result = crate::query_pool_tokens_upsert(
database,
&crate::PoolTokenDto::new(
pool_materialization.pool_id,
lp_token_id,
crate::PoolTokenRole::LpMint,
None,
None,
),
)
.await;
if let Err(error) = upsert_lp_pool_token_result {
return Err(error);
}
}
let listing_result = crate::dex_pool_materialization::ensure_pool_listing(
database,
input,
pool_materialization.pool_id,
pair_materialization.pair_id,
)
.await;
let listing_materialization = match listing_result {
Ok(listing_materialization) => listing_materialization,
Err(error) => return Err(error),
};
return Ok(crate::DexPoolDetectionResult {
decoded_event_id: input.decoded_event_id,
dex_id: input.dex_id,
pool_id: pool_materialization.pool_id,
pair_id: pair_materialization.pair_id,
pool_listing_id: listing_materialization.pool_listing_id,
created_pool: pool_materialization.created_pool,
created_pair: pair_materialization.created_pair,
created_listing: listing_materialization.created_listing,
});
}
#[derive(Debug, Clone)]
struct OrderedPoolTokens {
base_mint: std::string::String,
quote_mint: std::string::String,
base_vault_address: std::option::Option<std::string::String>,
quote_vault_address: std::option::Option<std::string::String>,
}
fn ordered_pool_tokens_from_input(
input: &crate::dex_pool_materialization::DexPoolMaterializationInput,
) -> OrderedPoolTokens {
match input.token_order {
crate::dex_pool_materialization::DexPoolTokenOrder::AlreadyBaseQuote => {
return OrderedPoolTokens {
base_mint: input.token_a_mint.clone(),
quote_mint: input.token_b_mint.clone(),
base_vault_address: input.token_a_vault_address.clone(),
quote_vault_address: input.token_b_vault_address.clone(),
};
},
crate::dex_pool_materialization::DexPoolTokenOrder::ChooseBaseQuoteFromTokenAB => {
let base_is_token_a = crate::dex_pool_materialization::choose_base_quote_order(
input.token_a_mint.as_str(),
input.token_b_mint.as_str(),
);
let base_mint = if base_is_token_a {
input.token_a_mint.clone()
} else {
input.token_b_mint.clone()
};
let quote_mint = if base_is_token_a {
input.token_b_mint.clone()
} else {
input.token_a_mint.clone()
};
let base_vault_address = if base_is_token_a {
input.token_a_vault_address.clone()
} else {
input.token_b_vault_address.clone()
};
let quote_vault_address = if base_is_token_a {
input.token_b_vault_address.clone()
} else {
input.token_a_vault_address.clone()
};
return OrderedPoolTokens {
base_mint,
quote_mint,
base_vault_address,
quote_vault_address,
};
},
}
}
async fn ensure_pool(
database: &crate::Database,
input: &crate::dex_pool_materialization::DexPoolMaterializationInput,
) -> Result<PoolMaterialization, crate::Error> {
let existing_pool_result =
crate::query_pools_get_by_address(database, input.pool_address.as_str()).await;
let existing_pool_option = match existing_pool_result {
Ok(existing_pool_option) => existing_pool_option,
Err(error) => return Err(error),
};
let created_pool = existing_pool_option.is_none();
let pool_id = match existing_pool_option {
Some(pool) => match pool.id {
Some(pool_id) => pool_id,
None => {
return Err(crate::Error::InvalidState(format!(
"pool '{}' has no internal id",
pool.address
)));
},
},
None => {
let pool_dto = crate::PoolDto::new(
input.dex_id,
input.pool_address.clone(),
input.pool_kind,
input.pool_status,
);
let upsert_result = crate::query_pools_upsert(database, &pool_dto).await;
match upsert_result {
Ok(pool_id) => pool_id,
Err(error) => return Err(error),
}
},
};
return Ok(PoolMaterialization { pool_id, created_pool });
}
async fn ensure_pair(
database: &crate::Database,
dex_id: i64,
pool_id: i64,
base_token_id: i64,
quote_token_id: i64,
base_mint: &str,
quote_mint: &str,
) -> Result<PairMaterialization, crate::Error> {
let existing_pair_result = crate::query_pairs_get_by_pool_id(database, pool_id).await;
let existing_pair_option = match existing_pair_result {
Ok(existing_pair_option) => existing_pair_option,
Err(error) => return Err(error),
};
let created_pair = existing_pair_option.is_none();
let pair_symbol = crate::dex_pool_materialization::build_pair_symbol(base_mint, quote_mint);
let pair_id = match existing_pair_option {
Some(pair) => match pair.id {
Some(pair_id) => pair_id,
None => {
return Err(crate::Error::InvalidState(format!(
"pair for pool '{}' has no internal id",
pool_id
)));
},
},
None => {
let pair_dto =
crate::PairDto::new(dex_id, pool_id, base_token_id, quote_token_id, pair_symbol);
let upsert_result = crate::query_pairs_upsert(database, &pair_dto).await;
match upsert_result {
Ok(pair_id) => pair_id,
Err(error) => return Err(error),
}
},
};
return Ok(PairMaterialization { pair_id, created_pair });
}
async fn ensure_pool_listing(
database: &crate::Database,
input: &crate::dex_pool_materialization::DexPoolMaterializationInput,
pool_id: i64,
pair_id: i64,
) -> Result<ListingMaterialization, crate::Error> {
let existing_listing_result =
crate::query_pool_listings_get_by_pool_id(database, pool_id).await;
let existing_listing_option = match existing_listing_result {
Ok(existing_listing_option) => existing_listing_option,
Err(error) => return Err(error),
};
let created_listing = existing_listing_option.is_none();
let pool_listing_id = match existing_listing_option {
Some(pool_listing) => pool_listing.id,
None => {
let listing_dto = crate::PoolListingDto::new(
input.dex_id,
pool_id,
Some(pair_id),
input.listing_source_kind,
input.source_endpoint_name.clone(),
None,
None,
None,
);
let upsert_result = crate::query_pool_listings_upsert(database, &listing_dto).await;
match upsert_result {
Ok(listing_id) => Some(listing_id),
Err(error) => return Err(error),
}
},
};
return Ok(ListingMaterialization { pool_listing_id, created_listing });
}
async fn ensure_token(database: &crate::Database, mint: &str) -> Result<i64, crate::Error> {
let token_result = crate::query_tokens_get_by_mint(database, mint).await;
let token_option = match token_result {
Ok(token_option) => token_option,
Err(error) => return Err(error),
};
match token_option {
Some(token) => match token.id {
Some(token_id) => return Ok(token_id),
None => {
return Err(crate::Error::InvalidState(format!(
"token '{}' has no internal id",
mint
)));
},
},
None => {
let token_dto = crate::TokenDto::new(
mint.to_string(),
None,
None,
None,
crate::SPL_TOKEN_PROGRAM_ID.to_string(),
crate::dex_pool_materialization::is_quote_mint(mint),
);
return crate::query_tokens_upsert(database, &token_dto).await;
},
}
}
#[derive(Debug, Clone)]
struct PoolMaterialization {
pool_id: i64,
created_pool: bool,
}
#[derive(Debug, Clone)]
struct PairMaterialization {
pair_id: i64,
created_pair: bool,
}
#[derive(Debug, Clone)]
struct ListingMaterialization {
pool_listing_id: std::option::Option<i64>,
created_listing: bool,
}
fn is_quote_mint(mint: &str) -> bool {
return mint == crate::WSOL_MINT_ID;
}
fn choose_base_quote_order(token_a_mint: &str, token_b_mint: &str) -> bool {
let token_a_is_quote = crate::dex_pool_materialization::is_quote_mint(token_a_mint);
let token_b_is_quote = crate::dex_pool_materialization::is_quote_mint(token_b_mint);
if token_a_is_quote && !token_b_is_quote {
return false;
}
if token_b_is_quote && !token_a_is_quote {
return true;
}
return true;
}
fn build_pair_symbol(
base_mint: &str,
quote_mint: &str,
) -> std::option::Option<std::string::String> {
let base_symbol = crate::dex_pool_materialization::symbol_hint_from_mint(base_mint);
let quote_symbol = crate::dex_pool_materialization::symbol_hint_from_mint(quote_mint);
match (base_symbol, quote_symbol) {
(Some(base_symbol), Some(quote_symbol)) => {
return Some(format!("{base_symbol}/{quote_symbol}"));
},
_ => return None,
}
}
fn symbol_hint_from_mint(mint: &str) -> std::option::Option<std::string::String> {
if mint == crate::WSOL_MINT_ID {
return Some("WSOL".to_string());
}
return None;
}
#[cfg(test)]
mod tests {
#[test]
fn quote_token_is_moved_to_quote_side_when_order_is_chosen() {
let input = crate::dex_pool_materialization::DexPoolMaterializationInput {
decoded_event_id: 1,
dex_id: 2,
pool_address: "Pool111".to_string(),
token_a_mint: crate::WSOL_MINT_ID.to_string(),
token_b_mint: "TokenB111".to_string(),
lp_mint: None,
token_a_vault_address: Some("VaultA111".to_string()),
token_b_vault_address: Some("VaultB111".to_string()),
pool_kind: crate::PoolKind::Amm,
pool_status: crate::PoolStatus::Active,
token_order:
crate::dex_pool_materialization::DexPoolTokenOrder::ChooseBaseQuoteFromTokenAB,
listing_source_kind: crate::ObservationSourceKind::Dex,
source_endpoint_name: Some("test".to_string()),
};
let ordered = super::ordered_pool_tokens_from_input(&input);
assert_eq!(ordered.base_mint, "TokenB111");
assert_eq!(ordered.quote_mint, crate::WSOL_MINT_ID);
assert_eq!(ordered.base_vault_address, Some("VaultB111".to_string()));
assert_eq!(ordered.quote_vault_address, Some("VaultA111".to_string()));
}
#[test]
fn already_base_quote_order_is_preserved() {
let input = crate::dex_pool_materialization::DexPoolMaterializationInput {
decoded_event_id: 1,
dex_id: 2,
pool_address: "Pool111".to_string(),
token_a_mint: crate::WSOL_MINT_ID.to_string(),
token_b_mint: "TokenB111".to_string(),
lp_mint: None,
token_a_vault_address: Some("BaseVault111".to_string()),
token_b_vault_address: Some("QuoteVault111".to_string()),
pool_kind: crate::PoolKind::Amm,
pool_status: crate::PoolStatus::Active,
token_order: crate::dex_pool_materialization::DexPoolTokenOrder::AlreadyBaseQuote,
listing_source_kind: crate::ObservationSourceKind::Dex,
source_endpoint_name: Some("test".to_string()),
};
let ordered = super::ordered_pool_tokens_from_input(&input);
assert_eq!(ordered.base_mint, crate::WSOL_MINT_ID);
assert_eq!(ordered.quote_mint, "TokenB111");
assert_eq!(ordered.base_vault_address, Some("BaseVault111".to_string()));
assert_eq!(ordered.quote_vault_address, Some("QuoteVault111".to_string()));
}
#[test]
fn pair_symbol_is_none_when_only_one_symbol_is_known() {
let pair_symbol = super::build_pair_symbol(crate::WSOL_MINT_ID, "TokenB111");
assert_eq!(pair_symbol, None);
}
#[test]
fn wsol_symbol_hint_is_known() {
let symbol = super::symbol_hint_from_mint(crate::WSOL_MINT_ID);
assert_eq!(symbol, Some("WSOL".to_string()));
}
}