listen_kit/
pump.rs

1use crate::jito::{get_jito_tip_pubkey, send_jito_tx};
2use crate::util::apply_fee;
3use log::{debug, error, info, warn};
4use solana_account_decoder::UiAccountEncoding;
5use solana_client::rpc_config::{
6    RpcAccountInfoConfig, RpcSendTransactionConfig, RpcTransactionConfig,
7};
8use solana_sdk::hash::Hash;
9use solana_sdk::system_instruction::transfer;
10use solana_sdk::transaction::{Transaction, VersionedTransaction};
11use std::error::Error;
12
13use std::str::FromStr;
14use tokio::time::{sleep, Duration};
15
16use borsh::{BorshDeserialize, BorshSerialize};
17
18use serde::{Deserialize, Serialize};
19use solana_client::nonblocking::rpc_client::RpcClient;
20use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel};
21use solana_sdk::instruction::{AccountMeta, Instruction};
22use solana_sdk::pubkey::Pubkey;
23use solana_sdk::signature::{Keypair, Signature};
24use solana_sdk::signer::Signer;
25use solana_transaction_status::{
26    EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, UiMessage,
27    UiParsedMessage, UiTransactionEncoding,
28};
29
30use crate::constants::{
31    ASSOCIATED_TOKEN_PROGRAM, EVENT_AUTHORITY, PUMP_BUY_METHOD,
32    PUMP_FEE_ADDRESS, PUMP_FUN_PROGRAM, PUMP_GLOBAL_ADDRESS, PUMP_SELL_METHOD,
33    RENT_PROGRAM, SYSTEM_PROGRAM_ID, TOKEN_PROGRAM,
34};
35use crate::util::{
36    make_compute_budget_ixs, pubkey_to_string,
37    string_to_pubkey, /*string_to_u64,*/
38};
39
40#[derive(BorshSerialize)]
41pub struct PumpFunSwapInstructionData {
42    pub method_id: [u8; 8],
43    pub token_amount: u64,
44    pub lamports: u64,
45}
46
47#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
48pub struct BondingCurveLayout {
49    pub blob1: u64,
50    pub virtual_token_reserves: u64,
51    pub virtual_sol_reserves: u64,
52    pub real_token_reserves: u64,
53    pub real_sol_reserves: u64,
54    pub blob4: u64,
55    pub complete: bool,
56}
57
58#[derive(Serialize, Deserialize, Debug, Clone)]
59pub struct PumpTokenData {
60    pub address: String,
61    pub balance: u64,
62    pub image_uri: String,
63    pub market_cap: f64,
64    pub mint: String,
65    pub name: String,
66    pub symbol: String,
67    pub value: f64,
68}
69
70impl BondingCurveLayout {
71    pub const LEN: usize = 8 + 8 + 8 + 8 + 8 + 8 + 1;
72
73    pub fn parse(data: &[u8]) -> Result<Self, std::io::Error> {
74        Self::try_from_slice(data)
75    }
76}
77
78pub async fn get_slot_created(
79    rpc_client: &RpcClient,
80    mint: &Pubkey,
81) -> Result<u64, Box<dyn std::error::Error>> {
82    let token_transactions =
83        rpc_client.get_signatures_for_address(mint).await?;
84
85    if token_transactions.is_empty() {
86        warn!("No transactions found for mint: {}", mint);
87        return Ok(0);
88    }
89
90    let first_tx_sig = token_transactions.last().unwrap();
91    let first_tx = rpc_client
92        .get_transaction_with_config(
93            &Signature::from_str(&first_tx_sig.signature).expect("signature"),
94            RpcTransactionConfig {
95                encoding: Some(UiTransactionEncoding::Json),
96                commitment: None,
97                max_supported_transaction_version: Some(0),
98            },
99        )
100        .await?;
101
102    Ok(first_tx.slot)
103}
104
105pub fn mint_to_pump_accounts(mint: &Pubkey) -> PumpAccounts {
106    // Derive the bonding curve address
107    let (bonding_curve, _) = Pubkey::find_program_address(
108        &[b"bonding-curve", mint.as_ref()],
109        &Pubkey::from_str(PUMP_FUN_PROGRAM).unwrap(),
110    );
111
112    // Derive the associated bonding curve address
113    let associated_bonding_curve =
114        spl_associated_token_account::get_associated_token_address(
115            &bonding_curve,
116            mint,
117        );
118
119    PumpAccounts {
120        mint: *mint,
121        bonding_curve,
122        associated_bonding_curve,
123        dev: Pubkey::default(),
124        metadata: Pubkey::default(),
125    }
126}
127pub async fn get_bonding_curve(
128    rpc_client: &RpcClient,
129    bonding_curve_pubkey: Pubkey,
130) -> Result<BondingCurveLayout, Box<dyn Error>> {
131    const MAX_RETRIES: u32 = 5;
132    const INITIAL_DELAY_MS: u64 = 200;
133    let mut retries = 0;
134    let mut delay = Duration::from_millis(INITIAL_DELAY_MS);
135
136    loop {
137        match rpc_client
138            .get_account_with_config(
139                &bonding_curve_pubkey,
140                RpcAccountInfoConfig {
141                    encoding: Some(UiAccountEncoding::Base64),
142                    commitment: Some(CommitmentConfig::processed()),
143                    data_slice: None,
144                    min_context_slot: None,
145                },
146            )
147            .await
148        {
149            Ok(res) => {
150                if let Some(account) = res.value {
151                    // Convert Vec<u8> to [u8; 49]
152                    let data_length = account.data.len();
153                    let data: [u8; 49] =
154                        account.data.try_into().map_err(|_| {
155                            format!("Invalid data length: {}", data_length)
156                        })?;
157
158                    debug!("Raw bytes: {:?}", data);
159
160                    let layout = BondingCurveLayout {
161                        blob1: u64::from_le_bytes(data[0..8].try_into()?),
162                        virtual_token_reserves: u64::from_le_bytes(
163                            data[8..16].try_into()?,
164                        ),
165                        virtual_sol_reserves: u64::from_le_bytes(
166                            data[16..24].try_into()?,
167                        ),
168                        real_token_reserves: u64::from_le_bytes(
169                            data[24..32].try_into()?,
170                        ),
171                        real_sol_reserves: u64::from_le_bytes(
172                            data[32..40].try_into()?,
173                        ),
174                        blob4: u64::from_le_bytes(data[40..48].try_into()?),
175                        complete: data[48] != 0,
176                    };
177
178                    debug!("Parsed BondingCurveLayout: {:?}", layout);
179                    return Ok(layout);
180                } else {
181                    if retries >= MAX_RETRIES {
182                        error!("Max retries reached. Account not found.");
183                        return Err(
184                            "Account not found after max retries".into()
185                        );
186                    }
187                    warn!(
188                        "Attempt {} failed: Account not found. Retrying in {:?}...",
189                        retries + 1,
190                        delay
191                    );
192                    sleep(delay).await;
193                    retries += 1;
194                    delay = Duration::from_millis(
195                        INITIAL_DELAY_MS * 2u64.pow(retries),
196                    );
197                    continue;
198                }
199            }
200            Err(e) => {
201                if retries >= MAX_RETRIES {
202                    error!("Max retries reached. Last error: {}", e);
203                    return Err(format!(
204                        "Max retries reached. Last error: {}",
205                        e
206                    )
207                    .into());
208                }
209                warn!(
210                    "Attempt {} failed: {}. Retrying in {:?}...",
211                    retries + 1,
212                    e,
213                    delay
214                );
215                sleep(delay).await;
216                retries += 1;
217                delay = Duration::from_millis(
218                    INITIAL_DELAY_MS * 2u64.pow(retries),
219                );
220            }
221        }
222    }
223}
224
225pub fn get_pump_token_amount(
226    virtual_sol_reserves: u64,
227    virtual_token_reserves: u64,
228    real_token_reserves: Option<u64>,
229    lamports: u64,
230) -> Result<u64, Box<dyn std::error::Error>> {
231    let virtual_sol_reserves = virtual_sol_reserves as u128;
232    let virtual_token_reserves = virtual_token_reserves as u128;
233    let amount_in = lamports as u128;
234
235    // Calculate reserves_product carefully to avoid overflow
236    let reserves_product = virtual_sol_reserves
237        .checked_mul(virtual_token_reserves)
238        .ok_or("Overflow in reserves product calculation")?;
239
240    let new_virtual_sol_reserve = virtual_sol_reserves
241        .checked_add(amount_in)
242        .ok_or("Overflow in new virtual SOL reserve calculation")?;
243
244    let new_virtual_token_reserve = reserves_product
245        .checked_div(new_virtual_sol_reserve)
246        .ok_or("Division by zero or overflow in new virtual token reserve calculation")?
247        .checked_add(1)
248        .ok_or("Overflow in new virtual token reserve calculation")?;
249
250    let amount_out = virtual_token_reserves
251        .checked_sub(new_virtual_token_reserve)
252        .ok_or("Underflow in amount out calculation")?;
253
254    let final_amount_out =
255        if let Some(real_token_reserves) = real_token_reserves {
256            std::cmp::min(amount_out, real_token_reserves as u128)
257        } else {
258            amount_out
259        };
260
261    Ok(final_amount_out as u64)
262}
263
264#[derive(Debug, Clone, Deserialize, Serialize)]
265pub struct PumpBuyRequest {
266    #[serde(
267        serialize_with = "pubkey_to_string",
268        deserialize_with = "string_to_pubkey"
269    )]
270    pub mint: Pubkey,
271    #[serde(
272        serialize_with = "pubkey_to_string",
273        deserialize_with = "string_to_pubkey"
274    )]
275    pub bonding_curve: Pubkey,
276    #[serde(
277        serialize_with = "pubkey_to_string",
278        deserialize_with = "string_to_pubkey"
279    )]
280    pub associated_bonding_curve: Pubkey,
281    // with typescript service, the data is passed as string
282    // commenting this out since I am passing u64 from rust service now
283    // #[serde(deserialize_with = "string_to_u64")]
284    pub virtual_token_reserves: u64,
285    // #[serde(deserialize_with = "string_to_u64")]
286    pub virtual_sol_reserves: u64,
287
288    pub slot: Option<u64>,
289}
290
291pub async fn buy_pump_token(
292    wallet: &Keypair,
293    latest_blockhash: Hash,
294    pump_accounts: PumpAccounts,
295    token_amount: u64,
296    lamports: u64,
297    tip: u64,
298) -> Result<(), Box<dyn Error>> {
299    let owner = wallet.pubkey();
300
301    info!("{} buying {} {}", owner, token_amount, pump_accounts.mint);
302
303    let mut ixs = _make_buy_ixs(
304        owner,
305        pump_accounts.mint,
306        pump_accounts.bonding_curve,
307        pump_accounts.associated_bonding_curve,
308        token_amount,
309        apply_fee(lamports),
310    )?;
311
312    ixs.push(transfer(&owner, &get_jito_tip_pubkey(), tip));
313
314    let tx = Transaction::new_signed_with_payer(
315        &ixs,
316        Some(&owner),
317        &[wallet],
318        latest_blockhash,
319    );
320
321    send_jito_tx(tx).await?;
322
323    Ok(())
324}
325
326pub fn _make_buy_ixs(
327    owner: Pubkey,
328    mint: Pubkey,
329    bonding_curve: Pubkey,
330    associated_bonding_curve: Pubkey,
331    token_amount: u64,
332    lamports: u64,
333) -> Result<Vec<Instruction>, Box<dyn Error>> {
334    let mut ixs = vec![];
335    let ata = spl_associated_token_account::get_associated_token_address(
336        &owner, &mint,
337    );
338
339    ixs.push(
340        spl_associated_token_account::instruction::create_associated_token_account_idempotent(
341            &owner,
342            &owner,
343            &mint,
344            &spl_token::id(),
345        ),
346    );
347    ixs.push(make_pump_swap_ix(
348        owner,
349        mint,
350        bonding_curve,
351        associated_bonding_curve,
352        token_amount,
353        lamports,
354        ata,
355    )?);
356
357    Ok(ixs)
358}
359
360async fn _send_tx_standard(
361    ixs: Vec<Instruction>,
362    wallet: &Keypair,
363    rpc_client: &RpcClient,
364    owner: Pubkey,
365) -> Result<(), Box<dyn Error>> {
366    let transaction =
367        VersionedTransaction::from(Transaction::new_signed_with_payer(
368            &ixs,
369            Some(&owner),
370            &[wallet],
371            rpc_client.get_latest_blockhash().await?,
372        ));
373    let res = rpc_client
374        .send_transaction_with_config(
375            &transaction,
376            RpcSendTransactionConfig {
377                skip_preflight: true,
378                min_context_slot: None,
379                preflight_commitment: Some(CommitmentLevel::Processed),
380                max_retries: None,
381                encoding: None,
382            },
383        )
384        .await;
385
386    match res {
387        Ok(sig) => {
388            info!("Transaction sent: {}", sig);
389        }
390        Err(e) => {
391            return Err(e.into());
392        }
393    }
394
395    Ok(())
396}
397
398#[timed::timed(duration(printer = "info!"))]
399pub async fn sell_pump_token(
400    wallet: &Keypair,
401    latest_blockhash: Hash,
402    pump_accounts: PumpAccounts,
403    token_amount: u64,
404) -> Result<(), Box<dyn Error>> {
405    let owner = wallet.pubkey();
406
407    let ata = spl_associated_token_account::get_associated_token_address(
408        &owner,
409        &pump_accounts.mint,
410    );
411
412    let mut ixs = vec![];
413    let mut compute_budget_ixs = make_compute_budget_ixs(69_000, 69_000);
414    let sell_ix = make_pump_sell_ix(owner, pump_accounts, token_amount, ata)?;
415    ixs.append(&mut compute_budget_ixs);
416    ixs.push(sell_ix);
417    ixs.push(transfer(&owner, &get_jito_tip_pubkey(), 30_000));
418
419    let tx = Transaction::new_signed_with_payer(
420        &ixs,
421        Some(&owner),
422        &[wallet],
423        latest_blockhash,
424    );
425
426    send_jito_tx(tx).await?;
427
428    Ok(())
429}
430
431/// Interact With Pump.Fun - 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
432/// #1 - Global
433/// #2 - Fee Recipient: Pump.fun Fee Account (Writable)
434/// #3 - Mint
435/// #4 - Bonding Curve (Writable)
436/// #5 - Associated Bonding Curve (Writable)
437/// #6 - Associated Token Account (ATA) (Writable)
438/// #7 - User (Writable Signer Fee-Payer)
439/// #8 - System Program
440/// #9 - Associated Token Program
441/// #10 - Token Program
442/// #11 - Event Authority
443/// #12 - Program: Pump.fun Program
444pub fn make_pump_sell_ix(
445    owner: Pubkey,
446    pump_accounts: PumpAccounts,
447    token_amount: u64,
448    ata: Pubkey,
449) -> Result<Instruction, Box<dyn Error>> {
450    let accounts: [AccountMeta; 12] = [
451        AccountMeta::new_readonly(
452            Pubkey::from_str(PUMP_GLOBAL_ADDRESS)?,
453            false,
454        ),
455        AccountMeta::new(Pubkey::from_str(PUMP_FEE_ADDRESS)?, false),
456        AccountMeta::new_readonly(pump_accounts.mint, false),
457        AccountMeta::new(pump_accounts.bonding_curve, false),
458        AccountMeta::new(pump_accounts.associated_bonding_curve, false),
459        AccountMeta::new(ata, false),
460        AccountMeta::new(owner, true),
461        AccountMeta::new_readonly(Pubkey::from_str(SYSTEM_PROGRAM_ID)?, false),
462        AccountMeta::new_readonly(
463            Pubkey::from_str(ASSOCIATED_TOKEN_PROGRAM)?,
464            false,
465        ),
466        AccountMeta::new_readonly(Pubkey::from_str(TOKEN_PROGRAM)?, false),
467        AccountMeta::new_readonly(Pubkey::from_str(EVENT_AUTHORITY)?, false),
468        AccountMeta::new_readonly(Pubkey::from_str(PUMP_FUN_PROGRAM)?, false),
469    ];
470
471    // max slippage, careful if not using frontrun protection
472    let data = PumpFunSwapInstructionData {
473        method_id: PUMP_SELL_METHOD,
474        token_amount,
475        lamports: 0,
476    };
477
478    Ok(Instruction::new_with_borsh(
479        Pubkey::from_str(PUMP_FUN_PROGRAM)?,
480        &data,
481        accounts.to_vec(),
482    ))
483}
484
485/// Interact With Pump.Fun 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
486/// Input Accounts
487/// #1 - Global: 4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf
488/// #2 - Fee Recipient: Pump.fun Fee Account (Writable)
489/// #3 - Mint
490/// #4 - Bonding Curve (Writable)
491/// #5 - Associated Bonding Curve (Writable)
492/// #6 - Associated User Account (Writable) (ATA)
493/// #7 - User - owner, sender (Writable, Signer, Fee Payer)
494/// #8 - System Program (11111111111111111111111111111111)
495/// #9 - Token Program (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)
496/// #10 - Rent (SysvarRent111111111111111111111111111111111)
497/// #11 - Event Authority: Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1
498/// #12 - Program: Pump.fun Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
499pub fn make_pump_swap_ix(
500    owner: Pubkey,
501    mint: Pubkey,
502    bonding_curve: Pubkey,
503    associated_bonding_curve: Pubkey,
504    token_amount: u64,
505    lamports: u64,
506    ata: Pubkey,
507) -> Result<Instruction, Box<dyn Error>> {
508    let accounts: [AccountMeta; 12] = [
509        AccountMeta::new_readonly(
510            Pubkey::from_str(PUMP_GLOBAL_ADDRESS)?,
511            false,
512        ),
513        AccountMeta::new(Pubkey::from_str(PUMP_FEE_ADDRESS)?, false),
514        AccountMeta::new_readonly(mint, false),
515        AccountMeta::new(bonding_curve, false),
516        AccountMeta::new(associated_bonding_curve, false),
517        AccountMeta::new(ata, false),
518        AccountMeta::new(owner, true),
519        AccountMeta::new_readonly(Pubkey::from_str(SYSTEM_PROGRAM_ID)?, false),
520        AccountMeta::new_readonly(Pubkey::from_str(TOKEN_PROGRAM)?, false),
521        AccountMeta::new_readonly(Pubkey::from_str(RENT_PROGRAM)?, false),
522        AccountMeta::new_readonly(Pubkey::from_str(EVENT_AUTHORITY)?, false),
523        AccountMeta::new_readonly(Pubkey::from_str(PUMP_FUN_PROGRAM)?, false),
524    ];
525
526    let data = PumpFunSwapInstructionData {
527        method_id: PUMP_BUY_METHOD,
528        token_amount,
529        lamports,
530    };
531
532    Ok(Instruction::new_with_borsh(
533        Pubkey::from_str(PUMP_FUN_PROGRAM)?,
534        &data,
535        accounts.to_vec(),
536    ))
537}
538
539#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
540pub struct PumpAccounts {
541    #[serde(
542        serialize_with = "pubkey_to_string",
543        deserialize_with = "string_to_pubkey"
544    )]
545    pub mint: Pubkey,
546    #[serde(
547        serialize_with = "pubkey_to_string",
548        deserialize_with = "string_to_pubkey"
549    )]
550    pub bonding_curve: Pubkey,
551    #[serde(
552        serialize_with = "pubkey_to_string",
553        deserialize_with = "string_to_pubkey"
554    )]
555    pub associated_bonding_curve: Pubkey,
556    #[serde(
557        serialize_with = "pubkey_to_string",
558        deserialize_with = "string_to_pubkey"
559    )]
560    pub dev: Pubkey,
561    #[serde(
562        serialize_with = "pubkey_to_string",
563        deserialize_with = "string_to_pubkey"
564    )]
565    pub metadata: Pubkey,
566}
567
568pub fn parse_pump_accounts(
569    tx: EncodedConfirmedTransactionWithStatusMeta,
570) -> Result<PumpAccounts, Box<dyn Error>> {
571    if let EncodedTransaction::Json(tx) = &tx.transaction.transaction {
572        if let UiMessage::Parsed(UiParsedMessage {
573            account_keys,
574            instructions: _,
575            recent_blockhash: _,
576            address_table_lookups: _,
577        }) = &tx.message
578        {
579            debug!("Account keys: {:?}", account_keys);
580            if account_keys.len() >= 5 {
581                let dev = account_keys[0].pubkey.parse()?;
582                let mint = account_keys[1].pubkey.parse()?;
583                let bonding_curve = account_keys[3].pubkey.parse()?;
584                let associated_bonding_curve =
585                    account_keys[4].pubkey.parse()?;
586                let metadata = account_keys[5].pubkey.parse()?;
587
588                Ok(PumpAccounts {
589                    mint,
590                    bonding_curve,
591                    associated_bonding_curve,
592                    dev,
593                    metadata,
594                })
595            } else {
596                Err("Not enough account keys".into())
597            }
598        } else {
599            Err("Not a parsed transaction".into())
600        }
601    } else {
602        Err("Not a JSON transaction".into())
603    }
604}
605
606#[derive(Serialize, Deserialize, Debug)]
607pub struct PumpTokenInfo {
608    pub associated_bonding_curve: String,
609    pub bonding_curve: String,
610    pub complete: bool,
611    pub created_timestamp: i64,
612    pub creator: String,
613    pub description: String,
614    pub image_uri: String,
615    pub inverted: bool,
616    pub is_currently_live: bool,
617    pub king_of_the_hill_timestamp: i64,
618    pub last_reply: i64,
619    pub market_cap: f64,
620    pub market_id: String,
621    pub metadata_uri: String,
622    pub mint: String,
623    pub name: String,
624    pub nsfw: bool,
625    pub profile_image: Option<String>,
626    pub raydium_pool: String,
627    pub reply_count: i32,
628    pub show_name: bool,
629    pub symbol: String,
630    pub telegram: Option<String>,
631    pub total_supply: i64,
632    pub twitter: Option<String>,
633    pub usd_market_cap: f64,
634    pub username: Option<String>,
635    pub virtual_sol_reserves: i64,
636    pub virtual_token_reserves: i64,
637    pub website: Option<String>,
638}
639
640#[derive(Serialize, Deserialize, Debug)]
641pub struct IPFSMetadata {
642    pub name: String,
643    pub symbol: String,
644    pub description: String,
645    pub image: String,
646    #[serde(rename = "showName")]
647    pub show_name: Option<bool>,
648    #[serde(rename = "createdOn")]
649    pub created_on: Option<String>,
650    pub twitter: Option<String>,
651    pub telegram: Option<String>,
652    pub website: Option<String>,
653}
654
655pub async fn fetch_metadata(
656    mint: &Pubkey,
657) -> Result<PumpTokenInfo, Box<dyn Error>> {
658    const MAX_RETRIES: u32 = 3;
659    const INITIAL_DELAY_MS: u64 = 100;
660
661    let mut retry_count = 0;
662    let mut delay_ms = INITIAL_DELAY_MS;
663
664    loop {
665        match fetch_metadata_inner(mint).await {
666            Ok(metadata) => {
667                info!("Metadata fetched successfully");
668                return Ok(metadata);
669            }
670            Err(e) => {
671                if retry_count >= MAX_RETRIES {
672                    info!("Failed to fetch metadata after all retries");
673                    return Err(e);
674                }
675                info!(
676                    "Retry attempt {} failed: {:?}. Retrying in {} ms...",
677                    retry_count + 1,
678                    e,
679                    delay_ms
680                );
681                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
682                retry_count += 1;
683                delay_ms *= 2; // Exponential backoff
684            }
685        }
686    }
687}
688
689async fn fetch_metadata_inner(
690    mint: &Pubkey,
691) -> Result<PumpTokenInfo, Box<dyn Error>> {
692    let url = format!("https://frontend-api.pump.fun/coins/{}", mint);
693    info!("Fetching metadata from: {}", url);
694    let res = reqwest::get(&url).await?;
695    info!("res: {:?}", res);
696    let data = res.json::<PumpTokenInfo>().await?;
697    Ok(data)
698}
699
700#[cfg(test)]
701mod tests {
702    use solana_sdk::signer::EncodableKey;
703
704    use crate::util::env;
705
706    use super::*;
707
708    #[tokio::test]
709    async fn test_fetch_metadata() {
710        let metadata = fetch_metadata(
711            &Pubkey::from_str("4cRkQ2dntpusYag6Zmvco8T78WxK9Jqh1eEZJox8pump")
712                .expect("parse mint"),
713        )
714        .await
715        .expect("fetch_metadata");
716
717        assert_eq!(metadata.name, "🗿".to_string());
718        assert_eq!(metadata.symbol, "🗿".to_string());
719        assert_eq!(
720            metadata.image_uri,
721            "https://cf-ipfs.com/ipfs/QmXn5xkUMxNQ5c5Sfct8rFTq9jNi6jsSHm1yLY2nQyeSke".to_string()
722        );
723        assert_eq!(
724            metadata.twitter,
725            Some("https://x.com/thefirstgigasol".to_string())
726        );
727        assert_eq!(
728            metadata.telegram,
729            Some("https://t.me/+keptGgOKxN45YWRl".to_string())
730        );
731        assert_eq!(
732            metadata.website,
733            Some("https://thefirstgiga.com/".to_string())
734        );
735    }
736
737    #[test]
738    fn test_parse_pump_accounts() {
739        let sample_tx = std::fs::read_to_string("mocks/pump_fun_tx.json")
740            .expect("read tx");
741        let tx: EncodedConfirmedTransactionWithStatusMeta =
742            serde_json::from_str(&sample_tx).expect("parse tx");
743        let accounts = parse_pump_accounts(tx).expect("parse accounts");
744        println!("{:?}", accounts);
745        assert!(
746            accounts.mint.to_string()
747                == "6kPvKNrLqg23mApAvHzMKWohhVdSrA54HvrpYud8pump"
748        );
749        assert!(
750            accounts.bonding_curve.to_string()
751                == "6TGz5VAFF6UpSmTSk9327utugSWJCyVeVVFXDtZnMtNp"
752        );
753        assert!(
754            accounts.associated_bonding_curve.to_string()
755                == "4VwNGUif2ubbPjx4YNHmxEH7L4Yt2QFeo8uVTrVC3F68"
756        );
757        assert!(
758            accounts.dev.to_string()
759                == "2wgo94ZaiUNUkFBSKNaKsUgEANgSdex7gRpFKR39DPzw"
760        );
761    }
762
763    #[tokio::test]
764    #[ignore]
765    async fn test_buy_pump_token() {
766        dotenv::from_filename(".env").unwrap();
767        // 0.00069 sol
768        let lamports = 690000;
769        let pump_accounts = PumpAccounts {
770            mint: Pubkey::from_str(
771                "5KEDcNGebCcLptWzknqVmPRNLHfiHA9Mm2djVE26pump",
772            )
773            .expect("parse mint"),
774            bonding_curve: Pubkey::from_str(
775                "Drhj4djqLsPyiA9qK2YmBngteFba8XhhvuQoBToW6pMS",
776            )
777            .expect("parse bonding curve"),
778            associated_bonding_curve: Pubkey::from_str(
779                "7uXq8diH862Dh8NgMHt5Tzsai8SvURhH58rArgxvs7o1",
780            )
781            .expect("parse associated bonding curve"),
782            dev: Pubkey::from_str(
783                "Gizxxed4uXCzL7Q8DyALDVoEEDfMkSV7XyUNrPDnPJ9J",
784            )
785            .expect("parse associated user"),
786            metadata: Pubkey::default(), // not required
787        };
788        let wallet = Keypair::read_from_file(env("FUND_KEYPAIR_PATH"))
789            .expect("read wallet");
790        let rpc_client =
791            RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
792        let tip = 50_000;
793        buy_pump_token(
794            &wallet,
795            rpc_client
796                .get_latest_blockhash()
797                .await
798                .expect("get blockhash"),
799            pump_accounts,
800            100_000,
801            lamports,
802            tip,
803        )
804        .await
805        .expect("buy pump token");
806    }
807
808    #[tokio::test]
809    async fn test_get_bonding_curve_incomplete() {
810        let rpc_client =
811            RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
812        let bonding_curve_pubkey = Pubkey::from_str(
813            "Drhj4djqLsPyiA9qK2YmBngteFba8XhhvuQoBToW6pMS", // some shitter
814        )
815        .expect("parse bonding curve");
816
817        let bonding_curve =
818            get_bonding_curve(&rpc_client, bonding_curve_pubkey)
819                .await
820                .expect("get bonding curve");
821
822        println!("{:?}", bonding_curve);
823
824        assert!(!bonding_curve.complete);
825        assert_ne!(bonding_curve.virtual_token_reserves, 0);
826        assert_ne!(bonding_curve.virtual_sol_reserves, 0);
827        assert_ne!(bonding_curve.real_token_reserves, 0);
828    }
829
830    #[tokio::test]
831    async fn test_get_bonding_curve_complete() {
832        let rpc_client =
833            RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
834        let bonding_curve_pubkey = Pubkey::from_str(
835            "EB5tQ64HwNjaEoKKYAPkZqndwbULX249EuWSnkjfvR3y", // michi
836        )
837        .expect("parse bonding curve");
838
839        let bonding_curve =
840            get_bonding_curve(&rpc_client, bonding_curve_pubkey)
841                .await
842                .expect("get bonding curve");
843
844        println!("{:?}", bonding_curve);
845
846        assert!(bonding_curve.complete);
847        assert_eq!(bonding_curve.virtual_token_reserves, 0);
848        assert_eq!(bonding_curve.virtual_sol_reserves, 0);
849        assert_eq!(bonding_curve.real_token_reserves, 0);
850    }
851
852    #[tokio::test]
853    async fn test_get_pump_token_amount() {
854        // captured from prod
855        let bonding_curve = BondingCurveLayout {
856            blob1: 6966180631402821399,
857            virtual_token_reserves: 1072964268463317,
858            virtual_sol_reserves: 30000999057,
859            real_token_reserves: 793064268463317,
860            real_sol_reserves: 999057,
861            blob4: 1000000000000000,
862            complete: false,
863        };
864        let lamports = 500000;
865        let expected_token_amount = 17852389307u64;
866        let token_amount = get_pump_token_amount(
867            bonding_curve.virtual_sol_reserves,
868            bonding_curve.virtual_token_reserves,
869            Some(bonding_curve.real_token_reserves),
870            lamports,
871        )
872        .expect("get token amount");
873        // allow 10% less or more
874        // 0.9 * expected < actual < 1.1 * expected
875        let low_thresh = 0.9 * expected_token_amount as f64;
876        let high_thresh = 1.1 * expected_token_amount as f64;
877        let token_amount = token_amount as f64;
878        assert!(token_amount >= low_thresh);
879        assert!(token_amount <= high_thresh);
880    }
881}