0.7.27 +Refactor
This commit is contained in:
@@ -2,12 +2,9 @@
|
||||
|
||||
//! DexLab Swap/Pool transaction decoder.
|
||||
|
||||
/// DexLab Swap/Pool program id.
|
||||
pub const KB_DEXLAB_PROGRAM_ID: &str = "DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N";
|
||||
|
||||
/// Decoded DexLab create-pool event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbDexlabCreatePoolDecoded {
|
||||
pub struct DexlabCreatePoolDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -32,7 +29,7 @@ pub struct KbDexlabCreatePoolDecoded {
|
||||
|
||||
/// Decoded DexLab swap event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbDexlabSwapDecoded {
|
||||
pub struct DexlabSwapDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -42,7 +39,7 @@ pub struct KbDexlabSwapDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side relative to normalized base.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Optional pool account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional token A mint.
|
||||
@@ -55,25 +52,25 @@ pub struct KbDexlabSwapDecoded {
|
||||
|
||||
/// Decoded DexLab event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbDexlabDecodedEvent {
|
||||
pub enum DexlabDecodedEvent {
|
||||
/// Pool creation.
|
||||
CreatePool(KbDexlabCreatePoolDecoded),
|
||||
CreatePool(DexlabCreatePoolDecoded),
|
||||
/// Swap.
|
||||
Swap(KbDexlabSwapDecoded),
|
||||
Swap(DexlabSwapDecoded),
|
||||
}
|
||||
|
||||
/// DexLab decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbDexlabDecoder;
|
||||
pub struct DexlabDecoder;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KbDexlabInstructionKind {
|
||||
enum DexlabInstructionKind {
|
||||
CreatePool,
|
||||
Swap,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl KbDexlabDecoder {
|
||||
impl DexlabDecoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -82,14 +79,14 @@ impl KbDexlabDecoder {
|
||||
/// Decodes one projected transaction into zero or more DexLab events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbDexlabDecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::DexlabDecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -100,13 +97,13 @@ impl KbDexlabDecoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -117,7 +114,7 @@ impl KbDexlabDecoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_DEXLAB_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::DEXLAB_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -125,44 +122,42 @@ impl KbDexlabDecoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let parsed_json_result =
|
||||
kb_parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json = match parsed_json_result {
|
||||
Ok(parsed_json) => parsed_json,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_kind =
|
||||
kb_classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = kb_extract_string_by_candidate_keys(
|
||||
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 0));
|
||||
let token_a_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 0));
|
||||
let token_a_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenA", "tokenAMint", "mintA", "baseMint", "token0Mint", "mint0"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 1));
|
||||
let token_b_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 1));
|
||||
let token_b_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenB", "tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 2));
|
||||
let creator = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 2));
|
||||
let creator = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["payer", "creator", "user", "owner"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 3));
|
||||
let fee_tier = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 3));
|
||||
let fee_tier = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["feeTier", "fee_tier", "tradeFeeTier", "feeRate"],
|
||||
);
|
||||
if instruction_kind == KbDexlabInstructionKind::CreatePool {
|
||||
if instruction_kind == DexlabInstructionKind::CreatePool {
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "dexlab",
|
||||
"eventKind": "create_pool",
|
||||
@@ -178,8 +173,8 @@ impl KbDexlabDecoder {
|
||||
"creator": creator,
|
||||
"feeTier": fee_tier
|
||||
});
|
||||
decoded_events.push(crate::KbDexlabDecodedEvent::CreatePool(
|
||||
crate::KbDexlabCreatePoolDecoded {
|
||||
decoded_events.push(crate::DexlabDecodedEvent::CreatePool(
|
||||
crate::DexlabCreatePoolDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -194,8 +189,8 @@ impl KbDexlabDecoder {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if instruction_kind == KbDexlabInstructionKind::Swap {
|
||||
let trade_side = kb_infer_trade_side(&log_messages);
|
||||
if instruction_kind == DexlabInstructionKind::Swap {
|
||||
let trade_side = infer_trade_side(&log_messages);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "dexlab",
|
||||
"eventKind": "swap",
|
||||
@@ -210,57 +205,55 @@ impl KbDexlabDecoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"tradeSide": format!("{:?}", trade_side)
|
||||
});
|
||||
decoded_events.push(crate::KbDexlabDecodedEvent::Swap(
|
||||
crate::KbDexlabSwapDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
program_id: program_id.clone(),
|
||||
trade_side,
|
||||
pool_account,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
payload_json,
|
||||
},
|
||||
));
|
||||
decoded_events.push(crate::DexlabDecodedEvent::Swap(crate::DexlabSwapDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
program_id: program_id.clone(),
|
||||
trade_side,
|
||||
pool_account,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
payload_json,
|
||||
}));
|
||||
}
|
||||
}
|
||||
return Ok(decoded_events);
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_classify_instruction_kind(
|
||||
fn classify_instruction_kind(
|
||||
parsed_json: std::option::Option<&serde_json::Value>,
|
||||
log_messages: &[std::string::String],
|
||||
) -> KbDexlabInstructionKind {
|
||||
let parsed_instruction_name = kb_extract_string_by_candidate_keys(
|
||||
) -> DexlabInstructionKind {
|
||||
let parsed_instruction_name = extract_string_by_candidate_keys(
|
||||
parsed_json,
|
||||
&["instruction", "instructionName", "type", "name"],
|
||||
);
|
||||
if let Some(parsed_instruction_name) = parsed_instruction_name {
|
||||
let normalized = kb_normalize_text(parsed_instruction_name.as_str());
|
||||
let normalized = normalize_text(parsed_instruction_name.as_str());
|
||||
|
||||
if normalized.contains("createpool") || normalized.contains("initializepool") {
|
||||
return KbDexlabInstructionKind::CreatePool;
|
||||
return DexlabInstructionKind::CreatePool;
|
||||
}
|
||||
if normalized == "swap" {
|
||||
return KbDexlabInstructionKind::Swap;
|
||||
return DexlabInstructionKind::Swap;
|
||||
}
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "create_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "createpool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepool")
|
||||
if log_messages_contain_keyword(log_messages, "create_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "createpool")
|
||||
|| log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepool")
|
||||
{
|
||||
return KbDexlabInstructionKind::CreatePool;
|
||||
return DexlabInstructionKind::CreatePool;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "swap") {
|
||||
return KbDexlabInstructionKind::Swap;
|
||||
if log_messages_contain_keyword(log_messages, "swap") {
|
||||
return DexlabInstructionKind::Swap;
|
||||
}
|
||||
return KbDexlabInstructionKind::Unknown;
|
||||
return DexlabInstructionKind::Unknown;
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -287,10 +280,13 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = kb_normalize_text(keyword);
|
||||
for log_message in log_messages {
|
||||
let log_normalized = kb_normalize_text(log_message.as_str());
|
||||
fn log_messages_contain_keyword(
|
||||
log_messages_contain_keyword: &[std::string::String],
|
||||
keyword: &str,
|
||||
) -> bool {
|
||||
let keyword_normalized = normalize_text(keyword);
|
||||
for log_message in log_messages_contain_keyword {
|
||||
let log_normalized = normalize_text(log_message.as_str());
|
||||
if log_normalized.contains(keyword_normalized.as_str()) {
|
||||
return true;
|
||||
}
|
||||
@@ -298,9 +294,9 @@ fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
fn normalize_text(normalize_text: &str) -> std::string::String {
|
||||
let mut normalized = std::string::String::new();
|
||||
for character in value.chars() {
|
||||
for character in normalize_text.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
normalized.push(character.to_ascii_lowercase());
|
||||
}
|
||||
@@ -308,14 +304,14 @@ fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -331,9 +327,9 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_parse_optional_parsed_json(
|
||||
fn parse_optional_parsed_json(
|
||||
parsed_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::KbError> {
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
|
||||
let parsed_json = match parsed_json {
|
||||
Some(parsed_json) => parsed_json,
|
||||
None => return Ok(None),
|
||||
@@ -342,7 +338,7 @@ fn kb_parse_optional_parsed_json(
|
||||
match value_result {
|
||||
Ok(value) => return Ok(Some(value)),
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction parsed_json '{}': {}",
|
||||
parsed_json, error
|
||||
)));
|
||||
@@ -350,7 +346,7 @@ fn kb_parse_optional_parsed_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -358,10 +354,10 @@ fn kb_extract_string_by_candidate_keys(
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
return kb_extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
return extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys_inner(
|
||||
fn extract_string_by_candidate_keys_inner(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -377,7 +373,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -387,7 +383,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -396,30 +392,30 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
accounts: &[std::string::String],
|
||||
fn extract_account(
|
||||
extract_account: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
if index >= accounts.len() {
|
||||
if index >= extract_account.len() {
|
||||
return None;
|
||||
}
|
||||
return Some(accounts[index].clone());
|
||||
return Some(extract_account[index].clone());
|
||||
}
|
||||
|
||||
fn kb_infer_trade_side(log_messages: &[std::string::String]) -> crate::KbSwapTradeSide {
|
||||
if kb_log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::KbSwapTradeSide::BuyBase;
|
||||
fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide {
|
||||
if log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::SwapTradeSide::BuyBase;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::KbSwapTradeSide::SellBase;
|
||||
if log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::SwapTradeSide::SellBase;
|
||||
}
|
||||
return crate::KbSwapTradeSide::Unknown;
|
||||
return crate::SwapTradeSide::Unknown;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-dexlab-create-1".to_string(),
|
||||
Some(893001),
|
||||
Some(1779800001),
|
||||
@@ -446,19 +442,19 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_create_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_create_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
801,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_DEXLAB_PROGRAM_ID.to_string()),
|
||||
Some(crate::DEXLAB_PROGRAM_ID.to_string()),
|
||||
Some("dexlab".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DexlabPool111",
|
||||
"DexlabTokenA111",
|
||||
"So11111111111111111111111111111111111111112",
|
||||
crate::WSOL_MINT_ID,
|
||||
"DexlabCreator111"
|
||||
])
|
||||
.to_string(),
|
||||
@@ -470,7 +466,7 @@ mod tests {
|
||||
"instruction": "create_pool",
|
||||
"pool": "DexlabPool111",
|
||||
"tokenA": "DexlabTokenA111",
|
||||
"tokenB": "So11111111111111111111111111111111111111112",
|
||||
"tokenB": crate::WSOL_MINT_ID,
|
||||
"payer": "DexlabCreator111",
|
||||
"feeTier": "0.3%"
|
||||
}
|
||||
@@ -482,8 +478,8 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-dexlab-swap-1".to_string(),
|
||||
Some(893002),
|
||||
Some(1779800002),
|
||||
@@ -510,21 +506,17 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_swap_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
803,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_DEXLAB_PROGRAM_ID.to_string()),
|
||||
Some(crate::DEXLAB_PROGRAM_ID.to_string()),
|
||||
Some("dexlab".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DexlabSwapPool111",
|
||||
"DexlabSwapTokenA111",
|
||||
"So11111111111111111111111111111111111111112"
|
||||
])
|
||||
.to_string(),
|
||||
serde_json::json!(["DexlabSwapPool111", "DexlabSwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(
|
||||
@@ -533,7 +525,7 @@ mod tests {
|
||||
"instruction": "swap",
|
||||
"pool": "DexlabSwapPool111",
|
||||
"tokenA": "DexlabSwapTokenA111",
|
||||
"tokenB": "So11111111111111111111111111111111111111112"
|
||||
"tokenB": crate::WSOL_MINT_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -545,7 +537,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dexlab_create_pool_is_detected() {
|
||||
let decoder = crate::KbDexlabDecoder::new();
|
||||
let decoder = crate::DexlabDecoder::new();
|
||||
let transaction = make_create_transaction();
|
||||
let instructions = vec![make_create_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -555,18 +547,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbDexlabDecodedEvent::CreatePool(event) => {
|
||||
crate::DexlabDecodedEvent::CreatePool(event) => {
|
||||
assert_eq!(event.transaction_id, 801);
|
||||
assert_eq!(event.instruction_id, 802);
|
||||
assert_eq!(event.pool_account, Some("DexlabPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DexlabTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
assert_eq!(event.fee_tier, Some("0.3%".to_string()));
|
||||
},
|
||||
crate::KbDexlabDecodedEvent::Swap(_) => {
|
||||
crate::DexlabDecodedEvent::Swap(_) => {
|
||||
panic!("unexpected swap event")
|
||||
},
|
||||
}
|
||||
@@ -574,7 +563,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dexlab_swap_is_detected() {
|
||||
let decoder = crate::KbDexlabDecoder::new();
|
||||
let decoder = crate::DexlabDecoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let instructions = vec![make_swap_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -584,17 +573,14 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbDexlabDecodedEvent::Swap(event) => {
|
||||
crate::DexlabDecodedEvent::Swap(event) => {
|
||||
assert_eq!(event.transaction_id, 803);
|
||||
assert_eq!(event.instruction_id, 804);
|
||||
assert_eq!(event.pool_account, Some("DexlabSwapPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DexlabSwapTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
},
|
||||
crate::KbDexlabDecodedEvent::CreatePool(_) => {
|
||||
crate::DexlabDecodedEvent::CreatePool(_) => {
|
||||
panic!("unexpected create event")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
//! FluxBeam transaction decoder.
|
||||
|
||||
/// FluxBeam program id.
|
||||
pub const KB_FLUXBEAM_PROGRAM_ID: &str = "FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X";
|
||||
|
||||
/// Decoded FluxBeam create-pool event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbFluxbeamCreatePoolDecoded {
|
||||
pub struct FluxbeamCreatePoolDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -32,7 +29,7 @@ pub struct KbFluxbeamCreatePoolDecoded {
|
||||
|
||||
/// Decoded FluxBeam swap event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbFluxbeamSwapDecoded {
|
||||
pub struct FluxbeamSwapDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -42,7 +39,7 @@ pub struct KbFluxbeamSwapDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side relative to normalized base.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Optional pool account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional token A mint.
|
||||
@@ -55,25 +52,25 @@ pub struct KbFluxbeamSwapDecoded {
|
||||
|
||||
/// Decoded FluxBeam event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbFluxbeamDecodedEvent {
|
||||
pub enum FluxbeamDecodedEvent {
|
||||
/// Pool creation.
|
||||
CreatePool(KbFluxbeamCreatePoolDecoded),
|
||||
CreatePool(FluxbeamCreatePoolDecoded),
|
||||
/// Swap.
|
||||
Swap(KbFluxbeamSwapDecoded),
|
||||
Swap(FluxbeamSwapDecoded),
|
||||
}
|
||||
|
||||
/// FluxBeam decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbFluxbeamDecoder;
|
||||
pub struct FluxbeamDecoder;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KbFluxbeamInstructionKind {
|
||||
enum FluxbeamInstructionKind {
|
||||
CreatePool,
|
||||
Swap,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl KbFluxbeamDecoder {
|
||||
impl FluxbeamDecoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -82,14 +79,14 @@ impl KbFluxbeamDecoder {
|
||||
/// Decodes one projected transaction into zero or more FluxBeam events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbFluxbeamDecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::FluxbeamDecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -100,13 +97,13 @@ impl KbFluxbeamDecoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -117,7 +114,7 @@ impl KbFluxbeamDecoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_FLUXBEAM_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::FLUXBEAM_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -125,45 +122,43 @@ impl KbFluxbeamDecoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let parsed_json_result =
|
||||
kb_parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json = match parsed_json_result {
|
||||
Ok(parsed_json) => parsed_json,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_kind =
|
||||
kb_classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = kb_extract_string_by_candidate_keys(
|
||||
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 0));
|
||||
let lp_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 0));
|
||||
let lp_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["lpMint", "lp_mint", "lpTokenMint"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 1));
|
||||
let token_a_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 1));
|
||||
let token_a_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenA", "tokenAMint", "mintA", "baseMint", "token0Mint", "mint0"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 2));
|
||||
let token_b_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 2));
|
||||
let token_b_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenB", "tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 3));
|
||||
let creator = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 3));
|
||||
let creator = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["payer", "creator", "user", "owner"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 4));
|
||||
if instruction_kind == KbFluxbeamInstructionKind::CreatePool {
|
||||
.or_else(|| return extract_account(&accounts, 4));
|
||||
if instruction_kind == FluxbeamInstructionKind::CreatePool {
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "fluxbeam",
|
||||
"eventKind": "create_pool",
|
||||
@@ -179,8 +174,8 @@ impl KbFluxbeamDecoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"creator": creator
|
||||
});
|
||||
decoded_events.push(crate::KbFluxbeamDecodedEvent::CreatePool(
|
||||
crate::KbFluxbeamCreatePoolDecoded {
|
||||
decoded_events.push(crate::FluxbeamDecodedEvent::CreatePool(
|
||||
crate::FluxbeamCreatePoolDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -195,8 +190,8 @@ impl KbFluxbeamDecoder {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if instruction_kind == KbFluxbeamInstructionKind::Swap {
|
||||
let trade_side = kb_infer_trade_side(&log_messages);
|
||||
if instruction_kind == FluxbeamInstructionKind::Swap {
|
||||
let trade_side = infer_trade_side(&log_messages);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "fluxbeam",
|
||||
"eventKind": "swap",
|
||||
@@ -211,8 +206,8 @@ impl KbFluxbeamDecoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"tradeSide": format!("{:?}", trade_side)
|
||||
});
|
||||
decoded_events.push(crate::KbFluxbeamDecodedEvent::Swap(
|
||||
crate::KbFluxbeamSwapDecoded {
|
||||
decoded_events.push(crate::FluxbeamDecodedEvent::Swap(
|
||||
crate::FluxbeamSwapDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -230,37 +225,37 @@ impl KbFluxbeamDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_classify_instruction_kind(
|
||||
fn classify_instruction_kind(
|
||||
parsed_json: std::option::Option<&serde_json::Value>,
|
||||
log_messages: &[std::string::String],
|
||||
) -> KbFluxbeamInstructionKind {
|
||||
let parsed_instruction_name = kb_extract_string_by_candidate_keys(
|
||||
) -> FluxbeamInstructionKind {
|
||||
let parsed_instruction_name = extract_string_by_candidate_keys(
|
||||
parsed_json,
|
||||
&["instruction", "instructionName", "type", "name"],
|
||||
);
|
||||
if let Some(parsed_instruction_name) = parsed_instruction_name {
|
||||
let normalized = kb_normalize_text(parsed_instruction_name.as_str());
|
||||
let normalized = normalize_text(parsed_instruction_name.as_str());
|
||||
if normalized.contains("createpool") || normalized.contains("initializepool") {
|
||||
return KbFluxbeamInstructionKind::CreatePool;
|
||||
return FluxbeamInstructionKind::CreatePool;
|
||||
}
|
||||
if normalized == "swap" {
|
||||
return KbFluxbeamInstructionKind::Swap;
|
||||
return FluxbeamInstructionKind::Swap;
|
||||
}
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "create_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "createpool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepool")
|
||||
if log_messages_contain_keyword(log_messages, "create_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "createpool")
|
||||
|| log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepool")
|
||||
{
|
||||
return KbFluxbeamInstructionKind::CreatePool;
|
||||
return FluxbeamInstructionKind::CreatePool;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "swap") {
|
||||
return KbFluxbeamInstructionKind::Swap;
|
||||
if log_messages_contain_keyword(log_messages, "swap") {
|
||||
return FluxbeamInstructionKind::Swap;
|
||||
}
|
||||
return KbFluxbeamInstructionKind::Unknown;
|
||||
return FluxbeamInstructionKind::Unknown;
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -287,10 +282,10 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = kb_normalize_text(keyword);
|
||||
fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = normalize_text(keyword);
|
||||
for log_message in log_messages {
|
||||
let log_normalized = kb_normalize_text(log_message.as_str());
|
||||
let log_normalized = normalize_text(log_message.as_str());
|
||||
if log_normalized.contains(keyword_normalized.as_str()) {
|
||||
return true;
|
||||
}
|
||||
@@ -298,7 +293,7 @@ fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
fn normalize_text(value: &str) -> std::string::String {
|
||||
let mut normalized = std::string::String::new();
|
||||
for character in value.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
@@ -308,14 +303,14 @@ fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -331,9 +326,9 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_parse_optional_parsed_json(
|
||||
fn parse_optional_parsed_json(
|
||||
parsed_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::KbError> {
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
|
||||
let parsed_json = match parsed_json {
|
||||
Some(parsed_json) => parsed_json,
|
||||
None => return Ok(None),
|
||||
@@ -342,7 +337,7 @@ fn kb_parse_optional_parsed_json(
|
||||
match value_result {
|
||||
Ok(value) => return Ok(Some(value)),
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction parsed_json '{}': {}",
|
||||
parsed_json, error
|
||||
)));
|
||||
@@ -350,7 +345,7 @@ fn kb_parse_optional_parsed_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -358,10 +353,10 @@ fn kb_extract_string_by_candidate_keys(
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
return kb_extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
return extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys_inner(
|
||||
fn extract_string_by_candidate_keys_inner(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -377,7 +372,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -387,7 +382,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -396,7 +391,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
fn extract_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -406,20 +401,20 @@ fn kb_extract_account(
|
||||
return Some(accounts[index].clone());
|
||||
}
|
||||
|
||||
fn kb_infer_trade_side(log_messages: &[std::string::String]) -> crate::KbSwapTradeSide {
|
||||
if kb_log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::KbSwapTradeSide::BuyBase;
|
||||
fn infer_trade_side(infer_trade_side: &[std::string::String]) -> crate::SwapTradeSide {
|
||||
if log_messages_contain_keyword(infer_trade_side, "buy") {
|
||||
return crate::SwapTradeSide::BuyBase;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::KbSwapTradeSide::SellBase;
|
||||
if log_messages_contain_keyword(infer_trade_side, "sell") {
|
||||
return crate::SwapTradeSide::SellBase;
|
||||
}
|
||||
return crate::KbSwapTradeSide::Unknown;
|
||||
return crate::SwapTradeSide::Unknown;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-fluxbeam-create-1".to_string(),
|
||||
Some(892001),
|
||||
Some(1779700001),
|
||||
@@ -446,20 +441,20 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_create_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_create_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
701,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_FLUXBEAM_PROGRAM_ID.to_string()),
|
||||
Some(crate::FLUXBEAM_PROGRAM_ID.to_string()),
|
||||
Some("fluxbeam".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"FluxPool111",
|
||||
"FluxLpMint111",
|
||||
"FluxTokenA111",
|
||||
"So11111111111111111111111111111111111111112",
|
||||
crate::WSOL_MINT_ID,
|
||||
"FluxCreator111"
|
||||
])
|
||||
.to_string(),
|
||||
@@ -472,7 +467,7 @@ mod tests {
|
||||
"pool": "FluxPool111",
|
||||
"lpMint": "FluxLpMint111",
|
||||
"tokenA": "FluxTokenA111",
|
||||
"tokenB": "So11111111111111111111111111111111111111112",
|
||||
"tokenB": crate::WSOL_MINT_ID,
|
||||
"payer": "FluxCreator111"
|
||||
}
|
||||
})
|
||||
@@ -483,8 +478,8 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-fluxbeam-swap-1".to_string(),
|
||||
Some(892002),
|
||||
Some(1779700002),
|
||||
@@ -511,20 +506,20 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_swap_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
703,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_FLUXBEAM_PROGRAM_ID.to_string()),
|
||||
Some(crate::FLUXBEAM_PROGRAM_ID.to_string()),
|
||||
Some("fluxbeam".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"FluxSwapPool111",
|
||||
"FluxSwapLpMint111",
|
||||
"FluxSwapTokenA111",
|
||||
"So11111111111111111111111111111111111111112"
|
||||
crate::WSOL_MINT_ID
|
||||
])
|
||||
.to_string(),
|
||||
None,
|
||||
@@ -535,7 +530,7 @@ mod tests {
|
||||
"instruction": "swap",
|
||||
"pool": "FluxSwapPool111",
|
||||
"tokenA": "FluxSwapTokenA111",
|
||||
"tokenB": "So11111111111111111111111111111111111111112"
|
||||
"tokenB": crate::WSOL_MINT_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -547,7 +542,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn fluxbeam_create_pool_is_detected() {
|
||||
let decoder = crate::KbFluxbeamDecoder::new();
|
||||
let decoder = crate::FluxbeamDecoder::new();
|
||||
let transaction = make_create_transaction();
|
||||
let instructions = vec![make_create_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -557,18 +552,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbFluxbeamDecodedEvent::CreatePool(event) => {
|
||||
crate::FluxbeamDecodedEvent::CreatePool(event) => {
|
||||
assert_eq!(event.transaction_id, 701);
|
||||
assert_eq!(event.instruction_id, 702);
|
||||
assert_eq!(event.pool_account, Some("FluxPool111".to_string()));
|
||||
assert_eq!(event.lp_mint, Some("FluxLpMint111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("FluxTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
},
|
||||
crate::KbFluxbeamDecodedEvent::Swap(_) => {
|
||||
crate::FluxbeamDecodedEvent::Swap(_) => {
|
||||
panic!("unexpected swap event")
|
||||
},
|
||||
}
|
||||
@@ -576,7 +568,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn fluxbeam_swap_is_detected() {
|
||||
let decoder = crate::KbFluxbeamDecoder::new();
|
||||
let decoder = crate::FluxbeamDecoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let instructions = vec![make_swap_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -586,17 +578,14 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbFluxbeamDecodedEvent::Swap(event) => {
|
||||
crate::FluxbeamDecodedEvent::Swap(event) => {
|
||||
assert_eq!(event.transaction_id, 703);
|
||||
assert_eq!(event.instruction_id, 704);
|
||||
assert_eq!(event.pool_account, Some("FluxSwapPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("FluxSwapTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
},
|
||||
crate::KbFluxbeamDecodedEvent::CreatePool(_) => {
|
||||
crate::FluxbeamDecodedEvent::CreatePool(_) => {
|
||||
panic!("unexpected create event")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
//! Meteora DAMM v1 transaction decoder.
|
||||
|
||||
/// Meteora DAMM v1 program id.
|
||||
pub const KB_METEORA_DAMM_V1_PROGRAM_ID: &str = "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB";
|
||||
|
||||
/// Decoded Meteora DAMM v1 create-pool event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbMeteoraDammV1CreatePoolDecoded {
|
||||
pub struct MeteoraDammV1CreatePoolDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -34,7 +31,7 @@ pub struct KbMeteoraDammV1CreatePoolDecoded {
|
||||
|
||||
/// Decoded Meteora DAMM v1 swap event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbMeteoraDammV1SwapDecoded {
|
||||
pub struct MeteoraDammV1SwapDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -44,7 +41,7 @@ pub struct KbMeteoraDammV1SwapDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side relative to normalized base.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Optional pool account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional token A mint.
|
||||
@@ -57,26 +54,26 @@ pub struct KbMeteoraDammV1SwapDecoded {
|
||||
|
||||
/// Decoded Meteora DAMM v1 event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbMeteoraDammV1DecodedEvent {
|
||||
pub enum MeteoraDammV1DecodedEvent {
|
||||
/// Pool creation.
|
||||
CreatePool(KbMeteoraDammV1CreatePoolDecoded),
|
||||
CreatePool(MeteoraDammV1CreatePoolDecoded),
|
||||
/// Swap.
|
||||
Swap(KbMeteoraDammV1SwapDecoded),
|
||||
Swap(MeteoraDammV1SwapDecoded),
|
||||
}
|
||||
|
||||
/// Meteora DAMM v1 decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbMeteoraDammV1Decoder;
|
||||
pub struct MeteoraDammV1Decoder;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KbMeteoraDammV1InstructionKind {
|
||||
enum MeteoraDammV1InstructionKind {
|
||||
CreatePool,
|
||||
CreatePoolWithConfig,
|
||||
Swap,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl KbMeteoraDammV1Decoder {
|
||||
impl MeteoraDammV1Decoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -85,14 +82,14 @@ impl KbMeteoraDammV1Decoder {
|
||||
/// Decodes one projected transaction into zero or more Meteora DAMM v1 events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbMeteoraDammV1DecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::MeteoraDammV1DecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -103,13 +100,13 @@ impl KbMeteoraDammV1Decoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -120,7 +117,7 @@ impl KbMeteoraDammV1Decoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_METEORA_DAMM_V1_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::METEORA_DAMM_V1_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -128,49 +125,47 @@ impl KbMeteoraDammV1Decoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let parsed_json_result =
|
||||
kb_parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json = match parsed_json_result {
|
||||
Ok(parsed_json) => parsed_json,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_kind =
|
||||
kb_classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = kb_extract_string_by_candidate_keys(
|
||||
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 0));
|
||||
let token_a_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 0));
|
||||
let token_a_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenAMint", "mintA", "baseMint", "token0Mint", "mint0", "coinMint"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 1));
|
||||
let token_b_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 1));
|
||||
let token_b_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1", "pcMint"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 2));
|
||||
let config_account = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 2));
|
||||
let config_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["config", "poolConfig", "ammConfig", "tradeFeeConfig"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 3));
|
||||
let creator = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 3));
|
||||
let creator = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["creator", "payer", "user", "owner"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 4));
|
||||
if instruction_kind == KbMeteoraDammV1InstructionKind::CreatePool
|
||||
|| instruction_kind == KbMeteoraDammV1InstructionKind::CreatePoolWithConfig
|
||||
.or_else(|| return extract_account(&accounts, 4));
|
||||
if instruction_kind == MeteoraDammV1InstructionKind::CreatePool
|
||||
|| instruction_kind == MeteoraDammV1InstructionKind::CreatePoolWithConfig
|
||||
{
|
||||
let used_config =
|
||||
instruction_kind == KbMeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
instruction_kind == MeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "meteora_damm_v1",
|
||||
"eventKind": "create_pool",
|
||||
@@ -187,8 +182,8 @@ impl KbMeteoraDammV1Decoder {
|
||||
"configAccount": config_account,
|
||||
"creator": creator
|
||||
});
|
||||
decoded_events.push(crate::KbMeteoraDammV1DecodedEvent::CreatePool(
|
||||
crate::KbMeteoraDammV1CreatePoolDecoded {
|
||||
decoded_events.push(crate::MeteoraDammV1DecodedEvent::CreatePool(
|
||||
crate::MeteoraDammV1CreatePoolDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -204,8 +199,8 @@ impl KbMeteoraDammV1Decoder {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if instruction_kind == KbMeteoraDammV1InstructionKind::Swap {
|
||||
let trade_side = kb_infer_trade_side(&log_messages);
|
||||
if instruction_kind == MeteoraDammV1InstructionKind::Swap {
|
||||
let trade_side = infer_trade_side(&log_messages);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "meteora_damm_v1",
|
||||
"eventKind": "swap",
|
||||
@@ -221,8 +216,8 @@ impl KbMeteoraDammV1Decoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"tradeSide": format!("{:?}", trade_side)
|
||||
});
|
||||
decoded_events.push(crate::KbMeteoraDammV1DecodedEvent::Swap(
|
||||
crate::KbMeteoraDammV1SwapDecoded {
|
||||
decoded_events.push(crate::MeteoraDammV1DecodedEvent::Swap(
|
||||
crate::MeteoraDammV1SwapDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -240,46 +235,46 @@ impl KbMeteoraDammV1Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_classify_instruction_kind(
|
||||
fn classify_instruction_kind(
|
||||
parsed_json: std::option::Option<&serde_json::Value>,
|
||||
log_messages: &[std::string::String],
|
||||
) -> KbMeteoraDammV1InstructionKind {
|
||||
let parsed_instruction_name = kb_extract_string_by_candidate_keys(
|
||||
) -> MeteoraDammV1InstructionKind {
|
||||
let parsed_instruction_name = extract_string_by_candidate_keys(
|
||||
parsed_json,
|
||||
&["instruction", "instructionName", "type", "name"],
|
||||
);
|
||||
if let Some(parsed_instruction_name) = parsed_instruction_name {
|
||||
let normalized = kb_normalize_text(parsed_instruction_name.as_str());
|
||||
let normalized = normalize_text(parsed_instruction_name.as_str());
|
||||
if normalized.contains("initializepoolwithconfig") {
|
||||
return KbMeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
return MeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
}
|
||||
if normalized.contains("initializepool") {
|
||||
return KbMeteoraDammV1InstructionKind::CreatePool;
|
||||
return MeteoraDammV1InstructionKind::CreatePool;
|
||||
}
|
||||
if normalized == "swap" {
|
||||
return KbMeteoraDammV1InstructionKind::Swap;
|
||||
return MeteoraDammV1InstructionKind::Swap;
|
||||
}
|
||||
}
|
||||
if kb_value_contains_any_key(parsed_json, &["poolConfig", "ammConfig", "tradeFeeConfig"]) {
|
||||
return KbMeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
if value_contains_any_key(parsed_json, &["poolConfig", "ammConfig", "tradeFeeConfig"]) {
|
||||
return MeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "initialize_pool_with_config")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepoolwithconfig")
|
||||
if log_messages_contain_keyword(log_messages, "initialize_pool_with_config")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepoolwithconfig")
|
||||
{
|
||||
return KbMeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
return MeteoraDammV1InstructionKind::CreatePoolWithConfig;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepool")
|
||||
if log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepool")
|
||||
{
|
||||
return KbMeteoraDammV1InstructionKind::CreatePool;
|
||||
return MeteoraDammV1InstructionKind::CreatePool;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "swap") {
|
||||
return KbMeteoraDammV1InstructionKind::Swap;
|
||||
if log_messages_contain_keyword(log_messages, "swap") {
|
||||
return MeteoraDammV1InstructionKind::Swap;
|
||||
}
|
||||
return KbMeteoraDammV1InstructionKind::Unknown;
|
||||
return MeteoraDammV1InstructionKind::Unknown;
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -307,10 +302,10 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = kb_normalize_text(keyword);
|
||||
fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = normalize_text(keyword);
|
||||
for log_message in log_messages {
|
||||
let log_normalized = kb_normalize_text(log_message.as_str());
|
||||
let log_normalized = normalize_text(log_message.as_str());
|
||||
if log_normalized.contains(keyword_normalized.as_str()) {
|
||||
return true;
|
||||
}
|
||||
@@ -318,7 +313,7 @@ fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
fn normalize_text(value: &str) -> std::string::String {
|
||||
let mut normalized = std::string::String::new();
|
||||
for character in value.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
@@ -328,14 +323,14 @@ fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -351,9 +346,9 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_parse_optional_parsed_json(
|
||||
fn parse_optional_parsed_json(
|
||||
parsed_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::KbError> {
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
|
||||
let parsed_json = match parsed_json {
|
||||
Some(parsed_json) => parsed_json,
|
||||
None => return Ok(None),
|
||||
@@ -362,7 +357,7 @@ fn kb_parse_optional_parsed_json(
|
||||
match value_result {
|
||||
Ok(value) => return Ok(Some(value)),
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction parsed_json '{}': {}",
|
||||
parsed_json, error
|
||||
)));
|
||||
@@ -370,7 +365,7 @@ fn kb_parse_optional_parsed_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -378,10 +373,10 @@ fn kb_extract_string_by_candidate_keys(
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
return kb_extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
return extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys_inner(
|
||||
fn extract_string_by_candidate_keys_inner(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -397,7 +392,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -407,7 +402,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -416,7 +411,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key(
|
||||
fn value_contains_any_key(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> bool {
|
||||
@@ -424,10 +419,10 @@ fn kb_value_contains_any_key(
|
||||
Some(value) => value,
|
||||
None => return false,
|
||||
};
|
||||
return kb_value_contains_any_key_inner(value, candidate_keys);
|
||||
return value_contains_any_key_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
if let Some(object) = value.as_object() {
|
||||
for candidate_key in candidate_keys {
|
||||
if object.contains_key(*candidate_key) {
|
||||
@@ -435,7 +430,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -443,7 +438,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -451,7 +446,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
fn extract_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -461,20 +456,20 @@ fn kb_extract_account(
|
||||
return Some(accounts[index].clone());
|
||||
}
|
||||
|
||||
fn kb_infer_trade_side(log_messages: &[std::string::String]) -> crate::KbSwapTradeSide {
|
||||
if kb_log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::KbSwapTradeSide::BuyBase;
|
||||
fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide {
|
||||
if log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::SwapTradeSide::BuyBase;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::KbSwapTradeSide::SellBase;
|
||||
if log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::SwapTradeSide::SellBase;
|
||||
}
|
||||
return crate::KbSwapTradeSide::Unknown;
|
||||
return crate::SwapTradeSide::Unknown;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-damm-v1-create-1".to_string(),
|
||||
Some(890001),
|
||||
Some(1779500001),
|
||||
@@ -501,19 +496,19 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_create_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_create_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
501,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v1".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DammV1Pool111",
|
||||
"DammV1TokenA111",
|
||||
"So11111111111111111111111111111111111111112",
|
||||
crate::WSOL_MINT_ID,
|
||||
"DammV1Config111",
|
||||
"DammV1Creator111"
|
||||
])
|
||||
@@ -526,7 +521,7 @@ mod tests {
|
||||
"instruction": "initialize_pool_with_config",
|
||||
"pool": "DammV1Pool111",
|
||||
"tokenAMint": "DammV1TokenA111",
|
||||
"tokenBMint": "So11111111111111111111111111111111111111112",
|
||||
"tokenBMint": crate::WSOL_MINT_ID,
|
||||
"config": "DammV1Config111",
|
||||
"creator": "DammV1Creator111"
|
||||
}
|
||||
@@ -538,8 +533,8 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-damm-v1-swap-1".to_string(),
|
||||
Some(890002),
|
||||
Some(1779500002),
|
||||
@@ -566,21 +561,17 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_swap_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
503,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v1".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DammV1SwapPool111",
|
||||
"DammV1SwapTokenA111",
|
||||
"So11111111111111111111111111111111111111112"
|
||||
])
|
||||
.to_string(),
|
||||
serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(
|
||||
@@ -589,7 +580,7 @@ mod tests {
|
||||
"instruction": "swap",
|
||||
"pool": "DammV1SwapPool111",
|
||||
"tokenAMint": "DammV1SwapTokenA111",
|
||||
"tokenBMint": "So11111111111111111111111111111111111111112"
|
||||
"tokenBMint": crate::WSOL_MINT_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -601,7 +592,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_damm_v1_create_pool_is_detected() {
|
||||
let decoder = crate::KbMeteoraDammV1Decoder::new();
|
||||
let decoder = crate::MeteoraDammV1Decoder::new();
|
||||
let transaction = make_create_transaction();
|
||||
let instructions = vec![make_create_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -611,18 +602,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDammV1DecodedEvent::CreatePool(event) => {
|
||||
crate::MeteoraDammV1DecodedEvent::CreatePool(event) => {
|
||||
assert_eq!(event.transaction_id, 501);
|
||||
assert_eq!(event.instruction_id, 502);
|
||||
assert_eq!(event.pool_account, Some("DammV1Pool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DammV1TokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
assert!(event.used_config);
|
||||
},
|
||||
crate::KbMeteoraDammV1DecodedEvent::Swap(_) => {
|
||||
crate::MeteoraDammV1DecodedEvent::Swap(_) => {
|
||||
panic!("unexpected swap event")
|
||||
},
|
||||
}
|
||||
@@ -630,7 +618,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_damm_v1_swap_is_detected() {
|
||||
let decoder = crate::KbMeteoraDammV1Decoder::new();
|
||||
let decoder = crate::MeteoraDammV1Decoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let instructions = vec![make_swap_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -640,17 +628,14 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDammV1DecodedEvent::Swap(event) => {
|
||||
crate::MeteoraDammV1DecodedEvent::Swap(event) => {
|
||||
assert_eq!(event.transaction_id, 503);
|
||||
assert_eq!(event.instruction_id, 504);
|
||||
assert_eq!(event.pool_account, Some("DammV1SwapPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DammV1SwapTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
},
|
||||
crate::KbMeteoraDammV1DecodedEvent::CreatePool(_) => {
|
||||
crate::MeteoraDammV1DecodedEvent::CreatePool(_) => {
|
||||
panic!("unexpected create event")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
//! Meteora DAMM v2 transaction decoder.
|
||||
|
||||
/// Meteora DAMM v2 program id.
|
||||
pub const KB_METEORA_DAMM_V2_PROGRAM_ID: &str = "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG";
|
||||
|
||||
/// Decoded Meteora DAMM v2 create-pool event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbMeteoraDammV2CreatePoolDecoded {
|
||||
pub struct MeteoraDammV2CreatePoolDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -34,7 +31,7 @@ pub struct KbMeteoraDammV2CreatePoolDecoded {
|
||||
|
||||
/// Decoded Meteora DAMM v2 swap event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbMeteoraDammV2SwapDecoded {
|
||||
pub struct MeteoraDammV2SwapDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -44,7 +41,7 @@ pub struct KbMeteoraDammV2SwapDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side relative to normalized base.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Optional pool account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional token A mint.
|
||||
@@ -59,19 +56,19 @@ pub struct KbMeteoraDammV2SwapDecoded {
|
||||
|
||||
/// Decoded Meteora DAMM v2 event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbMeteoraDammV2DecodedEvent {
|
||||
pub enum MeteoraDammV2DecodedEvent {
|
||||
/// Pool creation.
|
||||
CreatePool(KbMeteoraDammV2CreatePoolDecoded),
|
||||
CreatePool(MeteoraDammV2CreatePoolDecoded),
|
||||
/// Swap / swap2.
|
||||
Swap(KbMeteoraDammV2SwapDecoded),
|
||||
Swap(MeteoraDammV2SwapDecoded),
|
||||
}
|
||||
|
||||
/// Meteora DAMM v2 decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbMeteoraDammV2Decoder;
|
||||
pub struct MeteoraDammV2Decoder;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KbMeteoraDammV2InstructionKind {
|
||||
enum MeteoraDammV2InstructionKind {
|
||||
CreatePoolStatic,
|
||||
CreatePoolDynamic,
|
||||
CreatePoolCustomizable,
|
||||
@@ -79,7 +76,7 @@ enum KbMeteoraDammV2InstructionKind {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl KbMeteoraDammV2Decoder {
|
||||
impl MeteoraDammV2Decoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -88,14 +85,14 @@ impl KbMeteoraDammV2Decoder {
|
||||
/// Decodes one projected transaction into zero or more Meteora DAMM v2 events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbMeteoraDammV2DecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::MeteoraDammV2DecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -106,13 +103,13 @@ impl KbMeteoraDammV2Decoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -123,7 +120,7 @@ impl KbMeteoraDammV2Decoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_METEORA_DAMM_V2_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::METEORA_DAMM_V2_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -131,52 +128,50 @@ impl KbMeteoraDammV2Decoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let parsed_json_result =
|
||||
kb_parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json = match parsed_json_result {
|
||||
Ok(parsed_json) => parsed_json,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_kind =
|
||||
kb_classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = kb_extract_string_by_candidate_keys(
|
||||
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["pool", "poolAddress", "poolAccount", "poolState", "cpAmm"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 0));
|
||||
let token_a_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 0));
|
||||
let token_a_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenAMint", "mintA", "baseMint", "token0Mint", "mint0"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 1));
|
||||
let token_b_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 1));
|
||||
let token_b_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 2));
|
||||
let config_account = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 2));
|
||||
let config_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["staticConfig", "dynamicConfig", "config", "poolConfig"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 3));
|
||||
let creator = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 3));
|
||||
let creator = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["creator", "payer", "user", "owner"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 4));
|
||||
if instruction_kind == KbMeteoraDammV2InstructionKind::CreatePoolStatic
|
||||
|| instruction_kind == KbMeteoraDammV2InstructionKind::CreatePoolDynamic
|
||||
|| instruction_kind == KbMeteoraDammV2InstructionKind::CreatePoolCustomizable
|
||||
.or_else(|| return extract_account(&accounts, 4));
|
||||
if instruction_kind == MeteoraDammV2InstructionKind::CreatePoolStatic
|
||||
|| instruction_kind == MeteoraDammV2InstructionKind::CreatePoolDynamic
|
||||
|| instruction_kind == MeteoraDammV2InstructionKind::CreatePoolCustomizable
|
||||
{
|
||||
let create_kind = match instruction_kind {
|
||||
KbMeteoraDammV2InstructionKind::CreatePoolStatic => "static".to_string(),
|
||||
KbMeteoraDammV2InstructionKind::CreatePoolDynamic => "dynamic".to_string(),
|
||||
KbMeteoraDammV2InstructionKind::CreatePoolCustomizable => {
|
||||
MeteoraDammV2InstructionKind::CreatePoolStatic => "static".to_string(),
|
||||
MeteoraDammV2InstructionKind::CreatePoolDynamic => "dynamic".to_string(),
|
||||
MeteoraDammV2InstructionKind::CreatePoolCustomizable => {
|
||||
"customizable".to_string()
|
||||
},
|
||||
_ => "unknown".to_string(),
|
||||
@@ -197,8 +192,8 @@ impl KbMeteoraDammV2Decoder {
|
||||
"configAccount": config_account,
|
||||
"creator": creator
|
||||
});
|
||||
decoded_events.push(crate::KbMeteoraDammV2DecodedEvent::CreatePool(
|
||||
crate::KbMeteoraDammV2CreatePoolDecoded {
|
||||
decoded_events.push(crate::MeteoraDammV2DecodedEvent::CreatePool(
|
||||
crate::MeteoraDammV2CreatePoolDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -214,10 +209,10 @@ impl KbMeteoraDammV2Decoder {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if instruction_kind == KbMeteoraDammV2InstructionKind::Swap {
|
||||
let used_swap2 = kb_log_messages_contain_keyword(&log_messages, "swap2")
|
||||
|| kb_value_contains_any_key(parsed_json.as_ref(), &["swap2", "isSwap2"]);
|
||||
let trade_side = kb_infer_trade_side(&log_messages);
|
||||
if instruction_kind == MeteoraDammV2InstructionKind::Swap {
|
||||
let used_swap2 = log_messages_contain_keyword(&log_messages, "swap2")
|
||||
|| value_contains_any_key(parsed_json.as_ref(), &["swap2", "isSwap2"]);
|
||||
let trade_side = infer_trade_side(&log_messages);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "meteora_damm_v2",
|
||||
"eventKind": "swap",
|
||||
@@ -233,8 +228,8 @@ impl KbMeteoraDammV2Decoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"tradeSide": format!("{:?}", trade_side)
|
||||
});
|
||||
decoded_events.push(crate::KbMeteoraDammV2DecodedEvent::Swap(
|
||||
crate::KbMeteoraDammV2SwapDecoded {
|
||||
decoded_events.push(crate::MeteoraDammV2DecodedEvent::Swap(
|
||||
crate::MeteoraDammV2SwapDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -253,7 +248,7 @@ impl KbMeteoraDammV2Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -281,65 +276,65 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_classify_instruction_kind(
|
||||
fn classify_instruction_kind(
|
||||
parsed_json: std::option::Option<&serde_json::Value>,
|
||||
log_messages: &[std::string::String],
|
||||
) -> KbMeteoraDammV2InstructionKind {
|
||||
let parsed_instruction_name = kb_extract_string_by_candidate_keys(
|
||||
) -> MeteoraDammV2InstructionKind {
|
||||
let parsed_instruction_name = extract_string_by_candidate_keys(
|
||||
parsed_json,
|
||||
&["instruction", "instructionName", "type", "name"],
|
||||
);
|
||||
if let Some(parsed_instruction_name) = parsed_instruction_name {
|
||||
let normalized = kb_normalize_text(parsed_instruction_name.as_str());
|
||||
let normalized = normalize_text(parsed_instruction_name.as_str());
|
||||
if normalized.contains("initializepoolwithdynamicconfig") {
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolDynamic;
|
||||
return MeteoraDammV2InstructionKind::CreatePoolDynamic;
|
||||
}
|
||||
if normalized.contains("initializecustomizablepool") {
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolCustomizable;
|
||||
return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
|
||||
}
|
||||
if normalized.contains("initializepool") {
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolStatic;
|
||||
return MeteoraDammV2InstructionKind::CreatePoolStatic;
|
||||
}
|
||||
if normalized == "swap" || normalized == "swap2" {
|
||||
return KbMeteoraDammV2InstructionKind::Swap;
|
||||
return MeteoraDammV2InstructionKind::Swap;
|
||||
}
|
||||
}
|
||||
if kb_value_contains_any_key(parsed_json, &["dynamicConfig"]) {
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolDynamic;
|
||||
if value_contains_any_key(parsed_json, &["dynamicConfig"]) {
|
||||
return MeteoraDammV2InstructionKind::CreatePoolDynamic;
|
||||
}
|
||||
if kb_value_contains_any_key(parsed_json, &["staticConfig"]) {
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolStatic;
|
||||
if value_contains_any_key(parsed_json, &["staticConfig"]) {
|
||||
return MeteoraDammV2InstructionKind::CreatePoolStatic;
|
||||
}
|
||||
if kb_value_contains_any_key(parsed_json, &["poolActivationPoint", "isCustomizablePool"]) {
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolCustomizable;
|
||||
if value_contains_any_key(parsed_json, &["poolActivationPoint", "isCustomizablePool"]) {
|
||||
return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "initialize_pool_with_dynamic_config")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepoolwithdynamicconfig")
|
||||
if log_messages_contain_keyword(log_messages, "initialize_pool_with_dynamic_config")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepoolwithdynamicconfig")
|
||||
{
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolDynamic;
|
||||
return MeteoraDammV2InstructionKind::CreatePoolDynamic;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "initialize_customizable_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializecustomizablepool")
|
||||
if log_messages_contain_keyword(log_messages, "initialize_customizable_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "initializecustomizablepool")
|
||||
{
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolCustomizable;
|
||||
return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepool")
|
||||
if log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepool")
|
||||
{
|
||||
return KbMeteoraDammV2InstructionKind::CreatePoolStatic;
|
||||
return MeteoraDammV2InstructionKind::CreatePoolStatic;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "swap2")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "swap")
|
||||
if log_messages_contain_keyword(log_messages, "swap2")
|
||||
|| log_messages_contain_keyword(log_messages, "swap")
|
||||
{
|
||||
return KbMeteoraDammV2InstructionKind::Swap;
|
||||
return MeteoraDammV2InstructionKind::Swap;
|
||||
}
|
||||
return KbMeteoraDammV2InstructionKind::Unknown;
|
||||
return MeteoraDammV2InstructionKind::Unknown;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = kb_normalize_text(keyword);
|
||||
fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = normalize_text(keyword);
|
||||
for log_message in log_messages {
|
||||
let log_normalized = kb_normalize_text(log_message.as_str());
|
||||
let log_normalized = normalize_text(log_message.as_str());
|
||||
if log_normalized.contains(keyword_normalized.as_str()) {
|
||||
return true;
|
||||
}
|
||||
@@ -347,9 +342,9 @@ fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
fn normalize_text(normalize_text: &str) -> std::string::String {
|
||||
let mut normalized = std::string::String::new();
|
||||
for character in value.chars() {
|
||||
for character in normalize_text.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
normalized.push(character.to_ascii_lowercase());
|
||||
}
|
||||
@@ -357,14 +352,14 @@ fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -380,9 +375,9 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_parse_optional_parsed_json(
|
||||
fn parse_optional_parsed_json(
|
||||
parsed_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::KbError> {
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
|
||||
let parsed_json = match parsed_json {
|
||||
Some(parsed_json) => parsed_json,
|
||||
None => return Ok(None),
|
||||
@@ -391,7 +386,7 @@ fn kb_parse_optional_parsed_json(
|
||||
match value_result {
|
||||
Ok(value) => return Ok(Some(value)),
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction parsed_json '{}': {}",
|
||||
parsed_json, error
|
||||
)));
|
||||
@@ -399,7 +394,7 @@ fn kb_parse_optional_parsed_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -407,10 +402,10 @@ fn kb_extract_string_by_candidate_keys(
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
return kb_extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
return extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys_inner(
|
||||
fn extract_string_by_candidate_keys_inner(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -426,7 +421,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -436,7 +431,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -445,7 +440,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key(
|
||||
fn value_contains_any_key(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> bool {
|
||||
@@ -453,10 +448,10 @@ fn kb_value_contains_any_key(
|
||||
Some(value) => value,
|
||||
None => return false,
|
||||
};
|
||||
return kb_value_contains_any_key_inner(value, candidate_keys);
|
||||
return value_contains_any_key_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
if let Some(object) = value.as_object() {
|
||||
for candidate_key in candidate_keys {
|
||||
if object.contains_key(*candidate_key) {
|
||||
@@ -464,7 +459,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -472,7 +467,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -480,7 +475,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
fn extract_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -490,20 +485,20 @@ fn kb_extract_account(
|
||||
return Some(accounts[index].clone());
|
||||
}
|
||||
|
||||
fn kb_infer_trade_side(log_messages: &[std::string::String]) -> crate::KbSwapTradeSide {
|
||||
if kb_log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::KbSwapTradeSide::BuyBase;
|
||||
fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide {
|
||||
if log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::SwapTradeSide::BuyBase;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::KbSwapTradeSide::SellBase;
|
||||
if log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::SwapTradeSide::SellBase;
|
||||
}
|
||||
return crate::KbSwapTradeSide::Unknown;
|
||||
return crate::SwapTradeSide::Unknown;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-damm-v2-create-1".to_string(),
|
||||
Some(889001),
|
||||
Some(1779400001),
|
||||
@@ -530,19 +525,19 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_create_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_create_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
401,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v2".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DammV2Pool111",
|
||||
"DammV2TokenA111",
|
||||
"So11111111111111111111111111111111111111112",
|
||||
crate::WSOL_MINT_ID,
|
||||
"DammV2Config111",
|
||||
"DammV2Creator111"
|
||||
])
|
||||
@@ -555,7 +550,7 @@ mod tests {
|
||||
"instruction": "initialize_customizable_pool",
|
||||
"pool": "DammV2Pool111",
|
||||
"tokenAMint": "DammV2TokenA111",
|
||||
"tokenBMint": "So11111111111111111111111111111111111111112",
|
||||
"tokenBMint": crate::WSOL_MINT_ID,
|
||||
"creator": "DammV2Creator111",
|
||||
"isCustomizablePool": true
|
||||
}
|
||||
@@ -567,8 +562,8 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-damm-v2-swap-1".to_string(),
|
||||
Some(889002),
|
||||
Some(1779400002),
|
||||
@@ -595,21 +590,17 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_swap_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
403,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
|
||||
Some("meteora-damm-v2".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DammV2SwapPool111",
|
||||
"DammV2SwapTokenA111",
|
||||
"So11111111111111111111111111111111111111112"
|
||||
])
|
||||
.to_string(),
|
||||
serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(
|
||||
@@ -618,7 +609,7 @@ mod tests {
|
||||
"instruction": "swap2",
|
||||
"pool": "DammV2SwapPool111",
|
||||
"tokenAMint": "DammV2SwapTokenA111",
|
||||
"tokenBMint": "So11111111111111111111111111111111111111112"
|
||||
"tokenBMint": crate::WSOL_MINT_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -630,7 +621,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_damm_v2_create_pool_is_detected() {
|
||||
let decoder = crate::KbMeteoraDammV2Decoder::new();
|
||||
let decoder = crate::MeteoraDammV2Decoder::new();
|
||||
let transaction = make_create_transaction();
|
||||
let instructions = vec![make_create_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -640,18 +631,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDammV2DecodedEvent::CreatePool(event) => {
|
||||
crate::MeteoraDammV2DecodedEvent::CreatePool(event) => {
|
||||
assert_eq!(event.transaction_id, 401);
|
||||
assert_eq!(event.instruction_id, 402);
|
||||
assert_eq!(event.pool_account, Some("DammV2Pool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DammV2TokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
assert_eq!(event.create_kind, "customizable".to_string());
|
||||
},
|
||||
crate::KbMeteoraDammV2DecodedEvent::Swap(_) => {
|
||||
crate::MeteoraDammV2DecodedEvent::Swap(_) => {
|
||||
panic!("unexpected swap event")
|
||||
},
|
||||
}
|
||||
@@ -659,7 +647,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_damm_v2_swap_is_detected() {
|
||||
let decoder = crate::KbMeteoraDammV2Decoder::new();
|
||||
let decoder = crate::MeteoraDammV2Decoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let instructions = vec![make_swap_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -669,18 +657,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDammV2DecodedEvent::Swap(event) => {
|
||||
crate::MeteoraDammV2DecodedEvent::Swap(event) => {
|
||||
assert_eq!(event.transaction_id, 403);
|
||||
assert_eq!(event.instruction_id, 404);
|
||||
assert_eq!(event.pool_account, Some("DammV2SwapPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DammV2SwapTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
assert!(event.used_swap2);
|
||||
},
|
||||
crate::KbMeteoraDammV2DecodedEvent::CreatePool(_) => {
|
||||
crate::MeteoraDammV2DecodedEvent::CreatePool(_) => {
|
||||
panic!("unexpected create event")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
//! Meteora Dynamic Bonding Curve (DBC) transaction decoder.
|
||||
|
||||
/// Meteora DBC program id.
|
||||
pub const KB_METEORA_DBC_PROGRAM_ID: &str = "dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN";
|
||||
|
||||
/// Decoded Meteora DBC create-pool event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbMeteoraDbcCreatePoolDecoded {
|
||||
pub struct MeteoraDbcCreatePoolDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -32,7 +29,7 @@ pub struct KbMeteoraDbcCreatePoolDecoded {
|
||||
|
||||
/// Decoded Meteora DBC swap event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbMeteoraDbcSwapDecoded {
|
||||
pub struct MeteoraDbcSwapDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -42,7 +39,7 @@ pub struct KbMeteoraDbcSwapDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side relative to normalized base.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Optional pool account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional base mint.
|
||||
@@ -55,15 +52,15 @@ pub struct KbMeteoraDbcSwapDecoded {
|
||||
|
||||
/// Decoded Meteora DBC event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbMeteoraDbcDecodedEvent {
|
||||
pub enum MeteoraDbcDecodedEvent {
|
||||
/// Create pool / launch pool.
|
||||
CreatePool(KbMeteoraDbcCreatePoolDecoded),
|
||||
CreatePool(MeteoraDbcCreatePoolDecoded),
|
||||
/// Swap / swap2.
|
||||
Swap(KbMeteoraDbcSwapDecoded),
|
||||
Swap(MeteoraDbcSwapDecoded),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KbMeteoraDbcInstructionKind {
|
||||
enum MeteoraDbcInstructionKind {
|
||||
CreatePool,
|
||||
Swap,
|
||||
Unknown,
|
||||
@@ -71,9 +68,9 @@ enum KbMeteoraDbcInstructionKind {
|
||||
|
||||
/// Meteora DBC decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbMeteoraDbcDecoder;
|
||||
pub struct MeteoraDbcDecoder;
|
||||
|
||||
impl KbMeteoraDbcDecoder {
|
||||
impl MeteoraDbcDecoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -82,14 +79,14 @@ impl KbMeteoraDbcDecoder {
|
||||
/// Decodes one projected transaction into zero or more Meteora DBC events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbMeteoraDbcDecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::MeteoraDbcDecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -100,13 +97,13 @@ impl KbMeteoraDbcDecoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -117,7 +114,7 @@ impl KbMeteoraDbcDecoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_METEORA_DBC_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::METEORA_DBC_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -125,45 +122,43 @@ impl KbMeteoraDbcDecoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let parsed_json_result =
|
||||
kb_parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json = match parsed_json_result {
|
||||
Ok(parsed_json) => parsed_json,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_kind =
|
||||
kb_classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = kb_extract_string_by_candidate_keys(
|
||||
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["pool", "poolAccount", "poolState", "virtualPool", "poolKey"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 0));
|
||||
let token_a_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 0));
|
||||
let token_a_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["baseMint", "tokenAMint", "mintA", "token0Mint", "mint0"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 1));
|
||||
let token_b_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 1));
|
||||
let token_b_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["quoteMint", "tokenBMint", "mintB", "token1Mint", "mint1"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 2));
|
||||
let config_account = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 2));
|
||||
let config_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["poolConfig", "config", "dbcConfig", "curveConfig"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 3));
|
||||
let creator = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 3));
|
||||
let creator = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["creator", "poolCreator", "owner", "user"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 4));
|
||||
if instruction_kind == KbMeteoraDbcInstructionKind::CreatePool {
|
||||
.or_else(|| return extract_account(&accounts, 4));
|
||||
if instruction_kind == MeteoraDbcInstructionKind::CreatePool {
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "meteora_dbc",
|
||||
"eventKind": "create_pool",
|
||||
@@ -180,8 +175,8 @@ impl KbMeteoraDbcDecoder {
|
||||
"configAccount": config_account,
|
||||
"creator": creator
|
||||
});
|
||||
decoded_events.push(crate::KbMeteoraDbcDecodedEvent::CreatePool(
|
||||
crate::KbMeteoraDbcCreatePoolDecoded {
|
||||
decoded_events.push(crate::MeteoraDbcDecodedEvent::CreatePool(
|
||||
crate::MeteoraDbcCreatePoolDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -196,8 +191,8 @@ impl KbMeteoraDbcDecoder {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if instruction_kind == KbMeteoraDbcInstructionKind::Swap {
|
||||
let trade_side = kb_infer_trade_side(&log_messages);
|
||||
if instruction_kind == MeteoraDbcInstructionKind::Swap {
|
||||
let trade_side = infer_trade_side(&log_messages);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "meteora_dbc",
|
||||
"eventKind": "swap",
|
||||
@@ -213,8 +208,8 @@ impl KbMeteoraDbcDecoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"tradeSide": format!("{:?}", trade_side)
|
||||
});
|
||||
decoded_events.push(crate::KbMeteoraDbcDecodedEvent::Swap(
|
||||
crate::KbMeteoraDbcSwapDecoded {
|
||||
decoded_events.push(crate::MeteoraDbcDecodedEvent::Swap(
|
||||
crate::MeteoraDbcSwapDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -232,7 +227,7 @@ impl KbMeteoraDbcDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -260,22 +255,22 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_any_keyword(
|
||||
fn log_messages_contain_any_keyword(
|
||||
log_messages: &[std::string::String],
|
||||
keywords: &[&str],
|
||||
) -> bool {
|
||||
for keyword in keywords {
|
||||
if kb_log_messages_contain_keyword(log_messages, keyword) {
|
||||
if log_messages_contain_keyword(log_messages, keyword) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = kb_normalize_log_text(keyword);
|
||||
fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = normalize_log_text(keyword);
|
||||
for log_message in log_messages {
|
||||
let log_normalized = kb_normalize_log_text(log_message.as_str());
|
||||
let log_normalized = normalize_log_text(log_message.as_str());
|
||||
if log_normalized.contains(keyword_normalized.as_str()) {
|
||||
return true;
|
||||
}
|
||||
@@ -283,7 +278,7 @@ fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_normalize_log_text(value: &str) -> std::string::String {
|
||||
fn normalize_log_text(value: &str) -> std::string::String {
|
||||
let mut normalized = std::string::String::new();
|
||||
for character in value.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
@@ -293,14 +288,14 @@ fn kb_normalize_log_text(value: &str) -> std::string::String {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -316,9 +311,9 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_parse_optional_parsed_json(
|
||||
fn parse_optional_parsed_json(
|
||||
parsed_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::KbError> {
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
|
||||
let parsed_json = match parsed_json {
|
||||
Some(parsed_json) => parsed_json,
|
||||
None => return Ok(None),
|
||||
@@ -327,7 +322,7 @@ fn kb_parse_optional_parsed_json(
|
||||
match value_result {
|
||||
Ok(value) => return Ok(Some(value)),
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction parsed_json '{}': {}",
|
||||
parsed_json, error
|
||||
)));
|
||||
@@ -335,7 +330,7 @@ fn kb_parse_optional_parsed_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -343,10 +338,10 @@ fn kb_extract_string_by_candidate_keys(
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
return kb_extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
return extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys_inner(
|
||||
fn extract_string_by_candidate_keys_inner(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -362,7 +357,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -372,7 +367,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -381,7 +376,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key(
|
||||
fn value_contains_any_key(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> bool {
|
||||
@@ -389,10 +384,10 @@ fn kb_value_contains_any_key(
|
||||
Some(value) => value,
|
||||
None => return false,
|
||||
};
|
||||
return kb_value_contains_any_key_inner(value, candidate_keys);
|
||||
return value_contains_any_key_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
if let Some(object) = value.as_object() {
|
||||
for candidate_key in candidate_keys {
|
||||
if object.contains_key(*candidate_key) {
|
||||
@@ -400,7 +395,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -408,7 +403,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -416,7 +411,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
fn extract_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -426,59 +421,59 @@ fn kb_extract_account(
|
||||
return Some(accounts[index].clone());
|
||||
}
|
||||
|
||||
fn kb_infer_trade_side(log_messages: &[std::string::String]) -> crate::KbSwapTradeSide {
|
||||
if kb_log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::KbSwapTradeSide::BuyBase;
|
||||
fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide {
|
||||
if log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::SwapTradeSide::BuyBase;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::KbSwapTradeSide::SellBase;
|
||||
if log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::SwapTradeSide::SellBase;
|
||||
}
|
||||
return crate::KbSwapTradeSide::Unknown;
|
||||
return crate::SwapTradeSide::Unknown;
|
||||
}
|
||||
|
||||
fn kb_classify_instruction_kind(
|
||||
fn classify_instruction_kind(
|
||||
parsed_json: std::option::Option<&serde_json::Value>,
|
||||
log_messages: &[std::string::String],
|
||||
) -> KbMeteoraDbcInstructionKind {
|
||||
let parsed_instruction_name = kb_extract_string_by_candidate_keys(
|
||||
) -> MeteoraDbcInstructionKind {
|
||||
let parsed_instruction_name = extract_string_by_candidate_keys(
|
||||
parsed_json,
|
||||
&["instruction", "instructionName", "type", "name"],
|
||||
);
|
||||
if let Some(parsed_instruction_name) = parsed_instruction_name {
|
||||
let normalized = kb_normalize_log_text(parsed_instruction_name.as_str());
|
||||
let normalized = normalize_log_text(parsed_instruction_name.as_str());
|
||||
if normalized.contains("createpool")
|
||||
|| normalized.contains("initializepool")
|
||||
|| normalized.contains("launchpool")
|
||||
{
|
||||
return KbMeteoraDbcInstructionKind::CreatePool;
|
||||
return MeteoraDbcInstructionKind::CreatePool;
|
||||
}
|
||||
if normalized == "swap" || normalized == "swap2" {
|
||||
return KbMeteoraDbcInstructionKind::Swap;
|
||||
return MeteoraDbcInstructionKind::Swap;
|
||||
}
|
||||
}
|
||||
let has_create_config = kb_value_contains_any_key(
|
||||
let has_create_config = value_contains_any_key(
|
||||
parsed_json,
|
||||
&["poolConfig", "migrationQuoteThreshold", "curveConfig", "dbcConfig"],
|
||||
);
|
||||
if has_create_config {
|
||||
return KbMeteoraDbcInstructionKind::CreatePool;
|
||||
return MeteoraDbcInstructionKind::CreatePool;
|
||||
}
|
||||
if kb_log_messages_contain_any_keyword(
|
||||
if log_messages_contain_any_keyword(
|
||||
log_messages,
|
||||
&["create_pool", "createpool", "initialize_pool", "initializepool", "launch_pool"],
|
||||
) {
|
||||
return KbMeteoraDbcInstructionKind::CreatePool;
|
||||
return MeteoraDbcInstructionKind::CreatePool;
|
||||
}
|
||||
if kb_log_messages_contain_any_keyword(log_messages, &["swap2", "swap"]) {
|
||||
return KbMeteoraDbcInstructionKind::Swap;
|
||||
if log_messages_contain_any_keyword(log_messages, &["swap2", "swap"]) {
|
||||
return MeteoraDbcInstructionKind::Swap;
|
||||
}
|
||||
return KbMeteoraDbcInstructionKind::Unknown;
|
||||
return MeteoraDbcInstructionKind::Unknown;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-dbc-create-1".to_string(),
|
||||
Some(888001),
|
||||
Some(1779300001),
|
||||
@@ -505,19 +500,19 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_create_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_create_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
301,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_METEORA_DBC_PROGRAM_ID.to_string()),
|
||||
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dbc".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DbcPool111",
|
||||
"DbcTokenA111",
|
||||
"So11111111111111111111111111111111111111112",
|
||||
crate::WSOL_MINT_ID,
|
||||
"DbcConfig111",
|
||||
"DbcCreator111"
|
||||
])
|
||||
@@ -529,7 +524,7 @@ mod tests {
|
||||
"info": {
|
||||
"pool": "DbcPool111",
|
||||
"baseMint": "DbcTokenA111",
|
||||
"quoteMint": "So11111111111111111111111111111111111111112",
|
||||
"quoteMint": crate::WSOL_MINT_ID,
|
||||
"poolConfig": "DbcConfig111",
|
||||
"creator": "DbcCreator111"
|
||||
}
|
||||
@@ -541,8 +536,8 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-meteora-dbc-swap-1".to_string(),
|
||||
Some(888002),
|
||||
Some(1779300002),
|
||||
@@ -569,21 +564,17 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_swap_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
303,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_METEORA_DBC_PROGRAM_ID.to_string()),
|
||||
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
|
||||
Some("meteora-dbc".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"DbcPoolSwap111",
|
||||
"DbcSwapTokenA111",
|
||||
"So11111111111111111111111111111111111111112"
|
||||
])
|
||||
.to_string(),
|
||||
serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(
|
||||
@@ -591,7 +582,7 @@ mod tests {
|
||||
"info": {
|
||||
"pool": "DbcPoolSwap111",
|
||||
"baseMint": "DbcSwapTokenA111",
|
||||
"quoteMint": "So11111111111111111111111111111111111111112"
|
||||
"quoteMint": crate::WSOL_MINT_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -603,7 +594,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_dbc_create_pool_is_detected() {
|
||||
let decoder = crate::KbMeteoraDbcDecoder::new();
|
||||
let decoder = crate::MeteoraDbcDecoder::new();
|
||||
let transaction = make_create_transaction();
|
||||
let instructions = vec![make_create_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -613,18 +604,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDbcDecodedEvent::CreatePool(event) => {
|
||||
crate::MeteoraDbcDecodedEvent::CreatePool(event) => {
|
||||
assert_eq!(event.transaction_id, 301);
|
||||
assert_eq!(event.instruction_id, 302);
|
||||
assert_eq!(event.pool_account, Some("DbcPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DbcTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
assert_eq!(event.config_account, Some("DbcConfig111".to_string()));
|
||||
},
|
||||
crate::KbMeteoraDbcDecodedEvent::Swap(_) => {
|
||||
crate::MeteoraDbcDecodedEvent::Swap(_) => {
|
||||
panic!("unexpected swap event")
|
||||
},
|
||||
}
|
||||
@@ -632,7 +620,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_dbc_swap_is_detected() {
|
||||
let decoder = crate::KbMeteoraDbcDecoder::new();
|
||||
let decoder = crate::MeteoraDbcDecoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let instructions = vec![make_swap_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -642,17 +630,14 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDbcDecodedEvent::Swap(event) => {
|
||||
crate::MeteoraDbcDecodedEvent::Swap(event) => {
|
||||
assert_eq!(event.transaction_id, 303);
|
||||
assert_eq!(event.instruction_id, 304);
|
||||
assert_eq!(event.pool_account, Some("DbcPoolSwap111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("DbcSwapTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
},
|
||||
crate::KbMeteoraDbcDecodedEvent::CreatePool(_) => {
|
||||
crate::MeteoraDbcDecodedEvent::CreatePool(_) => {
|
||||
panic!("unexpected create event")
|
||||
},
|
||||
}
|
||||
@@ -660,13 +645,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_dbc_quote_mint_alone_does_not_trigger_create_pool() {
|
||||
let decoder = crate::KbMeteoraDbcDecoder::new();
|
||||
let decoder = crate::MeteoraDbcDecoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let mut instruction = make_swap_instruction();
|
||||
instruction.parsed_json = Some(
|
||||
serde_json::json!({
|
||||
"info": {
|
||||
"quoteMint": "So11111111111111111111111111111111111111112"
|
||||
"quoteMint": crate::WSOL_MINT_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -678,8 +663,8 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDbcDecodedEvent::Swap(_) => {},
|
||||
crate::KbMeteoraDbcDecodedEvent::CreatePool(_) => {
|
||||
crate::MeteoraDbcDecodedEvent::Swap(_) => {},
|
||||
crate::MeteoraDbcDecodedEvent::CreatePool(_) => {
|
||||
panic!("unexpected create event")
|
||||
},
|
||||
}
|
||||
@@ -687,7 +672,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn meteora_dbc_pool_config_triggers_create_pool() {
|
||||
let decoder = crate::KbMeteoraDbcDecoder::new();
|
||||
let decoder = crate::MeteoraDbcDecoder::new();
|
||||
let transaction = make_create_transaction();
|
||||
let instruction = make_create_instruction();
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
|
||||
@@ -697,8 +682,8 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbMeteoraDbcDecodedEvent::CreatePool(_) => {},
|
||||
crate::KbMeteoraDbcDecodedEvent::Swap(_) => {
|
||||
crate::MeteoraDbcDecodedEvent::CreatePool(_) => {},
|
||||
crate::MeteoraDbcDecodedEvent::Swap(_) => {
|
||||
panic!("unexpected swap event")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
//! Orca Whirlpools transaction decoder.
|
||||
|
||||
/// Orca Whirlpools program id.
|
||||
pub const KB_ORCA_WHIRLPOOLS_PROGRAM_ID: &str = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc";
|
||||
|
||||
/// Decoded Orca Whirlpools create-pool event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbOrcaWhirlpoolsCreatePoolDecoded {
|
||||
pub struct OrcaWhirlpoolsCreatePoolDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -34,7 +31,7 @@ pub struct KbOrcaWhirlpoolsCreatePoolDecoded {
|
||||
|
||||
/// Decoded Orca Whirlpools swap event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbOrcaWhirlpoolsSwapDecoded {
|
||||
pub struct OrcaWhirlpoolsSwapDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -44,7 +41,7 @@ pub struct KbOrcaWhirlpoolsSwapDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side relative to normalized base.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Optional whirlpool account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional token A mint.
|
||||
@@ -59,19 +56,19 @@ pub struct KbOrcaWhirlpoolsSwapDecoded {
|
||||
|
||||
/// Decoded Orca Whirlpools event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbOrcaWhirlpoolsDecodedEvent {
|
||||
pub enum OrcaWhirlpoolsDecodedEvent {
|
||||
/// Pool creation.
|
||||
CreatePool(KbOrcaWhirlpoolsCreatePoolDecoded),
|
||||
CreatePool(OrcaWhirlpoolsCreatePoolDecoded),
|
||||
/// Swap / swap_v2.
|
||||
Swap(KbOrcaWhirlpoolsSwapDecoded),
|
||||
Swap(OrcaWhirlpoolsSwapDecoded),
|
||||
}
|
||||
|
||||
/// Orca Whirlpools decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbOrcaWhirlpoolsDecoder;
|
||||
pub struct OrcaWhirlpoolsDecoder;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KbOrcaWhirlpoolsInstructionKind {
|
||||
enum OrcaWhirlpoolsInstructionKind {
|
||||
InitializePool,
|
||||
InitializePoolV2,
|
||||
Swap,
|
||||
@@ -79,7 +76,7 @@ enum KbOrcaWhirlpoolsInstructionKind {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl KbOrcaWhirlpoolsDecoder {
|
||||
impl OrcaWhirlpoolsDecoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -88,14 +85,14 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
/// Decodes one projected transaction into zero or more Orca Whirlpools events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbOrcaWhirlpoolsDecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::OrcaWhirlpoolsDecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -106,13 +103,13 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -123,7 +120,7 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_ORCA_WHIRLPOOLS_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::ORCA_WHIRLPOOLS_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -131,48 +128,46 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let parsed_json_result =
|
||||
kb_parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json = match parsed_json_result {
|
||||
Ok(parsed_json) => parsed_json,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_kind =
|
||||
kb_classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = kb_extract_string_by_candidate_keys(
|
||||
let instruction_kind = classify_instruction_kind(parsed_json.as_ref(), &log_messages);
|
||||
let pool_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["whirlpool", "pool", "poolAddress", "poolAccount", "whirlpoolAddress"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 0));
|
||||
let token_a_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 0));
|
||||
let token_a_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenMintA", "tokenAMint", "mintA", "baseMint", "token0Mint", "mint0"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 1));
|
||||
let token_b_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 1));
|
||||
let token_b_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenMintB", "tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 2));
|
||||
let config_account = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 2));
|
||||
let config_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["whirlpoolsConfig", "config", "configAccount", "whirlpoolConfig"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 3));
|
||||
let creator = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 3));
|
||||
let creator = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["funder", "creator", "payer", "user", "owner"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 4));
|
||||
if instruction_kind == KbOrcaWhirlpoolsInstructionKind::InitializePool
|
||||
|| instruction_kind == KbOrcaWhirlpoolsInstructionKind::InitializePoolV2
|
||||
.or_else(|| return extract_account(&accounts, 4));
|
||||
if instruction_kind == OrcaWhirlpoolsInstructionKind::InitializePool
|
||||
|| instruction_kind == OrcaWhirlpoolsInstructionKind::InitializePoolV2
|
||||
{
|
||||
let used_v2 = instruction_kind == KbOrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
let used_v2 = instruction_kind == OrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "orca_whirlpools",
|
||||
"eventKind": "create_pool",
|
||||
@@ -189,8 +184,8 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
"configAccount": config_account,
|
||||
"creator": creator
|
||||
});
|
||||
decoded_events.push(crate::KbOrcaWhirlpoolsDecodedEvent::CreatePool(
|
||||
crate::KbOrcaWhirlpoolsCreatePoolDecoded {
|
||||
decoded_events.push(crate::OrcaWhirlpoolsDecodedEvent::CreatePool(
|
||||
crate::OrcaWhirlpoolsCreatePoolDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -206,11 +201,11 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if instruction_kind == KbOrcaWhirlpoolsInstructionKind::Swap
|
||||
|| instruction_kind == KbOrcaWhirlpoolsInstructionKind::SwapV2
|
||||
if instruction_kind == OrcaWhirlpoolsInstructionKind::Swap
|
||||
|| instruction_kind == OrcaWhirlpoolsInstructionKind::SwapV2
|
||||
{
|
||||
let used_v2 = instruction_kind == KbOrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
let trade_side = kb_infer_trade_side(&log_messages);
|
||||
let used_v2 = instruction_kind == OrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
let trade_side = infer_trade_side(&log_messages);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "orca_whirlpools",
|
||||
"eventKind": "swap",
|
||||
@@ -226,8 +221,8 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"tradeSide": format!("{:?}", trade_side)
|
||||
});
|
||||
decoded_events.push(crate::KbOrcaWhirlpoolsDecodedEvent::Swap(
|
||||
crate::KbOrcaWhirlpoolsSwapDecoded {
|
||||
decoded_events.push(crate::OrcaWhirlpoolsDecodedEvent::Swap(
|
||||
crate::OrcaWhirlpoolsSwapDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -246,61 +241,61 @@ impl KbOrcaWhirlpoolsDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_classify_instruction_kind(
|
||||
fn classify_instruction_kind(
|
||||
parsed_json: std::option::Option<&serde_json::Value>,
|
||||
log_messages: &[std::string::String],
|
||||
) -> KbOrcaWhirlpoolsInstructionKind {
|
||||
let parsed_instruction_name = kb_extract_string_by_candidate_keys(
|
||||
) -> OrcaWhirlpoolsInstructionKind {
|
||||
let parsed_instruction_name = extract_string_by_candidate_keys(
|
||||
parsed_json,
|
||||
&["instruction", "instructionName", "type", "name"],
|
||||
);
|
||||
if let Some(parsed_instruction_name) = parsed_instruction_name {
|
||||
let normalized = kb_normalize_text(parsed_instruction_name.as_str());
|
||||
let normalized = normalize_text(parsed_instruction_name.as_str());
|
||||
if normalized.contains("initializepoolv2") {
|
||||
return KbOrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
return OrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
}
|
||||
if normalized.contains("initializepool") {
|
||||
return KbOrcaWhirlpoolsInstructionKind::InitializePool;
|
||||
return OrcaWhirlpoolsInstructionKind::InitializePool;
|
||||
}
|
||||
if normalized == "swapv2" {
|
||||
return KbOrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
return OrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
}
|
||||
if normalized == "swap" {
|
||||
return KbOrcaWhirlpoolsInstructionKind::Swap;
|
||||
return OrcaWhirlpoolsInstructionKind::Swap;
|
||||
}
|
||||
}
|
||||
if kb_value_contains_any_key(parsed_json, &["tokenProgramA", "tokenProgramB", "memoProgram"])
|
||||
&& kb_log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
if value_contains_any_key(parsed_json, &["tokenProgramA", "tokenProgramB", "memoProgram"])
|
||||
&& log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
{
|
||||
return KbOrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
return OrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
}
|
||||
if kb_value_contains_any_key(parsed_json, &["tokenProgramA", "tokenProgramB", "memoProgram"])
|
||||
&& kb_log_messages_contain_keyword(log_messages, "swap")
|
||||
if value_contains_any_key(parsed_json, &["tokenProgramA", "tokenProgramB", "memoProgram"])
|
||||
&& log_messages_contain_keyword(log_messages, "swap")
|
||||
{
|
||||
return KbOrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
return OrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "initialize_pool_v2")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepoolv2")
|
||||
if log_messages_contain_keyword(log_messages, "initialize_pool_v2")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepoolv2")
|
||||
{
|
||||
return KbOrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
return OrcaWhirlpoolsInstructionKind::InitializePoolV2;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "initializepool")
|
||||
if log_messages_contain_keyword(log_messages, "initialize_pool")
|
||||
|| log_messages_contain_keyword(log_messages, "initializepool")
|
||||
{
|
||||
return KbOrcaWhirlpoolsInstructionKind::InitializePool;
|
||||
return OrcaWhirlpoolsInstructionKind::InitializePool;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "swap_v2")
|
||||
|| kb_log_messages_contain_keyword(log_messages, "swapv2")
|
||||
if log_messages_contain_keyword(log_messages, "swap_v2")
|
||||
|| log_messages_contain_keyword(log_messages, "swapv2")
|
||||
{
|
||||
return KbOrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
return OrcaWhirlpoolsInstructionKind::SwapV2;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "swap") {
|
||||
return KbOrcaWhirlpoolsInstructionKind::Swap;
|
||||
if log_messages_contain_keyword(log_messages, "swap") {
|
||||
return OrcaWhirlpoolsInstructionKind::Swap;
|
||||
}
|
||||
return KbOrcaWhirlpoolsInstructionKind::Unknown;
|
||||
return OrcaWhirlpoolsInstructionKind::Unknown;
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -328,10 +323,13 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = kb_normalize_text(keyword);
|
||||
for log_message in log_messages {
|
||||
let log_normalized = kb_normalize_text(log_message.as_str());
|
||||
fn log_messages_contain_keyword(
|
||||
log_messages_contain_keyword: &[std::string::String],
|
||||
keyword: &str,
|
||||
) -> bool {
|
||||
let keyword_normalized = normalize_text(keyword);
|
||||
for log_message in log_messages_contain_keyword {
|
||||
let log_normalized = normalize_text(log_message.as_str());
|
||||
if log_normalized.contains(keyword_normalized.as_str()) {
|
||||
return true;
|
||||
}
|
||||
@@ -339,9 +337,9 @@ fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
fn normalize_text(normalize_text: &str) -> std::string::String {
|
||||
let mut normalized = std::string::String::new();
|
||||
for character in value.chars() {
|
||||
for character in normalize_text.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
normalized.push(character.to_ascii_lowercase());
|
||||
}
|
||||
@@ -349,14 +347,14 @@ fn kb_normalize_text(value: &str) -> std::string::String {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -372,9 +370,9 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_parse_optional_parsed_json(
|
||||
fn parse_optional_parsed_json(
|
||||
parsed_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::KbError> {
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
|
||||
let parsed_json = match parsed_json {
|
||||
Some(parsed_json) => parsed_json,
|
||||
None => return Ok(None),
|
||||
@@ -383,7 +381,7 @@ fn kb_parse_optional_parsed_json(
|
||||
match value_result {
|
||||
Ok(value) => return Ok(Some(value)),
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction parsed_json '{}': {}",
|
||||
parsed_json, error
|
||||
)));
|
||||
@@ -391,7 +389,7 @@ fn kb_parse_optional_parsed_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -399,10 +397,10 @@ fn kb_extract_string_by_candidate_keys(
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
return kb_extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
return extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys_inner(
|
||||
fn extract_string_by_candidate_keys_inner(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -418,7 +416,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -429,7 +427,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -438,7 +436,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key(
|
||||
fn value_contains_any_key(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> bool {
|
||||
@@ -446,10 +444,10 @@ fn kb_value_contains_any_key(
|
||||
Some(value) => value,
|
||||
None => return false,
|
||||
};
|
||||
return kb_value_contains_any_key_inner(value, candidate_keys);
|
||||
return value_contains_any_key_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
|
||||
if let Some(object) = value.as_object() {
|
||||
for candidate_key in candidate_keys {
|
||||
if object.contains_key(*candidate_key) {
|
||||
@@ -457,7 +455,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -465,7 +463,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
if kb_value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
if value_contains_any_key_inner(nested_value, candidate_keys) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -473,7 +471,7 @@ fn kb_value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
fn extract_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -483,20 +481,20 @@ fn kb_extract_account(
|
||||
return Some(accounts[index].clone());
|
||||
}
|
||||
|
||||
fn kb_infer_trade_side(log_messages: &[std::string::String]) -> crate::KbSwapTradeSide {
|
||||
if kb_log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::KbSwapTradeSide::BuyBase;
|
||||
fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide {
|
||||
if log_messages_contain_keyword(log_messages, "buy") {
|
||||
return crate::SwapTradeSide::BuyBase;
|
||||
}
|
||||
if kb_log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::KbSwapTradeSide::SellBase;
|
||||
if log_messages_contain_keyword(log_messages, "sell") {
|
||||
return crate::SwapTradeSide::SellBase;
|
||||
}
|
||||
return crate::KbSwapTradeSide::Unknown;
|
||||
return crate::SwapTradeSide::Unknown;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_create_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_create_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-orca-create-1".to_string(),
|
||||
Some(891001),
|
||||
Some(1779600001),
|
||||
@@ -523,19 +521,19 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_create_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_create_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
601,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||
Some("orca-whirlpools".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"OrcaPool111",
|
||||
"OrcaTokenA111",
|
||||
"So11111111111111111111111111111111111111112",
|
||||
crate::WSOL_MINT_ID,
|
||||
"OrcaConfig111",
|
||||
"OrcaCreator111"
|
||||
])
|
||||
@@ -548,11 +546,11 @@ mod tests {
|
||||
"instruction": "initialize_pool_v2",
|
||||
"whirlpool": "OrcaPool111",
|
||||
"tokenMintA": "OrcaTokenA111",
|
||||
"tokenMintB": "So11111111111111111111111111111111111111112",
|
||||
"tokenMintB": crate::WSOL_MINT_ID,
|
||||
"whirlpoolsConfig": "OrcaConfig111",
|
||||
"funder": "OrcaCreator111",
|
||||
"tokenProgramA": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
"tokenProgramB": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
"tokenProgramA": crate::SPL_TOKEN_PROGRAM_ID,
|
||||
"tokenProgramB": crate::SPL_TOKEN_PROGRAM_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -562,8 +560,8 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_swap_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-orca-swap-1".to_string(),
|
||||
Some(891002),
|
||||
Some(1779600002),
|
||||
@@ -590,21 +588,17 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_swap_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_swap_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
603,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
|
||||
Some("orca-whirlpools".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
"OrcaSwapPool111",
|
||||
"OrcaSwapTokenA111",
|
||||
"So11111111111111111111111111111111111111112"
|
||||
])
|
||||
.to_string(),
|
||||
serde_json::json!(["OrcaSwapPool111", "OrcaSwapTokenA111", crate::WSOL_MINT_ID])
|
||||
.to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(
|
||||
@@ -613,7 +607,7 @@ mod tests {
|
||||
"instruction": "swap_v2",
|
||||
"whirlpool": "OrcaSwapPool111",
|
||||
"tokenMintA": "OrcaSwapTokenA111",
|
||||
"tokenMintB": "So11111111111111111111111111111111111111112"
|
||||
"tokenMintB": crate::WSOL_MINT_ID
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
@@ -625,7 +619,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn orca_whirlpools_create_pool_is_detected() {
|
||||
let decoder = crate::KbOrcaWhirlpoolsDecoder::new();
|
||||
let decoder = crate::OrcaWhirlpoolsDecoder::new();
|
||||
let transaction = make_create_transaction();
|
||||
let instructions = vec![make_create_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -635,19 +629,16 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbOrcaWhirlpoolsDecodedEvent::CreatePool(event) => {
|
||||
crate::OrcaWhirlpoolsDecodedEvent::CreatePool(event) => {
|
||||
assert_eq!(event.transaction_id, 601);
|
||||
assert_eq!(event.instruction_id, 602);
|
||||
assert_eq!(event.pool_account, Some("OrcaPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("OrcaTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
assert_eq!(event.config_account, Some("OrcaConfig111".to_string()));
|
||||
assert!(event.used_v2);
|
||||
},
|
||||
crate::KbOrcaWhirlpoolsDecodedEvent::Swap(_) => {
|
||||
crate::OrcaWhirlpoolsDecodedEvent::Swap(_) => {
|
||||
panic!("unexpected swap event")
|
||||
},
|
||||
}
|
||||
@@ -655,7 +646,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn orca_whirlpools_swap_is_detected() {
|
||||
let decoder = crate::KbOrcaWhirlpoolsDecoder::new();
|
||||
let decoder = crate::OrcaWhirlpoolsDecoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let instructions = vec![make_swap_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -665,18 +656,15 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbOrcaWhirlpoolsDecodedEvent::Swap(event) => {
|
||||
crate::OrcaWhirlpoolsDecodedEvent::Swap(event) => {
|
||||
assert_eq!(event.transaction_id, 603);
|
||||
assert_eq!(event.instruction_id, 604);
|
||||
assert_eq!(event.pool_account, Some("OrcaSwapPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("OrcaSwapTokenA111".to_string()));
|
||||
assert_eq!(
|
||||
event.token_b_mint,
|
||||
Some("So11111111111111111111111111111111111111112".to_string())
|
||||
);
|
||||
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
|
||||
assert!(event.used_v2);
|
||||
},
|
||||
crate::KbOrcaWhirlpoolsDecodedEvent::CreatePool(_) => {
|
||||
crate::OrcaWhirlpoolsDecodedEvent::CreatePool(_) => {
|
||||
panic!("unexpected create event")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
//! Pump.fun bonding-curve transaction decoder.
|
||||
|
||||
/// Pump.fun program id.
|
||||
pub const KB_PUMP_FUN_PROGRAM_ID: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
|
||||
|
||||
const KB_PUMP_FUN_BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
const KB_PUMP_FUN_SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
const PUMP_FUN_BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
const PUMP_FUN_SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
/// Decoded Pump.fun `create_v2` token event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbPumpFunCreateV2TokenDecoded {
|
||||
pub struct PumpFunCreateV2TokenDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -33,7 +30,7 @@ pub struct KbPumpFunCreateV2TokenDecoded {
|
||||
|
||||
/// Decoded Pump.fun bonding-curve trade event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbPumpFunTradeDecoded {
|
||||
pub struct PumpFunTradeDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -43,7 +40,7 @@ pub struct KbPumpFunTradeDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Token mint.
|
||||
pub mint: std::option::Option<std::string::String>,
|
||||
/// Bonding curve account.
|
||||
@@ -64,20 +61,20 @@ pub struct KbPumpFunTradeDecoded {
|
||||
|
||||
/// Decoded Pump.fun event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbPumpFunDecodedEvent {
|
||||
pub enum PumpFunDecodedEvent {
|
||||
/// `create_v2` token creation.
|
||||
CreateV2Token(KbPumpFunCreateV2TokenDecoded),
|
||||
CreateV2Token(PumpFunCreateV2TokenDecoded),
|
||||
/// Buy trade.
|
||||
BuyTrade(KbPumpFunTradeDecoded),
|
||||
BuyTrade(PumpFunTradeDecoded),
|
||||
/// Sell trade.
|
||||
SellTrade(KbPumpFunTradeDecoded),
|
||||
SellTrade(PumpFunTradeDecoded),
|
||||
}
|
||||
|
||||
/// Pump.fun decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbPumpFunDecoder;
|
||||
pub struct PumpFunDecoder;
|
||||
|
||||
impl KbPumpFunDecoder {
|
||||
impl PumpFunDecoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -86,14 +83,14 @@ impl KbPumpFunDecoder {
|
||||
/// Decodes one projected transaction into zero or more Pump.fun events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbPumpFunDecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::PumpFunDecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -107,15 +104,15 @@ impl KbPumpFunDecoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let has_create_v2_log = kb_log_messages_contain_keyword(&log_messages, "create_v2")
|
||||
|| kb_log_messages_contain_keyword(&log_messages, "createv2");
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let has_create_v2_log = log_messages_contain_keyword(&log_messages, "create_v2")
|
||||
|| log_messages_contain_keyword(&log_messages, "createv2");
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -126,7 +123,7 @@ impl KbPumpFunDecoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_PUMP_FUN_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::PUMP_FUN_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -134,35 +131,34 @@ impl KbPumpFunDecoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_data =
|
||||
kb_decode_optional_instruction_data(instruction.data_json.as_ref());
|
||||
let is_buy = kb_instruction_data_starts_with(
|
||||
let instruction_data = decode_optional_instruction_data(instruction.data_json.as_ref());
|
||||
let is_buy = instruction_data_starts_with(
|
||||
instruction_data.as_deref(),
|
||||
&KB_PUMP_FUN_BUY_DISCRIMINATOR,
|
||||
&PUMP_FUN_BUY_DISCRIMINATOR,
|
||||
);
|
||||
let is_sell = kb_instruction_data_starts_with(
|
||||
let is_sell = instruction_data_starts_with(
|
||||
instruction_data.as_deref(),
|
||||
&KB_PUMP_FUN_SELL_DISCRIMINATOR,
|
||||
&PUMP_FUN_SELL_DISCRIMINATOR,
|
||||
);
|
||||
if is_buy || is_sell {
|
||||
if accounts.len() < 7 {
|
||||
continue;
|
||||
}
|
||||
let amount_raw = kb_extract_u64_argument(instruction_data.as_deref(), 8);
|
||||
let sol_limit_raw = kb_extract_u64_argument(instruction_data.as_deref(), 16);
|
||||
let mint = kb_extract_account(&accounts, 2);
|
||||
let bonding_curve = kb_extract_account(&accounts, 3);
|
||||
let associated_bonding_curve = kb_extract_account(&accounts, 4);
|
||||
let associated_user = kb_extract_account(&accounts, 5);
|
||||
let user = kb_extract_account(&accounts, 6);
|
||||
let fee_recipient = kb_extract_account(&accounts, 1);
|
||||
let token_program = kb_extract_account(&accounts, 8);
|
||||
let creator_vault = kb_extract_account(&accounts, 9);
|
||||
let amount_raw = extract_u64_argument(instruction_data.as_deref(), 8);
|
||||
let sol_limit_raw = extract_u64_argument(instruction_data.as_deref(), 16);
|
||||
let mint = extract_account(&accounts, 2);
|
||||
let bonding_curve = extract_account(&accounts, 3);
|
||||
let associated_bonding_curve = extract_account(&accounts, 4);
|
||||
let associated_user = extract_account(&accounts, 5);
|
||||
let user = extract_account(&accounts, 6);
|
||||
let fee_recipient = extract_account(&accounts, 1);
|
||||
let token_program = extract_account(&accounts, 8);
|
||||
let creator_vault = extract_account(&accounts, 9);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "pump_fun",
|
||||
"signature": transaction.signature,
|
||||
@@ -187,15 +183,15 @@ impl KbPumpFunDecoder {
|
||||
"solLimitRaw": sol_limit_raw,
|
||||
"tradeSide": if is_buy { "BuyBase" } else { "SellBase" }
|
||||
});
|
||||
let event = crate::KbPumpFunTradeDecoded {
|
||||
let event = crate::PumpFunTradeDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
program_id: program_id.clone(),
|
||||
trade_side: if is_buy {
|
||||
crate::KbSwapTradeSide::BuyBase
|
||||
crate::SwapTradeSide::BuyBase
|
||||
} else {
|
||||
crate::KbSwapTradeSide::SellBase
|
||||
crate::SwapTradeSide::SellBase
|
||||
},
|
||||
mint,
|
||||
bonding_curve,
|
||||
@@ -207,9 +203,9 @@ impl KbPumpFunDecoder {
|
||||
payload_json,
|
||||
};
|
||||
if is_buy {
|
||||
decoded_events.push(crate::KbPumpFunDecodedEvent::BuyTrade(event));
|
||||
decoded_events.push(crate::PumpFunDecodedEvent::BuyTrade(event));
|
||||
} else {
|
||||
decoded_events.push(crate::KbPumpFunDecodedEvent::SellTrade(event));
|
||||
decoded_events.push(crate::PumpFunDecodedEvent::SellTrade(event));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -219,10 +215,10 @@ impl KbPumpFunDecoder {
|
||||
if accounts.len() < 6 {
|
||||
continue;
|
||||
}
|
||||
let mint = kb_extract_account(&accounts, 0);
|
||||
let bonding_curve = kb_extract_account(&accounts, 2);
|
||||
let associated_bonding_curve = kb_extract_account(&accounts, 3);
|
||||
let creator = kb_extract_account(&accounts, 5);
|
||||
let mint = extract_account(&accounts, 0);
|
||||
let bonding_curve = extract_account(&accounts, 2);
|
||||
let associated_bonding_curve = extract_account(&accounts, 3);
|
||||
let creator = extract_account(&accounts, 5);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "pump_fun",
|
||||
"eventKind": "create_v2_token",
|
||||
@@ -236,8 +232,8 @@ impl KbPumpFunDecoder {
|
||||
"associatedBondingCurve": associated_bonding_curve,
|
||||
"creator": creator
|
||||
});
|
||||
decoded_events.push(crate::KbPumpFunDecodedEvent::CreateV2Token(
|
||||
crate::KbPumpFunCreateV2TokenDecoded {
|
||||
decoded_events.push(crate::PumpFunDecodedEvent::CreateV2Token(
|
||||
crate::PumpFunCreateV2TokenDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -254,7 +250,7 @@ impl KbPumpFunDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_decode_optional_instruction_data(
|
||||
fn decode_optional_instruction_data(
|
||||
data_json: std::option::Option<&std::string::String>,
|
||||
) -> std::option::Option<std::vec::Vec<u8>> {
|
||||
let data_json = match data_json {
|
||||
@@ -276,7 +272,7 @@ fn kb_decode_optional_instruction_data(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_instruction_data_starts_with(
|
||||
fn instruction_data_starts_with(
|
||||
instruction_data: std::option::Option<&[u8]>,
|
||||
discriminator: &[u8; 8],
|
||||
) -> bool {
|
||||
@@ -290,7 +286,7 @@ fn kb_instruction_data_starts_with(
|
||||
return &instruction_data[0..discriminator.len()] == discriminator;
|
||||
}
|
||||
|
||||
fn kb_extract_u64_argument(
|
||||
fn extract_u64_argument(
|
||||
instruction_data: std::option::Option<&[u8]>,
|
||||
offset: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -307,7 +303,7 @@ fn kb_extract_u64_argument(
|
||||
return Some(u64::from_le_bytes(bytes).to_string());
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -335,10 +331,10 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = kb_normalize_log_text(keyword);
|
||||
fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_normalized = normalize_log_text(keyword);
|
||||
for log_message in log_messages {
|
||||
let log_normalized = kb_normalize_log_text(log_message.as_str());
|
||||
let log_normalized = normalize_log_text(log_message.as_str());
|
||||
if log_normalized.contains(keyword_normalized.as_str()) {
|
||||
return true;
|
||||
}
|
||||
@@ -346,14 +342,14 @@ fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -369,7 +365,7 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
fn extract_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -379,7 +375,7 @@ fn kb_extract_account(
|
||||
return Some(accounts[index].clone());
|
||||
}
|
||||
|
||||
fn kb_normalize_log_text(value: &str) -> std::string::String {
|
||||
fn normalize_log_text(value: &str) -> std::string::String {
|
||||
let mut normalized = std::string::String::new();
|
||||
for character in value.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
@@ -391,8 +387,8 @@ fn kb_normalize_log_text(value: &str) -> std::string::String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-pump-fun-test-1".to_string(),
|
||||
Some(777001),
|
||||
Some(1779200001),
|
||||
@@ -419,13 +415,13 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
91,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_PUMP_FUN_PROGRAM_ID.to_string()),
|
||||
Some(crate::PUMP_FUN_PROGRAM_ID.to_string()),
|
||||
Some("pump".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
@@ -450,7 +446,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pump_fun_create_v2_is_detected() {
|
||||
let decoder = crate::KbPumpFunDecoder::new();
|
||||
let decoder = crate::PumpFunDecoder::new();
|
||||
let transaction = make_transaction();
|
||||
let instructions = vec![make_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -460,7 +456,7 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbPumpFunDecodedEvent::CreateV2Token(event) => {
|
||||
crate::PumpFunDecodedEvent::CreateV2Token(event) => {
|
||||
assert_eq!(event.transaction_id, 91);
|
||||
assert_eq!(event.instruction_id, 17);
|
||||
assert_eq!(event.mint, Some("MintCreate111".to_string()));
|
||||
@@ -471,10 +467,10 @@ mod tests {
|
||||
);
|
||||
assert_eq!(event.creator, Some("Creator111".to_string()));
|
||||
},
|
||||
crate::KbPumpFunDecodedEvent::BuyTrade(_) => {
|
||||
crate::PumpFunDecodedEvent::BuyTrade(_) => {
|
||||
panic!("unexpected pump_fun buy trade event");
|
||||
},
|
||||
crate::KbPumpFunDecodedEvent::SellTrade(_) => {
|
||||
crate::PumpFunDecodedEvent::SellTrade(_) => {
|
||||
panic!("unexpected pump_fun sell trade event");
|
||||
},
|
||||
}
|
||||
@@ -482,7 +478,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pump_fun_create_v2_returns_none_without_expected_log() {
|
||||
let decoder = crate::KbPumpFunDecoder::new();
|
||||
let decoder = crate::PumpFunDecoder::new();
|
||||
let mut transaction = make_transaction();
|
||||
transaction.transaction_json = serde_json::json!({
|
||||
"slot": 777001,
|
||||
|
||||
@@ -2,12 +2,25 @@
|
||||
|
||||
//! PumpSwap AMM transaction decoder.
|
||||
|
||||
/// PumpSwap / PumpAMM program id.
|
||||
pub const KB_PUMP_SWAP_PROGRAM_ID: &str = "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA";
|
||||
const PUMP_SWAP_BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
const PUMP_SWAP_SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PumpSwapInstructionKind {
|
||||
Buy,
|
||||
Sell,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PumpSwapInstructionData {
|
||||
instruction_kind: PumpSwapInstructionKind,
|
||||
base_amount_raw: std::option::Option<std::string::String>,
|
||||
quote_amount_bound_raw: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Decoded PumpSwap trade event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbPumpSwapTradeDecoded {
|
||||
pub struct PumpSwapTradeDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -17,7 +30,7 @@ pub struct KbPumpSwapTradeDecoded {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Trade side.
|
||||
pub trade_side: crate::KbSwapTradeSide,
|
||||
pub trade_side: crate::SwapTradeSide,
|
||||
/// Optional heuristic pool account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional token A mint.
|
||||
@@ -32,18 +45,18 @@ pub struct KbPumpSwapTradeDecoded {
|
||||
|
||||
/// Decoded PumpSwap event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbPumpSwapDecodedEvent {
|
||||
pub enum PumpSwapDecodedEvent {
|
||||
/// Buy trade.
|
||||
BuyTrade(KbPumpSwapTradeDecoded),
|
||||
BuyTrade(PumpSwapTradeDecoded),
|
||||
/// Sell trade.
|
||||
SellTrade(KbPumpSwapTradeDecoded),
|
||||
SellTrade(PumpSwapTradeDecoded),
|
||||
}
|
||||
|
||||
/// PumpSwap decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbPumpSwapDecoder;
|
||||
pub struct PumpSwapDecoder;
|
||||
|
||||
impl KbPumpSwapDecoder {
|
||||
impl PumpSwapDecoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -52,14 +65,14 @@ impl KbPumpSwapDecoder {
|
||||
/// Decodes one projected transaction into zero or more PumpSwap events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbPumpSwapDecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::PumpSwapDecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -70,24 +83,21 @@ impl KbPumpSwapDecoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
continue;
|
||||
}
|
||||
let program_id_option = &instruction.program_id;
|
||||
let program_id = match program_id_option {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_PUMP_SWAP_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::PUMP_SWAP_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
let instruction_id_option = instruction.id;
|
||||
@@ -95,48 +105,75 @@ impl KbPumpSwapDecoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let parsed_json_result =
|
||||
kb_parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
|
||||
let parsed_json = match parsed_json_result {
|
||||
Ok(parsed_json) => parsed_json,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let pool_account = kb_extract_string_by_candidate_keys(
|
||||
let pool_account = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["pool", "poolAccount", "amm", "ammPool", "poolState"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 0));
|
||||
let token_a_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 0));
|
||||
let token_a_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenAMint", "baseMint", "mintA", "coinMint", "token0Mint", "mint0"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 3));
|
||||
let token_b_mint = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 3));
|
||||
let token_b_mint = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["tokenBMint", "quoteMint", "mintB", "pcMint", "token1Mint", "mint1"],
|
||||
)
|
||||
.or_else(|| return kb_extract_account(&accounts, 4));
|
||||
let pool_v2 = kb_extract_string_by_candidate_keys(
|
||||
.or_else(|| return extract_account(&accounts, 4));
|
||||
let pool_v2 = extract_string_by_candidate_keys(
|
||||
parsed_json.as_ref(),
|
||||
&["poolV2", "pool_v2", "ammV2", "bondingCurveV2", "bonding_curve_v2"],
|
||||
);
|
||||
let pool_base_token_account = kb_extract_account(&accounts, 7);
|
||||
let pool_quote_token_account = kb_extract_account(&accounts, 8);
|
||||
let is_buy = kb_log_messages_contain_keyword(&log_messages, "buy");
|
||||
let is_sell = kb_log_messages_contain_keyword(&log_messages, "sell");
|
||||
if !is_buy && !is_sell {
|
||||
continue;
|
||||
}
|
||||
let user_base_token_account = extract_account(&accounts, 5);
|
||||
let user_quote_token_account = extract_account(&accounts, 6);
|
||||
let pool_base_token_account = extract_account(&accounts, 7);
|
||||
let pool_quote_token_account = extract_account(&accounts, 8);
|
||||
let instruction_data_result =
|
||||
parse_pump_swap_instruction_data(instruction.data_json.as_ref());
|
||||
let instruction_data_option = match instruction_data_result {
|
||||
Ok(instruction_data_option) => instruction_data_option,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let instruction_data = match instruction_data_option {
|
||||
Some(instruction_data) => instruction_data,
|
||||
None => continue,
|
||||
};
|
||||
let instruction_kind = match instruction_data.instruction_kind {
|
||||
PumpSwapInstructionKind::Buy => "buy",
|
||||
PumpSwapInstructionKind::Sell => "sell",
|
||||
};
|
||||
let max_quote_amount_in_raw = match instruction_data.instruction_kind {
|
||||
PumpSwapInstructionKind::Buy => instruction_data.quote_amount_bound_raw.clone(),
|
||||
PumpSwapInstructionKind::Sell => None,
|
||||
};
|
||||
let min_quote_amount_out_raw = match instruction_data.instruction_kind {
|
||||
PumpSwapInstructionKind::Buy => None,
|
||||
PumpSwapInstructionKind::Sell => instruction_data.quote_amount_bound_raw.clone(),
|
||||
};
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "pump_swap",
|
||||
"signature": transaction.signature,
|
||||
"instructionId": instruction_id,
|
||||
"parentInstructionId": instruction.parent_instruction_id,
|
||||
"instructionIndex": instruction.instruction_index,
|
||||
"innerInstructionIndex": instruction.inner_instruction_index,
|
||||
"instructionKind": instruction_kind,
|
||||
"instructionBaseAmountRaw": instruction_data.base_amount_raw,
|
||||
"instructionQuoteAmountBoundRaw": instruction_data.quote_amount_bound_raw,
|
||||
"maxQuoteAmountInRaw": max_quote_amount_in_raw,
|
||||
"minQuoteAmountOutRaw": min_quote_amount_out_raw,
|
||||
"userBaseTokenAccount": user_base_token_account,
|
||||
"userQuoteTokenAccount": user_quote_token_account,
|
||||
"accounts": accounts,
|
||||
"parsed": parsed_json,
|
||||
"logMessages": log_messages,
|
||||
@@ -147,14 +184,14 @@ impl KbPumpSwapDecoder {
|
||||
"poolBaseTokenAccount": pool_base_token_account,
|
||||
"poolQuoteTokenAccount": pool_quote_token_account
|
||||
});
|
||||
if is_buy {
|
||||
decoded_events.push(crate::KbPumpSwapDecodedEvent::BuyTrade(
|
||||
crate::KbPumpSwapTradeDecoded {
|
||||
if instruction_data.instruction_kind == PumpSwapInstructionKind::Buy {
|
||||
decoded_events.push(crate::PumpSwapDecodedEvent::BuyTrade(
|
||||
crate::PumpSwapTradeDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
program_id: program_id.clone(),
|
||||
trade_side: crate::KbSwapTradeSide::BuyBase,
|
||||
trade_side: crate::SwapTradeSide::BuyBase,
|
||||
pool_account: pool_account.clone(),
|
||||
token_a_mint: token_a_mint.clone(),
|
||||
token_b_mint: token_b_mint.clone(),
|
||||
@@ -163,14 +200,14 @@ impl KbPumpSwapDecoder {
|
||||
},
|
||||
));
|
||||
}
|
||||
if is_sell {
|
||||
decoded_events.push(crate::KbPumpSwapDecodedEvent::SellTrade(
|
||||
crate::KbPumpSwapTradeDecoded {
|
||||
if instruction_data.instruction_kind == PumpSwapInstructionKind::Sell {
|
||||
decoded_events.push(crate::PumpSwapDecodedEvent::SellTrade(
|
||||
crate::PumpSwapTradeDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
program_id: program_id.clone(),
|
||||
trade_side: crate::KbSwapTradeSide::SellBase,
|
||||
trade_side: crate::SwapTradeSide::SellBase,
|
||||
pool_account,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
@@ -184,7 +221,7 @@ impl KbPumpSwapDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -213,25 +250,14 @@ fn kb_extract_log_messages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
|
||||
let keyword_lower = keyword.to_ascii_lowercase();
|
||||
for log_message in log_messages {
|
||||
let log_lower = log_message.to_ascii_lowercase();
|
||||
if log_lower.contains(keyword_lower.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -247,19 +273,19 @@ fn kb_parse_accounts_json(
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
accounts: &[std::string::String],
|
||||
fn extract_account(
|
||||
extract_account: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
if index >= accounts.len() {
|
||||
if index >= extract_account.len() {
|
||||
return None;
|
||||
}
|
||||
return Some(accounts[index].clone());
|
||||
return Some(extract_account[index].clone());
|
||||
}
|
||||
|
||||
fn kb_parse_optional_parsed_json(
|
||||
fn parse_optional_parsed_json(
|
||||
parsed_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::KbError> {
|
||||
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
|
||||
let parsed_json = match parsed_json {
|
||||
Some(parsed_json) => parsed_json,
|
||||
None => return Ok(None),
|
||||
@@ -268,7 +294,7 @@ fn kb_parse_optional_parsed_json(
|
||||
match value_result {
|
||||
Ok(value) => return Ok(Some(value)),
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction parsed_json '{}': {}",
|
||||
parsed_json, error
|
||||
)));
|
||||
@@ -276,7 +302,7 @@ fn kb_parse_optional_parsed_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
value: std::option::Option<&serde_json::Value>,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -284,10 +310,10 @@ fn kb_extract_string_by_candidate_keys(
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
return kb_extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
return extract_string_by_candidate_keys_inner(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys_inner(
|
||||
fn extract_string_by_candidate_keys_inner(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -303,7 +329,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -313,7 +339,7 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested_result =
|
||||
kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
|
||||
if nested_result.is_some() {
|
||||
return nested_result;
|
||||
}
|
||||
@@ -322,10 +348,94 @@ fn kb_extract_string_by_candidate_keys_inner(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn parse_pump_swap_instruction_data(
|
||||
data_json: std::option::Option<&std::string::String>,
|
||||
) -> Result<std::option::Option<PumpSwapInstructionData>, crate::Error> {
|
||||
let data_json = match data_json {
|
||||
Some(data_json) => data_json,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let data_text_result = parse_instruction_data_text(data_json.as_str());
|
||||
let data_text = match data_text_result {
|
||||
Ok(data_text) => data_text,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if data_text.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let decoded_result = bs58::decode(data_text.trim()).into_vec();
|
||||
let decoded = match decoded_result {
|
||||
Ok(decoded) => decoded,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot base58-decode PumpSwap instruction data '{}': {}",
|
||||
data_text, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
if decoded.len() < 8 {
|
||||
return Ok(None);
|
||||
}
|
||||
let discriminator = [
|
||||
decoded[0], decoded[1], decoded[2], decoded[3], decoded[4], decoded[5], decoded[6],
|
||||
decoded[7],
|
||||
];
|
||||
let instruction_kind = if discriminator == PUMP_SWAP_BUY_DISCRIMINATOR {
|
||||
PumpSwapInstructionKind::Buy
|
||||
} else if discriminator == PUMP_SWAP_SELL_DISCRIMINATOR {
|
||||
PumpSwapInstructionKind::Sell
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
let base_amount_raw = decode_u64_le_as_string(&decoded, 8);
|
||||
let quote_amount_bound_raw = decode_u64_le_as_string(&decoded, 16);
|
||||
return Ok(Some(PumpSwapInstructionData {
|
||||
instruction_kind,
|
||||
base_amount_raw,
|
||||
quote_amount_bound_raw,
|
||||
}));
|
||||
}
|
||||
|
||||
fn parse_instruction_data_text(data_json: &str) -> Result<std::string::String, crate::Error> {
|
||||
let trimmed = data_json.trim();
|
||||
if trimmed.starts_with('"') {
|
||||
let parsed_result = serde_json::from_str::<std::string::String>(trimmed);
|
||||
return match parsed_result {
|
||||
Ok(parsed) => Ok(parsed),
|
||||
Err(error) => Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction data JSON string '{}': {}",
|
||||
data_json, error
|
||||
))),
|
||||
};
|
||||
}
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
|
||||
fn decode_u64_le_as_string(data: &[u8], offset: usize) -> std::option::Option<std::string::String> {
|
||||
if data.len() < offset + 8 {
|
||||
return None;
|
||||
}
|
||||
let value = u64::from_le_bytes([
|
||||
data[offset],
|
||||
data[offset + 1],
|
||||
data[offset + 2],
|
||||
data[offset + 3],
|
||||
data[offset + 4],
|
||||
data[offset + 5],
|
||||
data[offset + 6],
|
||||
data[offset + 7],
|
||||
]);
|
||||
return Some(value.to_string());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_transaction_with_buy_log() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_pump_swap_buy_instruction_data_json() -> std::string::String {
|
||||
return serde_json::json!("AJTQ2h9DXrBfqJi53PDQG2Fvki5tkaTU3").to_string();
|
||||
}
|
||||
|
||||
fn make_transaction_with_buy_log() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-pump-swap-test-1".to_string(),
|
||||
Some(777002),
|
||||
Some(1779200002),
|
||||
@@ -352,13 +462,13 @@ mod tests {
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
92,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_PUMP_SWAP_PROGRAM_ID.to_string()),
|
||||
Some(crate::PUMP_SWAP_PROGRAM_ID.to_string()),
|
||||
Some("pump-amm".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
@@ -373,7 +483,7 @@ mod tests {
|
||||
"PoolQuoteVault111"
|
||||
])
|
||||
.to_string(),
|
||||
None,
|
||||
Some(make_pump_swap_buy_instruction_data_json()),
|
||||
None,
|
||||
Some(
|
||||
serde_json::json!({
|
||||
@@ -393,7 +503,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pump_swap_buy_is_detected() {
|
||||
let decoder = crate::KbPumpSwapDecoder::new();
|
||||
let decoder = crate::PumpSwapDecoder::new();
|
||||
let transaction = make_transaction_with_buy_log();
|
||||
let instructions = vec![make_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -403,7 +513,7 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbPumpSwapDecodedEvent::BuyTrade(event) => {
|
||||
crate::PumpSwapDecodedEvent::BuyTrade(event) => {
|
||||
assert_eq!(event.transaction_id, 92);
|
||||
assert_eq!(event.instruction_id, 18);
|
||||
assert_eq!(event.pool_account, Some("PumpPool111".to_string()));
|
||||
@@ -418,9 +528,9 @@ mod tests {
|
||||
event.payload_json.get("poolQuoteTokenAccount"),
|
||||
Some(&serde_json::Value::String("PoolQuoteVault111".to_string()))
|
||||
);
|
||||
assert_eq!(event.trade_side, crate::KbSwapTradeSide::BuyBase);
|
||||
assert_eq!(event.trade_side, crate::SwapTradeSide::BuyBase);
|
||||
},
|
||||
crate::KbPumpSwapDecodedEvent::SellTrade(_) => {
|
||||
crate::PumpSwapDecodedEvent::SellTrade(_) => {
|
||||
panic!("unexpected sell event")
|
||||
},
|
||||
}
|
||||
@@ -428,7 +538,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pump_swap_returns_none_without_buy_or_sell_log() {
|
||||
let decoder = crate::KbPumpSwapDecoder::new();
|
||||
let decoder = crate::PumpSwapDecoder::new();
|
||||
let mut transaction = make_transaction_with_buy_log();
|
||||
transaction.transaction_json = serde_json::json!({
|
||||
"slot": 777002,
|
||||
@@ -444,7 +554,9 @@ mod tests {
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let instructions = vec![make_instruction()];
|
||||
let mut instruction = make_instruction();
|
||||
instruction.data_json = None;
|
||||
let instructions = vec![instruction];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
let decoded = match decoded_result {
|
||||
Ok(decoded) => decoded,
|
||||
@@ -452,4 +564,40 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_swap_inner_buy_is_detected() {
|
||||
let decoder = crate::PumpSwapDecoder::new();
|
||||
let transaction = make_transaction_with_buy_log();
|
||||
let mut instruction = make_instruction();
|
||||
instruction.parent_instruction_id = Some(17);
|
||||
instruction.inner_instruction_index = Some(0);
|
||||
let instructions = vec![instruction];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
let decoded = match decoded_result {
|
||||
Ok(decoded) => decoded,
|
||||
Err(error) => panic!("decode must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::PumpSwapDecodedEvent::BuyTrade(event) => {
|
||||
assert_eq!(event.transaction_id, 92);
|
||||
assert_eq!(event.instruction_id, 18);
|
||||
assert_eq!(event.pool_account, Some("PumpPool111".to_string()));
|
||||
assert_eq!(event.token_a_mint, Some("TokenA111".to_string()));
|
||||
assert_eq!(event.token_b_mint, Some("TokenB111".to_string()));
|
||||
assert_eq!(
|
||||
event.payload_json.get("parentInstructionId"),
|
||||
Some(&serde_json::Value::Number(serde_json::Number::from(17)))
|
||||
);
|
||||
assert_eq!(
|
||||
event.payload_json.get("innerInstructionIndex"),
|
||||
Some(&serde_json::Value::Number(serde_json::Number::from(0)))
|
||||
);
|
||||
},
|
||||
crate::PumpSwapDecodedEvent::SellTrade(_) => {
|
||||
panic!("unexpected sell event")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
//! Raydium AmmV4 transaction decoder.
|
||||
|
||||
/// Raydium AmmV4 program id.
|
||||
pub const KB_RAYDIUM_AMM_V4_PROGRAM_ID: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
|
||||
|
||||
/// Decoded Raydium AmmV4 initialize2 pool event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct KbRaydiumAmmV4Initialize2PoolDecoded {
|
||||
pub struct RaydiumAmmV4Initialize2PoolDecoded {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Parent instruction id.
|
||||
@@ -32,16 +29,16 @@ pub struct KbRaydiumAmmV4Initialize2PoolDecoded {
|
||||
|
||||
/// Decoded Raydium AmmV4 event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KbRaydiumAmmV4DecodedEvent {
|
||||
pub enum RaydiumAmmV4DecodedEvent {
|
||||
/// `initialize2` pool creation-like event.
|
||||
Initialize2Pool(KbRaydiumAmmV4Initialize2PoolDecoded),
|
||||
Initialize2Pool(RaydiumAmmV4Initialize2PoolDecoded),
|
||||
}
|
||||
|
||||
/// Raydium AmmV4 decoder.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KbRaydiumAmmV4Decoder;
|
||||
pub struct RaydiumAmmV4Decoder;
|
||||
|
||||
impl KbRaydiumAmmV4Decoder {
|
||||
impl RaydiumAmmV4Decoder {
|
||||
/// Creates a new decoder.
|
||||
pub fn new() -> Self {
|
||||
return Self;
|
||||
@@ -50,14 +47,14 @@ impl KbRaydiumAmmV4Decoder {
|
||||
/// Decodes one projected transaction into zero or more Raydium AmmV4 events.
|
||||
pub fn decode_transaction(
|
||||
&self,
|
||||
transaction: &crate::KbChainTransactionDto,
|
||||
instructions: &[crate::KbChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::KbRaydiumAmmV4DecodedEvent>, crate::KbError> {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::RaydiumAmmV4DecodedEvent>, crate::Error> {
|
||||
let transaction_id_option = transaction.id;
|
||||
let transaction_id = match transaction_id_option {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::KbError::InvalidState(format!(
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"chain transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
@@ -68,14 +65,14 @@ impl KbRaydiumAmmV4Decoder {
|
||||
let transaction_json = match transaction_json_result {
|
||||
Ok(transaction_json) => transaction_json,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse transaction_json for signature '{}': {}",
|
||||
transaction.signature, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let log_messages = kb_extract_log_messages(&transaction_json);
|
||||
let has_initialize2_log = kb_log_messages_contain_initialize2(&log_messages);
|
||||
let log_messages = extract_log_messages(&transaction_json);
|
||||
let has_initialize2_log = log_messages_contain_initialize2(&log_messages);
|
||||
let mut decoded_events = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
if instruction.parent_instruction_id.is_some() {
|
||||
@@ -86,7 +83,7 @@ impl KbRaydiumAmmV4Decoder {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
if program_id.as_str() != crate::KB_RAYDIUM_AMM_V4_PROGRAM_ID {
|
||||
if program_id.as_str() != crate::RAYDIUM_AMM_V4_PROGRAM_ID {
|
||||
continue;
|
||||
}
|
||||
if !has_initialize2_log {
|
||||
@@ -97,7 +94,7 @@ impl KbRaydiumAmmV4Decoder {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
let accounts_result = kb_parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(error) => return Err(error),
|
||||
@@ -105,11 +102,11 @@ impl KbRaydiumAmmV4Decoder {
|
||||
if accounts.len() < 10 {
|
||||
continue;
|
||||
}
|
||||
let pool_account = kb_extract_account(&accounts, 4);
|
||||
let lp_mint = kb_extract_account(&accounts, 7);
|
||||
let token_a_mint = kb_extract_account(&accounts, 8);
|
||||
let token_b_mint = kb_extract_account(&accounts, 9);
|
||||
let market_account = kb_extract_account(&accounts, 16);
|
||||
let pool_account = extract_account(&accounts, 4);
|
||||
let lp_mint = extract_account(&accounts, 7);
|
||||
let token_a_mint = extract_account(&accounts, 8);
|
||||
let token_b_mint = extract_account(&accounts, 9);
|
||||
let market_account = extract_account(&accounts, 16);
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "raydium_amm_v4",
|
||||
"eventKind": "initialize2_pool",
|
||||
@@ -124,8 +121,8 @@ impl KbRaydiumAmmV4Decoder {
|
||||
"tokenBMint": token_b_mint,
|
||||
"marketAccount": market_account
|
||||
});
|
||||
decoded_events.push(crate::KbRaydiumAmmV4DecodedEvent::Initialize2Pool(
|
||||
crate::KbRaydiumAmmV4Initialize2PoolDecoded {
|
||||
decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(
|
||||
crate::RaydiumAmmV4Initialize2PoolDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
@@ -139,11 +136,11 @@ impl KbRaydiumAmmV4Decoder {
|
||||
},
|
||||
));
|
||||
}
|
||||
return Ok(decoded_events)
|
||||
return Ok(decoded_events);
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_log_messages(
|
||||
fn extract_log_messages(
|
||||
transaction_json: &serde_json::Value,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut messages = std::vec::Vec::new();
|
||||
@@ -168,26 +165,26 @@ fn kb_extract_log_messages(
|
||||
messages.push(text.to_string());
|
||||
}
|
||||
}
|
||||
return messages
|
||||
return messages;
|
||||
}
|
||||
|
||||
fn kb_log_messages_contain_initialize2(log_messages: &[std::string::String]) -> bool {
|
||||
fn log_messages_contain_initialize2(log_messages: &[std::string::String]) -> bool {
|
||||
for log_message in log_messages {
|
||||
if log_message.contains("initialize2") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
|
||||
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
|
||||
let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
|
||||
let values = match values_result {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse instruction accounts_json '{}': {}",
|
||||
accounts_json, error
|
||||
)));
|
||||
@@ -200,23 +197,23 @@ fn kb_parse_accounts_json(
|
||||
accounts.push(text.to_string());
|
||||
}
|
||||
}
|
||||
return Ok(accounts)
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
fn kb_extract_account(
|
||||
fn extract_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
if index >= accounts.len() {
|
||||
return None;
|
||||
}
|
||||
return Some(accounts[index].clone())
|
||||
return Some(accounts[index].clone());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn make_transaction() -> crate::KbChainTransactionDto {
|
||||
let mut dto = crate::KbChainTransactionDto::new(
|
||||
fn make_transaction() -> crate::ChainTransactionDto {
|
||||
let mut dto = crate::ChainTransactionDto::new(
|
||||
"sig-raydium-test-1".to_string(),
|
||||
Some(888888),
|
||||
Some(1778000000),
|
||||
@@ -240,16 +237,16 @@ mod tests {
|
||||
.to_string(),
|
||||
);
|
||||
dto.id = Some(42);
|
||||
return dto
|
||||
return dto;
|
||||
}
|
||||
|
||||
fn make_instruction() -> crate::KbChainInstructionDto {
|
||||
let mut dto = crate::KbChainInstructionDto::new(
|
||||
fn make_instruction() -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
42,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Some(crate::KB_RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
|
||||
Some("raydium-amm-v4".to_string()),
|
||||
Some(1),
|
||||
serde_json::json!([
|
||||
@@ -277,12 +274,12 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
dto.id = Some(7);
|
||||
return dto
|
||||
return dto;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raydium_amm_v4_initialize2_logs_are_detected() {
|
||||
let decoder = crate::KbRaydiumAmmV4Decoder::new();
|
||||
let decoder = crate::RaydiumAmmV4Decoder::new();
|
||||
let transaction = make_transaction();
|
||||
let instructions = vec![make_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
@@ -292,7 +289,7 @@ mod tests {
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::KbRaydiumAmmV4DecodedEvent::Initialize2Pool(event) => {
|
||||
crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(event) => {
|
||||
assert_eq!(event.transaction_id, 42);
|
||||
assert_eq!(event.instruction_id, 7);
|
||||
assert_eq!(event.pool_account, Some("Pool111".to_string()));
|
||||
@@ -306,7 +303,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn raydium_amm_v4_initialize2_returns_none_without_expected_log() {
|
||||
let decoder = crate::KbRaydiumAmmV4Decoder::new();
|
||||
let decoder = crate::RaydiumAmmV4Decoder::new();
|
||||
let mut transaction = make_transaction();
|
||||
transaction.transaction_json = serde_json::json!({
|
||||
"slot": 888888,
|
||||
|
||||
@@ -2,53 +2,50 @@
|
||||
|
||||
//! Raydium CLMM instruction decoder.
|
||||
|
||||
/// Raydium CLMM program id.
|
||||
pub const KB_RAYDIUM_CLMM_PROGRAM_ID: &str = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
|
||||
const RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR: [u8; 8] = [43, 4, 237, 11, 26, 201, 30, 98];
|
||||
|
||||
const KB_RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR: [u8; 8] = [43, 4, 237, 11, 26, 201, 30, 98];
|
||||
|
||||
const KB_RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR: [u8; 8] = [248, 198, 158, 145, 225, 117, 135, 200];
|
||||
const RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR: [u8; 8] = [248, 198, 158, 145, 225, 117, 135, 200];
|
||||
|
||||
/// Decoded Raydium CLMM event.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
pub enum KbRaydiumClmmDecodedEvent {
|
||||
pub enum RaydiumClmmDecodedEvent {
|
||||
/// Raydium CLMM swap_v2 event.
|
||||
SwapV2(crate::KbRaydiumClmmSwapV2Decoded),
|
||||
SwapV2(crate::RaydiumClmmSwapV2Decoded),
|
||||
}
|
||||
|
||||
impl KbRaydiumClmmDecodedEvent {
|
||||
impl RaydiumClmmDecodedEvent {
|
||||
/// Returns the normalized event kind.
|
||||
pub fn event_kind(&self) -> &'static str {
|
||||
match self {
|
||||
crate::KbRaydiumClmmDecodedEvent::SwapV2(_) => return "raydium_clmm.swap_v2",
|
||||
crate::RaydiumClmmDecodedEvent::SwapV2(_) => return "raydium_clmm.swap_v2",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the pool account.
|
||||
pub fn pool_account(&self) -> &str {
|
||||
match self {
|
||||
crate::KbRaydiumClmmDecodedEvent::SwapV2(event) => return event.pool_state.as_str(),
|
||||
crate::RaydiumClmmDecodedEvent::SwapV2(event) => return event.pool_state.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the normalized base mint.
|
||||
pub fn base_mint(&self) -> &str {
|
||||
match self {
|
||||
crate::KbRaydiumClmmDecodedEvent::SwapV2(event) => return event.base_mint.as_str(),
|
||||
crate::RaydiumClmmDecodedEvent::SwapV2(event) => return event.base_mint.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the normalized quote mint.
|
||||
pub fn quote_mint(&self) -> &str {
|
||||
match self {
|
||||
crate::KbRaydiumClmmDecodedEvent::SwapV2(event) => return event.quote_mint.as_str(),
|
||||
crate::RaydiumClmmDecodedEvent::SwapV2(event) => return event.quote_mint.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the decoded event to JSON payload.
|
||||
pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
|
||||
match self {
|
||||
crate::KbRaydiumClmmDecodedEvent::SwapV2(event) => {
|
||||
crate::RaydiumClmmDecodedEvent::SwapV2(event) => {
|
||||
let result = serde_json::to_string(event);
|
||||
match result {
|
||||
Ok(payload_json) => return Some(payload_json),
|
||||
@@ -61,7 +58,7 @@ impl KbRaydiumClmmDecodedEvent {
|
||||
|
||||
/// Decoded Raydium CLMM swap_v2 instruction.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
pub struct KbRaydiumClmmSwapV2Decoded {
|
||||
pub struct RaydiumClmmSwapV2Decoded {
|
||||
/// User performing the swap.
|
||||
pub payer: std::string::String,
|
||||
/// AMM config account.
|
||||
@@ -104,10 +101,10 @@ pub struct KbRaydiumClmmSwapV2Decoded {
|
||||
}
|
||||
|
||||
/// Decodes a Raydium CLMM instruction.
|
||||
pub fn kb_decode_raydium_clmm_instruction(
|
||||
pub fn decode_raydium_clmm_instruction(
|
||||
accounts_json: &str,
|
||||
data_json: &str,
|
||||
) -> std::vec::Vec<crate::KbRaydiumClmmDecodedEvent> {
|
||||
) -> std::vec::Vec<crate::RaydiumClmmDecodedEvent> {
|
||||
let mut decoded = std::vec::Vec::new();
|
||||
let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
|
||||
let accounts = match accounts_result {
|
||||
@@ -119,7 +116,7 @@ pub fn kb_decode_raydium_clmm_instruction(
|
||||
Ok(data_base58) => data_base58,
|
||||
Err(_) => data_json.to_string(),
|
||||
};
|
||||
let data_option = kb_decode_base58(data_base58.as_str());
|
||||
let data_option = decode_base58(data_base58.as_str());
|
||||
let data = match data_option {
|
||||
Some(data) => data,
|
||||
None => return decoded,
|
||||
@@ -127,83 +124,83 @@ pub fn kb_decode_raydium_clmm_instruction(
|
||||
if data.len() < 41 {
|
||||
return decoded;
|
||||
}
|
||||
let discriminator_option = kb_read_discriminator(data.as_slice());
|
||||
let discriminator_option = read_discriminator(data.as_slice());
|
||||
let discriminator = match discriminator_option {
|
||||
Some(discriminator) => discriminator,
|
||||
None => return decoded,
|
||||
};
|
||||
if discriminator == KB_RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR {
|
||||
if discriminator == RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR {
|
||||
return decoded;
|
||||
}
|
||||
if discriminator != KB_RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR {
|
||||
if discriminator != RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR {
|
||||
return decoded;
|
||||
}
|
||||
let event_option = kb_decode_swap_v2(accounts.as_slice(), data.as_slice());
|
||||
let event_option = decode_swap_v2(accounts.as_slice(), data.as_slice());
|
||||
let event = match event_option {
|
||||
Some(event) => event,
|
||||
None => return decoded,
|
||||
};
|
||||
decoded.push(crate::KbRaydiumClmmDecodedEvent::SwapV2(event));
|
||||
decoded.push(crate::RaydiumClmmDecodedEvent::SwapV2(event));
|
||||
return decoded;
|
||||
}
|
||||
|
||||
fn kb_decode_swap_v2(
|
||||
fn decode_swap_v2(
|
||||
accounts: &[std::string::String],
|
||||
data: &[u8],
|
||||
) -> std::option::Option<crate::KbRaydiumClmmSwapV2Decoded> {
|
||||
let payer = match kb_clone_account(accounts, 0) {
|
||||
) -> std::option::Option<crate::RaydiumClmmSwapV2Decoded> {
|
||||
let payer = match clone_account(accounts, 0) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let amm_config = match kb_clone_account(accounts, 1) {
|
||||
let amm_config = match clone_account(accounts, 1) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let pool_state = match kb_clone_account(accounts, 2) {
|
||||
let pool_state = match clone_account(accounts, 2) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let input_token_account = match kb_clone_account(accounts, 3) {
|
||||
let input_token_account = match clone_account(accounts, 3) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let output_token_account = match kb_clone_account(accounts, 4) {
|
||||
let output_token_account = match clone_account(accounts, 4) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let input_vault = match kb_clone_account(accounts, 5) {
|
||||
let input_vault = match clone_account(accounts, 5) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let output_vault = match kb_clone_account(accounts, 6) {
|
||||
let output_vault = match clone_account(accounts, 6) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let observation_state = match kb_clone_account(accounts, 7) {
|
||||
let observation_state = match clone_account(accounts, 7) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let input_vault_mint = match kb_clone_account(accounts, 11) {
|
||||
let input_vault_mint = match clone_account(accounts, 11) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let output_vault_mint = match kb_clone_account(accounts, 12) {
|
||||
let output_vault_mint = match clone_account(accounts, 12) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let amount = match kb_read_u64_le(data, 8) {
|
||||
let amount = match read_u64_le(data, 8) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let other_amount_threshold = match kb_read_u64_le(data, 16) {
|
||||
let other_amount_threshold = match read_u64_le(data, 16) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let sqrt_price_limit_x64 = match kb_read_u128_le(data, 24) {
|
||||
let sqrt_price_limit_x64 = match read_u128_le(data, 24) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let is_base_input = match kb_read_bool(data, 40) {
|
||||
let is_base_input = match read_bool(data, 40) {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
@@ -219,7 +216,7 @@ fn kb_decode_swap_v2(
|
||||
quote_vault = input_vault.clone();
|
||||
trade_side = "BuyBase".to_string();
|
||||
}
|
||||
return Some(crate::KbRaydiumClmmSwapV2Decoded {
|
||||
return Some(crate::RaydiumClmmSwapV2Decoded {
|
||||
payer,
|
||||
amm_config,
|
||||
pool_state,
|
||||
@@ -242,7 +239,7 @@ fn kb_decode_swap_v2(
|
||||
});
|
||||
}
|
||||
|
||||
fn kb_clone_account(
|
||||
fn clone_account(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -253,7 +250,7 @@ fn kb_clone_account(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_read_discriminator(data: &[u8]) -> std::option::Option<[u8; 8]> {
|
||||
fn read_discriminator(data: &[u8]) -> std::option::Option<[u8; 8]> {
|
||||
if data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
@@ -266,7 +263,7 @@ fn kb_read_discriminator(data: &[u8]) -> std::option::Option<[u8; 8]> {
|
||||
return Some(bytes);
|
||||
}
|
||||
|
||||
fn kb_read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||
fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||
if data.len() < offset + 8 {
|
||||
return None;
|
||||
}
|
||||
@@ -279,7 +276,7 @@ fn kb_read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||
return Some(u64::from_le_bytes(bytes));
|
||||
}
|
||||
|
||||
fn kb_read_u128_le(data: &[u8], offset: usize) -> std::option::Option<u128> {
|
||||
fn read_u128_le(data: &[u8], offset: usize) -> std::option::Option<u128> {
|
||||
if data.len() < offset + 16 {
|
||||
return None;
|
||||
}
|
||||
@@ -292,7 +289,7 @@ fn kb_read_u128_le(data: &[u8], offset: usize) -> std::option::Option<u128> {
|
||||
return Some(u128::from_le_bytes(bytes));
|
||||
}
|
||||
|
||||
fn kb_read_bool(data: &[u8], offset: usize) -> std::option::Option<bool> {
|
||||
fn read_bool(data: &[u8], offset: usize) -> std::option::Option<bool> {
|
||||
if data.len() <= offset {
|
||||
return None;
|
||||
}
|
||||
@@ -303,7 +300,7 @@ fn kb_read_bool(data: &[u8], offset: usize) -> std::option::Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_decode_base58(input: &str) -> std::option::Option<std::vec::Vec<u8>> {
|
||||
fn decode_base58(input: &str) -> std::option::Option<std::vec::Vec<u8>> {
|
||||
let alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".as_bytes();
|
||||
let mut bytes: std::vec::Vec<u8> = std::vec::Vec::new();
|
||||
for input_byte in input.bytes() {
|
||||
@@ -376,13 +373,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decodes_swap_v2() {
|
||||
let events = crate::kb_decode_raydium_clmm_instruction(
|
||||
let events = crate::decode_raydium_clmm_instruction(
|
||||
sample_swap_v2_accounts_json(),
|
||||
r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
|
||||
);
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
crate::KbRaydiumClmmDecodedEvent::SwapV2(event) => {
|
||||
crate::RaydiumClmmDecodedEvent::SwapV2(event) => {
|
||||
assert_eq!(events[0].event_kind(), "raydium_clmm.swap_v2");
|
||||
assert_eq!(
|
||||
events[0].pool_account(),
|
||||
@@ -408,7 +405,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn serializes_swap_v2_payload_json() {
|
||||
let events = crate::kb_decode_raydium_clmm_instruction(
|
||||
let events = crate::decode_raydium_clmm_instruction(
|
||||
sample_swap_v2_accounts_json(),
|
||||
r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
|
||||
);
|
||||
@@ -426,7 +423,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ignores_invalid_data() {
|
||||
let events = crate::kb_decode_raydium_clmm_instruction(
|
||||
let events = crate::decode_raydium_clmm_instruction(
|
||||
sample_swap_v2_accounts_json(),
|
||||
r#""not-base58-data-0""#,
|
||||
);
|
||||
@@ -440,7 +437,7 @@ mod tests {
|
||||
"A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x",
|
||||
"GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR"
|
||||
]"#;
|
||||
let events = crate::kb_decode_raydium_clmm_instruction(
|
||||
let events = crate::decode_raydium_clmm_instruction(
|
||||
accounts_json,
|
||||
r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
|
||||
);
|
||||
@@ -456,7 +453,7 @@ mod tests {
|
||||
data[40] = 1;
|
||||
let encoded = bs58::encode(data).into_string();
|
||||
let data_json = format!("\"{}\"", encoded);
|
||||
let events = crate::kb_decode_raydium_clmm_instruction(
|
||||
let events = crate::decode_raydium_clmm_instruction(
|
||||
sample_swap_v2_accounts_json(),
|
||||
data_json.as_str(),
|
||||
);
|
||||
|
||||
@@ -5,29 +5,24 @@
|
||||
//! This module decodes Raydium constant product swap instructions from
|
||||
//! already-projected Solana transaction instructions.
|
||||
|
||||
/// Raydium CPMM mainnet program id.
|
||||
pub const KB_RAYDIUM_CPMM_PROGRAM_ID: &str = "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C";
|
||||
|
||||
/// Raydium CPMM `swap_base_input` discriminator.
|
||||
const KB_RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR: [u8; 8] =
|
||||
[143, 190, 90, 218, 196, 30, 51, 222];
|
||||
const RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR: [u8; 8] = [143, 190, 90, 218, 196, 30, 51, 222];
|
||||
|
||||
/// Raydium CPMM `swap_base_output` discriminator.
|
||||
const KB_RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR: [u8; 8] =
|
||||
[55, 217, 98, 86, 163, 74, 180, 173];
|
||||
const RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR: [u8; 8] = [55, 217, 98, 86, 163, 74, 180, 173];
|
||||
|
||||
/// Raydium CPMM decoded event.
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
||||
pub enum KbRaydiumCpmmDecodedEvent {
|
||||
pub enum RaydiumCpmmDecodedEvent {
|
||||
/// Swap where the user fixes the input amount.
|
||||
SwapBaseInput(KbRaydiumCpmmSwapDecoded),
|
||||
SwapBaseInput(RaydiumCpmmSwapDecoded),
|
||||
/// Swap where the user fixes the output amount.
|
||||
SwapBaseOutput(KbRaydiumCpmmSwapDecoded),
|
||||
SwapBaseOutput(RaydiumCpmmSwapDecoded),
|
||||
}
|
||||
|
||||
/// Raydium CPMM swap mode.
|
||||
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
||||
pub enum KbRaydiumCpmmSwapMode {
|
||||
pub enum RaydiumCpmmSwapMode {
|
||||
/// Fixed input swap.
|
||||
BaseInput,
|
||||
/// Fixed output swap.
|
||||
@@ -36,9 +31,9 @@ pub enum KbRaydiumCpmmSwapMode {
|
||||
|
||||
/// Raydium CPMM decoded swap.
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
||||
pub struct KbRaydiumCpmmSwapDecoded {
|
||||
pub struct RaydiumCpmmSwapDecoded {
|
||||
/// Instruction mode.
|
||||
pub swap_mode: KbRaydiumCpmmSwapMode,
|
||||
pub swap_mode: RaydiumCpmmSwapMode,
|
||||
/// User or payer account.
|
||||
pub payer: std::string::String,
|
||||
/// Raydium authority account.
|
||||
@@ -87,50 +82,50 @@ pub struct KbRaydiumCpmmSwapDecoded {
|
||||
pub amount_out_raw: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl KbRaydiumCpmmDecodedEvent {
|
||||
impl RaydiumCpmmDecodedEvent {
|
||||
/// Returns the storage event kind.
|
||||
pub fn event_kind(&self) -> &'static str {
|
||||
match self {
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseInput(_) => return "raydium_cpmm.swap_base_input",
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseOutput(_) => return "raydium_cpmm.swap_base_output",
|
||||
RaydiumCpmmDecodedEvent::SwapBaseInput(_) => return "raydium_cpmm.swap_base_input",
|
||||
RaydiumCpmmDecodedEvent::SwapBaseOutput(_) => return "raydium_cpmm.swap_base_output",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the pool account.
|
||||
pub fn pool_account(&self) -> &str {
|
||||
match self {
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.pool_state.as_str(),
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.pool_state.as_str(),
|
||||
RaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.pool_state.as_str(),
|
||||
RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.pool_state.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the normalized base mint.
|
||||
pub fn base_mint(&self) -> &str {
|
||||
match self {
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.base_mint.as_str(),
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.base_mint.as_str(),
|
||||
RaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.base_mint.as_str(),
|
||||
RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.base_mint.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the normalized quote mint.
|
||||
pub fn quote_mint(&self) -> &str {
|
||||
match self {
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.quote_mint.as_str(),
|
||||
KbRaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.quote_mint.as_str(),
|
||||
RaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.quote_mint.as_str(),
|
||||
RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.quote_mint.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the decoded event to JSON payload.
|
||||
pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
|
||||
match self {
|
||||
crate::KbRaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
|
||||
crate::RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
|
||||
let result = serde_json::to_string(event);
|
||||
match result {
|
||||
Ok(payload) => return Some(payload),
|
||||
Err(_) => return None,
|
||||
}
|
||||
},
|
||||
crate::KbRaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
|
||||
crate::RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
|
||||
let result = serde_json::to_string(event);
|
||||
match result {
|
||||
Ok(payload) => return Some(payload),
|
||||
@@ -142,15 +137,15 @@ impl KbRaydiumCpmmDecodedEvent {
|
||||
}
|
||||
|
||||
/// Decodes one Raydium CPMM instruction from projected instruction fields.
|
||||
pub fn kb_decode_raydium_cpmm_instruction(
|
||||
pub fn decode_raydium_cpmm_instruction(
|
||||
accounts_json: &str,
|
||||
data_json: &str,
|
||||
) -> std::vec::Vec<KbRaydiumCpmmDecodedEvent> {
|
||||
let accounts = match kb_parse_accounts_json(accounts_json) {
|
||||
) -> std::vec::Vec<RaydiumCpmmDecodedEvent> {
|
||||
let accounts = match parse_accounts_json(accounts_json) {
|
||||
Some(accounts) => accounts,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
let data_base58 = match kb_parse_data_json_as_base58(data_json) {
|
||||
let data_base58 = match parse_data_json_as_base58(data_json) {
|
||||
Some(data_base58) => data_base58,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
@@ -162,17 +157,17 @@ pub fn kb_decode_raydium_cpmm_instruction(
|
||||
return std::vec::Vec::new();
|
||||
}
|
||||
let discriminator = [data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]];
|
||||
if discriminator == KB_RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR {
|
||||
let amount_in = match kb_read_u64_le(data.as_slice(), 8) {
|
||||
if discriminator == RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR {
|
||||
let amount_in = match read_u64_le(data.as_slice(), 8) {
|
||||
Some(value) => value,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
let minimum_amount_out = match kb_read_u64_le(data.as_slice(), 16) {
|
||||
let minimum_amount_out = match read_u64_le(data.as_slice(), 16) {
|
||||
Some(value) => value,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
let swap = match kb_build_raydium_cpmm_swap(
|
||||
KbRaydiumCpmmSwapMode::BaseInput,
|
||||
let swap = match build_raydium_cpmm_swap(
|
||||
RaydiumCpmmSwapMode::BaseInput,
|
||||
accounts.as_slice(),
|
||||
Some(amount_in.to_string()),
|
||||
Some(minimum_amount_out.to_string()),
|
||||
@@ -182,19 +177,19 @@ pub fn kb_decode_raydium_cpmm_instruction(
|
||||
Some(swap) => swap,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
return vec![KbRaydiumCpmmDecodedEvent::SwapBaseInput(swap)];
|
||||
return vec![RaydiumCpmmDecodedEvent::SwapBaseInput(swap)];
|
||||
}
|
||||
if discriminator == KB_RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR {
|
||||
let max_amount_in = match kb_read_u64_le(data.as_slice(), 8) {
|
||||
if discriminator == RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR {
|
||||
let max_amount_in = match read_u64_le(data.as_slice(), 8) {
|
||||
Some(value) => value,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
let amount_out = match kb_read_u64_le(data.as_slice(), 16) {
|
||||
let amount_out = match read_u64_le(data.as_slice(), 16) {
|
||||
Some(value) => value,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
let swap = match kb_build_raydium_cpmm_swap(
|
||||
KbRaydiumCpmmSwapMode::BaseOutput,
|
||||
let swap = match build_raydium_cpmm_swap(
|
||||
RaydiumCpmmSwapMode::BaseOutput,
|
||||
accounts.as_slice(),
|
||||
None,
|
||||
None,
|
||||
@@ -204,19 +199,19 @@ pub fn kb_decode_raydium_cpmm_instruction(
|
||||
Some(swap) => swap,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
return vec![KbRaydiumCpmmDecodedEvent::SwapBaseOutput(swap)];
|
||||
return vec![RaydiumCpmmDecodedEvent::SwapBaseOutput(swap)];
|
||||
}
|
||||
return std::vec::Vec::new();
|
||||
}
|
||||
|
||||
fn kb_build_raydium_cpmm_swap(
|
||||
swap_mode: KbRaydiumCpmmSwapMode,
|
||||
fn build_raydium_cpmm_swap(
|
||||
swap_mode: RaydiumCpmmSwapMode,
|
||||
accounts: &[std::string::String],
|
||||
amount_in_raw: std::option::Option<std::string::String>,
|
||||
minimum_amount_out_raw: std::option::Option<std::string::String>,
|
||||
max_amount_in_raw: std::option::Option<std::string::String>,
|
||||
amount_out_raw: std::option::Option<std::string::String>,
|
||||
) -> std::option::Option<KbRaydiumCpmmSwapDecoded> {
|
||||
) -> std::option::Option<RaydiumCpmmSwapDecoded> {
|
||||
if accounts.len() < 13 {
|
||||
return None;
|
||||
}
|
||||
@@ -224,7 +219,7 @@ fn kb_build_raydium_cpmm_swap(
|
||||
let output_token_mint = accounts[11].clone();
|
||||
let input_vault = accounts[6].clone();
|
||||
let output_vault = accounts[7].clone();
|
||||
let normalized = kb_normalize_raydium_cpmm_pair(
|
||||
let normalized = normalize_raydium_cpmm_pair(
|
||||
input_token_mint.as_str(),
|
||||
output_token_mint.as_str(),
|
||||
input_vault.as_str(),
|
||||
@@ -232,7 +227,7 @@ fn kb_build_raydium_cpmm_swap(
|
||||
);
|
||||
let input_is_base = normalized.input_is_base;
|
||||
let trade_side = if input_is_base { "sell".to_string() } else { "buy".to_string() };
|
||||
return Some(KbRaydiumCpmmSwapDecoded {
|
||||
return Some(RaydiumCpmmSwapDecoded {
|
||||
swap_mode,
|
||||
payer: accounts[0].clone(),
|
||||
authority: accounts[1].clone(),
|
||||
@@ -260,7 +255,7 @@ fn kb_build_raydium_cpmm_swap(
|
||||
});
|
||||
}
|
||||
|
||||
struct KbRaydiumCpmmNormalizedPair {
|
||||
struct RaydiumCpmmNormalizedPair {
|
||||
base_mint: std::string::String,
|
||||
quote_mint: std::string::String,
|
||||
base_vault: std::string::String,
|
||||
@@ -268,14 +263,14 @@ struct KbRaydiumCpmmNormalizedPair {
|
||||
input_is_base: bool,
|
||||
}
|
||||
|
||||
fn kb_normalize_raydium_cpmm_pair(
|
||||
fn normalize_raydium_cpmm_pair(
|
||||
input_mint: &str,
|
||||
output_mint: &str,
|
||||
input_vault: &str,
|
||||
output_vault: &str,
|
||||
) -> KbRaydiumCpmmNormalizedPair {
|
||||
if kb_is_quote_mint(output_mint) && !kb_is_quote_mint(input_mint) {
|
||||
return KbRaydiumCpmmNormalizedPair {
|
||||
) -> RaydiumCpmmNormalizedPair {
|
||||
if is_quote_mint(output_mint) && !is_quote_mint(input_mint) {
|
||||
return RaydiumCpmmNormalizedPair {
|
||||
base_mint: input_mint.to_string(),
|
||||
quote_mint: output_mint.to_string(),
|
||||
base_vault: input_vault.to_string(),
|
||||
@@ -283,8 +278,8 @@ fn kb_normalize_raydium_cpmm_pair(
|
||||
input_is_base: true,
|
||||
};
|
||||
}
|
||||
if kb_is_quote_mint(input_mint) && !kb_is_quote_mint(output_mint) {
|
||||
return KbRaydiumCpmmNormalizedPair {
|
||||
if is_quote_mint(input_mint) && !is_quote_mint(output_mint) {
|
||||
return RaydiumCpmmNormalizedPair {
|
||||
base_mint: output_mint.to_string(),
|
||||
quote_mint: input_mint.to_string(),
|
||||
base_vault: output_vault.to_string(),
|
||||
@@ -293,7 +288,7 @@ fn kb_normalize_raydium_cpmm_pair(
|
||||
};
|
||||
}
|
||||
if input_mint <= output_mint {
|
||||
return KbRaydiumCpmmNormalizedPair {
|
||||
return RaydiumCpmmNormalizedPair {
|
||||
base_mint: input_mint.to_string(),
|
||||
quote_mint: output_mint.to_string(),
|
||||
base_vault: input_vault.to_string(),
|
||||
@@ -301,7 +296,7 @@ fn kb_normalize_raydium_cpmm_pair(
|
||||
input_is_base: true,
|
||||
};
|
||||
}
|
||||
return KbRaydiumCpmmNormalizedPair {
|
||||
return RaydiumCpmmNormalizedPair {
|
||||
base_mint: output_mint.to_string(),
|
||||
quote_mint: input_mint.to_string(),
|
||||
base_vault: output_vault.to_string(),
|
||||
@@ -310,8 +305,8 @@ fn kb_normalize_raydium_cpmm_pair(
|
||||
};
|
||||
}
|
||||
|
||||
fn kb_is_quote_mint(mint: &str) -> bool {
|
||||
if mint == "So11111111111111111111111111111111111111112" {
|
||||
fn is_quote_mint(mint: &str) -> bool {
|
||||
if mint == crate::WSOL_MINT_ID {
|
||||
return true;
|
||||
}
|
||||
if mint == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" {
|
||||
@@ -326,7 +321,7 @@ fn kb_is_quote_mint(mint: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn kb_parse_accounts_json(
|
||||
fn parse_accounts_json(
|
||||
accounts_json: &str,
|
||||
) -> std::option::Option<std::vec::Vec<std::string::String>> {
|
||||
let result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
|
||||
@@ -336,7 +331,7 @@ fn kb_parse_accounts_json(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_parse_data_json_as_base58(data_json: &str) -> std::option::Option<std::string::String> {
|
||||
fn parse_data_json_as_base58(data_json: &str) -> std::option::Option<std::string::String> {
|
||||
let json_string_result = serde_json::from_str::<std::string::String>(data_json);
|
||||
if let Ok(value) = json_string_result {
|
||||
return Some(value);
|
||||
@@ -352,7 +347,7 @@ fn kb_parse_data_json_as_base58(data_json: &str) -> std::option::Option<std::str
|
||||
return Some(without_quotes.to_string());
|
||||
}
|
||||
|
||||
fn kb_read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||
fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||
if data.len() < offset + 8 {
|
||||
return None;
|
||||
}
|
||||
@@ -388,13 +383,13 @@ mod tests {
|
||||
"USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB",
|
||||
"9fzQ17bEnqJSsyHL5CodJqY2sdjZJBQCQLbgFZ1BYTWn"
|
||||
]"#;
|
||||
let events = crate::kb_decode_raydium_cpmm_instruction(
|
||||
let events = crate::decode_raydium_cpmm_instruction(
|
||||
accounts_json,
|
||||
r#""E73fXHPWvSRF4Q2ZQFvPoeJBVGDUEMmxB""#,
|
||||
);
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
crate::KbRaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
|
||||
crate::RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
|
||||
assert_eq!(event.pool_state, "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV");
|
||||
assert_eq!(event.base_mint, "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk");
|
||||
assert_eq!(event.quote_mint, "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB");
|
||||
@@ -425,13 +420,13 @@ mod tests {
|
||||
"Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk",
|
||||
"9fzQ17bEnqJSsyHL5CodJqY2sdjZJBQCQLbgFZ1BYTWn"
|
||||
]"#;
|
||||
let events = crate::kb_decode_raydium_cpmm_instruction(
|
||||
let events = crate::decode_raydium_cpmm_instruction(
|
||||
accounts_json,
|
||||
r#""66JafaVu7KNEtTngqQyD7ETVurF3rxJ47""#,
|
||||
);
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
crate::KbRaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
|
||||
crate::RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
|
||||
assert_eq!(event.pool_state, "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV");
|
||||
assert_eq!(event.base_mint, "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk");
|
||||
assert_eq!(event.quote_mint, "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB");
|
||||
|
||||
Reference in New Issue
Block a user