├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── data ├── 6ogzHhzdrQr9Pgv6hZ2MNze7UrzBMAFyBBWUYp1Fhitx │ └── 2024-03-06 │ │ └── whales │ │ ├── analysis_march6.txt │ │ └── whale_details.csv ├── 8dtiEtPqkHJgeN5fy7iVg2gVAQMMzdSjjzvdjkjbM32j │ └── 2024-03-06 │ │ └── whales │ │ └── whale_details.csv ├── A3eME5CetyZPBoWbRUwY3tSe25S6tb18ba9ZPbWk9eFJ │ └── 2024-03-06 │ │ └── whales │ │ └── whale_details.csv ├── A6rSPi9JmJgVkW6BatsA6MjFYLseizPM2Fnt92coFjf4 │ └── 2024-03-08 │ │ └── whales │ │ └── whale_details.csv ├── AZsHEMXd36Bj1EMNXhowJajpUXzrKcK57wW4ZGXVa7yR │ └── 2024-03-06 │ │ └── whales │ │ └── whale_details.csv ├── CY2E69dSG9vBsMoaXDvYmMDSMEP4SZtRY1rqVQ9tkNDu │ └── 2024-03-06 │ │ └── whales │ │ └── whale_details.csv ├── EEfuxw7vmtoqG3EWzJAo6Txb5z1ci5wvMw2wrHwdQSq1 │ ├── 2024-03-06 │ │ └── whales │ │ │ └── whale_details.csv │ └── analysis_march6.tx └── JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN │ ├── 2024-03-06 │ └── whales │ │ └── whale_details.csv │ └── 2024-03-07 │ └── whales │ └── whale_details.csv ├── diesel.toml ├── migrations ├── .keep ├── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql ├── 2024-02-09-230011_01_schema_v1 │ ├── down.sql │ └── up.sql └── init-db.sql ├── public ├── birdeye │ └── amm_providers.json └── idls │ └── jupiter_aggregator_idl.json ├── run-migrations.sh └── src ├── db ├── db_session_manager.rs └── mod.rs ├── decoder ├── mod.rs └── tx_decoder.rs ├── http ├── base_http_client.rs ├── http_client_error.rs ├── mod.rs └── moralis_http_client.rs ├── main.rs ├── models ├── mod.rs ├── moralis │ ├── mod.rs │ └── token_price_response.rs └── solana │ ├── alchemy │ ├── get_program_accounts.rs │ ├── get_token_account_balance.rs │ ├── get_token_accounts_by_owner.rs │ ├── get_token_largest_accounts.rs │ ├── get_token_supply.rs │ └── mod.rs │ ├── mod.rs │ ├── solana_account_notification.rs │ ├── solana_block_notification.rs │ ├── solana_event_types.rs │ ├── solana_logs_notification.rs │ ├── solana_program_notification.rs │ └── solana_transaction.rs ├── schema.rs ├── scraper ├── birdeye_scraper.rs └── mod.rs ├── server ├── endpoints │ ├── accounts.rs │ ├── birdeye │ │ ├── mod.rs │ │ └── token_prices.rs │ ├── holders.rs │ ├── mod.rs │ ├── new_spls.rs │ ├── signatures_for_address.rs │ ├── transactions.rs │ └── whales.rs ├── http_server.rs ├── mod.rs └── ws_server.rs ├── strategy.md ├── subscriber ├── consume_stream.rs ├── mod.rs ├── websocket_event_types.rs └── websocket_subscriber.rs ├── trackers ├── binance │ ├── depth_tracker.rs │ ├── mod.rs │ └── rsi_tracker.rs ├── mod.rs └── raydium │ ├── mod.rs │ └── new_token_tracker.rs └── util ├── event_filters.rs ├── mod.rs └── serde_helper.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | /.env 4 | Dockerfile 5 | docker-compose.yml 6 | .debug/ 7 | .vscode/ 8 | prompts/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-sniper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Julian Martin Irigoyen "] 6 | description = "Websocket Server for PolygonIO - extendable to whatever you want. " 7 | license = "MIT" 8 | repository = "git@github.com:JulianIrigoyen/solana-sniper.git" 9 | readme = "README.md" 10 | keywords = ["web", "websocket", "async", "polygon", "crypto", "web3", "rust"] 11 | categories = ["web-programming", "asynchronous", "crypto", "web3", "finance", "trading", "data"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | # Flexible concrete Error Reporting type built on std::error::Error with customizable Reports 17 | eyre = "0.6" 18 | # low-latency cyclic dataflow computational model, which allows for the development of streaming data processing and iterative computations. This crate provides a framework for writing programs that can process data as it arrives and can also iterate over data for algorithms that refine results over time. 19 | timely = "0.12" 20 | # runtime for writing reliable, asynchronous, and slim applications with the Rust programming language. It is built on the async/await syntax for asynchronous programming. The features specified (full, io-util, sync, rt-multi-thread) enable various utilities for I/O, synchronization primitives, and multi-threaded runtime support. 21 | tokio = { version = "1", features = ["full", "io-util", "sync", "rt-multi-thread", "macros"] } 22 | # Rust implementation of the WebSocket protocol. This crate provides the core WebSocket capabilities used by tokio-tungstenite for both synchronous and asynchronous networking code. 23 | tungstenite = "0.21.0" 24 | # asynchronous WebSocket client and server library built on top of Tokio. It uses Tungstenite for WebSocket protocol support and provides async/await APIs for WebSocket communication. The native-tls feature enables TLS support for secure WebSocket connections. 25 | tokio-tungstenite = { version = "*", features = ["native-tls"] } 26 | # safe, extensible ORM and query builder for Rust. Diesel allows you to interact with databases in a Rustacean way, focusing on safety and expressiveness. The postgres feature enables support for the PostgreSQL database 27 | diesel = { version = "2.1.4", features = ["postgres", "r2d2", "serde_json"] } 28 | r2d2 = "0.8.9" 29 | 30 | 31 | uuid = { version = "1.7.0", features = ["v4", "fast-rng", "macro-diagnostics"]} 32 | 33 | # framework for serializing and deserializing Rust data structures efficiently and generically. The derive feature enables the use of derive macros (Serialize, Deserialize) to automatically implement the serialization and deserialization code for custom data structures. 34 | serde = { version = "1.0", features = ["derive"] } 35 | serde_json = "1.0" 36 | serde_derive = "1.0" 37 | 38 | log = "0.4.17" 39 | # provides utilities for working with futures and async programming in Rust. It's part of the futures ecosystem, which is foundational for asynchronous programming in Rust with futures and async/await. 40 | futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] } 41 | #Provides URL parsing and manipulation with the Rust type system. This crate is useful for working with web and network programming, allowing you to construct, parse, and manipulate URLs. 42 | url = "2.5.0" 43 | # Provides multi-producer, multi-consumer channels for message passing. This crate offers a more flexible, powerful alternative to the standard library's mpsc with additional features like select! for waiting on multiple channels. We use this to feed data from the websockets into timely dataflows. 44 | crossbeam-channel = "0.5" 45 | #Loads environment variables from a .env file into the process's environment variables. 46 | dotenv = "0.15.0" 47 | dotenv_codegen = "0.15.0" 48 | actix-web = "4.0" 49 | actix = "0.13" 50 | 51 | 52 | reqwest = { version = "0.11.24", features = ["json", "blocking"] } 53 | async-trait = "0.1.77" 54 | lazy_static = "1.4.0" 55 | solana-client = "1.18.3" 56 | solana-sdk = "1.18.3" 57 | rust_decimal = "1.0" 58 | hashbrown = "0.14.3" # or use std::collections::HashMap 59 | csv = "1.1" 60 | chrono = "0.4" 61 | anchor-client = "0.29.0" 62 | mpl-token-metadata = "4.1.1" 63 | borsh = "1.3.1" 64 | fantoccini = "0.20.0-rc.7" 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust is the best , Nothing is impossible wtih Rust 2 | 3 | ** Ofc , I have scniping bot which is made with typescript but rust is perfect 4 | 5 | [RTFM](https://solana.com/es/docs/rpc) <3 6 | 7 | "Me cabe la de disparar, pero hay que apuntar un poquito..." - Emiliano Migliorata 8 | 9 | ## Features: 10 | 11 | 1. Analyzes holders and token distribution for a given SPL address. Follow this example to basically do anything you want. 12 | 2. Track new tokens => 13 | 1. Subscribes to logs mentioning Raydium (more to come) and filter initialize2 events. 14 | 2. Grabs the signature and fetch the transaction to find the main token address. 15 | 3. Fetches new token metadata 16 | 17 | 3. Track wallets => working! Checkout l179 of main. Input your wallet and start tracking (must run the project! 18 | 1. Subscribes to the logs of the whale (l166) 19 | 2. Receive transaction signatures after consuming and parsing websocket events. 20 | 3. Fetches the transaction 21 | 4. Parses transaction instructions (for now, only supports TransferChecked instructions) and pre/post token balances 22 | 5. Outputs summary -> TODO: TELEGRAM BOT HERE 23 | 24 | 4. WIP: Smart Whale tracking 25 | 1. Manually input [10 wallets](https://birdeye.so/leaderboard/7D?chain=solana) 26 | 2. Track wallet transactions in 5/10 minute intervals. 27 | 3. Persist a report for the given interval with Transaction Summaries 28 | 4. Feed the summaries of each wallet to GPT to create a whale overall summary for the given interval 29 | 5. Feed the each whale's summary of summaries to GPT to create an overal 10 minut summary. 30 | 6. Persist, train, repeat, test, DBAB 31 | 32 | 33 | 34 | ## Nice to have: 35 | 36 | ### Technical Indicators 37 | 38 | - You receive alerts when there are some buy/sell signals following professional technical indicators. Recommended for professional users. 39 | (Example: "EMA of SOL/USD 1h cross up 70”) 40 | 41 | ### Token Stats Performance 42 | 43 | - You receive alerts when token(s) changes its non-price parameters such as volume, number of trades, ect. in a certain time frames. 44 | (Example: "SOL Price Change % in 1h is greater than 30%”) 45 | 46 | ### Trading Events 47 | 48 | - You receive alerts when specific actions happened, such as large buys, large sells or any trades by a wallet. 49 | (Example: "Wallet HhfmVzo...NxAFFKbWU2h (Solana) has a trade with value greater than $100k at Jupiter”) 50 | 51 | Market Movements 52 | 53 | You receive notifications following market events such as new trending tokens or new tokens listed. 54 | (Example: "SOL gets into Top10 Trending list") 55 | 56 | 57 | 58 | 59 | ### Run 60 | 61 | You will need to install [Rust](https://doc.rust-lang.org/book/ch01-01-installation.html) and [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html). 62 | Pull the repo, cd into it and run: 63 | 64 | 1. `cargo build` 65 | 2. `cargo run` 66 | 67 | This app uses Actix to expose an HTTP server, which you can test by making a request to `http://localhost:8080/api/holders` with the following body: 68 | ```json 69 | { 70 | "token_mint_addresses":["DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"] 71 | } 72 | ``` 73 | 74 | ##### Endpoints 75 | The list below summarizes the available endpoints through RPC aganst a public or private solana validator node: 76 | 77 | 1. **getLatestBlockhash** 78 | Purpose: Fetches the latest block hash along with its validity period. Essential for ensuring transactions are recent and will be accepted by the network. 79 | 2. **getProgramAccounts** 80 | Purpose: Retrieves all accounts owned by a specific program, useful for monitoring smart contracts, especially DeFi protocols, and NFT collections. 81 | 3. **getSignaturesForAddress** 82 | Purpose: Returns the signatures of transactions that involve a specific account. This is crucial for tracking transactions related to specific tokens or wallets, providing insights into market activity. 83 | 4. **getTransaction** 84 | Purpose: Fetches a confirmed transaction by its signature. Vital for analyzing transaction details, including participants, token amounts, and more. 85 | 5. **getAccountInfo** 86 | Purpose: Retrieves information about a specific account, including its current balance and owner program. This can be used to monitor the balances of key accounts, such as token treasuries or large holders. 87 | 6. **getTokenAccountBalance** 88 | Purpose: Returns the token balance of a specific SPL token account. It's useful for tracking the distribution and concentration of tokens among holders. 89 | 7. **getTokenAccountsByOwner** 90 | Purpose: Finds all SPL token accounts owned by a specific account. This is useful for identifying all the tokens held by a particular investor or contract. 91 | 8. **getTokenSupply** 92 | Purpose: Provides the total supply of an SPL token. Monitoring changes in token supply can offer insights into inflationary or deflationary pressures on a token's value. 93 | 9. **getSlot** 94 | Purpose: Retrieves the current slot, which is a measure of time in the Solana blockchain. It's useful for understanding the blockchain's state and the timing of transactions. 95 | 10. **getSlotLeader** 96 | Purpose: Identifies the current slot leader, which is the validator node responsible for producing blocks in the current slot. This can provide insights into network dynamics and validator performance. 97 | 98 | ## Use Case 99 | 100 | ### Identifying and Monitoring Top Traders 101 | - Use getSignaturesForAddress and getTransaction to track the activities of known wallets associated with top traders. 102 | ### Token Holder Analysis for Decentralization and Whale Tracking 103 | 104 | #### WebSocket Data: 105 | 106 | 1. [accountSubscribe](https://solana.com/es/docs/rpc/websocket/logssubscribe): Monitor changes to specific accounts in real-time, such as token balances changing. 107 | 2. [logsSubscribe](https://solana.com/es/docs/rpc/websocket/logssubscribe): Get real-time streaming of transaction logs, useful for live monitoring of contract interactions. 108 | 3. [signatureSubscribe](https://solana.com/es/docs/rpc/websocket/signaturesubscribe): Subscribe to receive a notification when the transaction with the given signature reaches the specified commitment level. 109 | 4. [blockSubscribe](https://solana.com/es/docs/rpc/websocket/blocksubscribe): Subscribe to receive notification anytime a new block is confirmed or finalized. 110 | 5. [programSubscribe](https://solana.com/es/docs/rpc/websocket/programsubscribe): Subscribe to a program to receive notifications when the lamports or data for an account owned by the given program changes 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /data/6ogzHhzdrQr9Pgv6hZ2MNze7UrzBMAFyBBWUYp1Fhitx/2024-03-06/whales/analysis_march6.txt: -------------------------------------------------------------------------------- 1 | Analyzing the provided information about RETARDIO, we can derive several insights about its distribution, market performance, and overall ecosystem health. Let's break down the analysis into sections: 2 | 3 | ### Token Distribution and Holder Analysis 4 | 5 | 1. **Concentration of Ownership**: The top holders' list shows a somewhat concentrated distribution of tokens among the top addresses. The top holder owns 2.43% of the total supply, with the top 3 addresses together holding over 6.69%. This concentration might indicate a risk of market manipulation if these holders decide to sell a significant portion of their holdings simultaneously. 6 | 7 | 2. **Decentralization Aspect**: With 9,880 current holders, the asset shows a decent level of decentralization in terms of holder count. However, the concentration among top holders suggests that the asset's influence is still somewhat centralized. 8 | 9 | 3. **Inferred Total Supply**: By using the highest owned percentage and corresponding amount, we can estimate the total supply. Assuming the top holder's 2.4273571854573526264136012300% represents 24,267,288.326759 tokens, the total supply is approximately 1,000,000,000,000 tokens when considering decimals and ownership percentage. 10 | 11 | ### Market Data Analysis 12 | 13 | 1. **Price Stability and Market Cap**: The current price of $0.01964 with a market cap of $19.6M suggests that the asset has a relatively low per-unit value but a reasonable market cap, indicating a broad distribution of tokens and active trading. 14 | 15 | 2. **Liquidity and Fully Diluted Valuation (FDV)**: The liquidity of 0.0001540 SOL and a FDV of $590K compared to the market cap suggests that while the market is active, the liquidity pool might be on the smaller side, which could lead to volatility in the asset's price. 16 | 17 | 3. **Transaction Activity and Volume**: High transaction activity (14,470 TXNS) and a total trading volume of $6.8M indicate a healthy level of market engagement. The ratio of buys to sells and the close buy and sell volumes suggest a balanced market dynamic, with slightly more buying pressure. 18 | 19 | 4. **Community Engagement**: With 2,457 makers, 1,749 buyers, and 1,427 sellers, the asset demonstrates a robust and active community. This level of engagement is crucial for maintaining liquidity and market stability. 20 | 21 | ### Performance Metrics 22 | 23 | 1. **Short-Term Gains**: Significant increases in value over short periods (18.36% in 1H, 23.46% in 6H, 28.89% in 24H) indicate strong short-term performance. This could attract more short-term traders but also signals potential volatility. 24 | 25 | 2. **Holder's Sentiment**: The proportion of buys to sells and the number of buyers versus sellers suggest a positive market sentiment, as more participants are inclined to buy rather than sell. 26 | 27 | ### Conclusion 28 | 29 | RETARDIO demonstrates a healthy market with active participation and significant short-term gains, suggesting positive market sentiment. However, the concentration among the top holders poses a risk to price stability and decentralization. The liquidity and FDV metrics, along with transaction activity, indicate an engaged community and an asset with potential for growth, but investors should be cautious of potential volatility due to the concentrated holdings and the liquidity pool size. The overall ecosystem appears vibrant, but strategic considerations should be made regarding the decentralization of holdings and enhancing liquidity to support sustainable growth. -------------------------------------------------------------------------------- /data/6ogzHhzdrQr9Pgv6hZ2MNze7UrzBMAFyBBWUYp1Fhitx/2024-03-06/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | CFJxcPREabsh4788hHcshb2DoZecHraXtqLDp3U49KQf,24267288326759,6,24267288.326759,2.4273571854573526264136012300 3 | Kt4fAS5qVCtEDufaxXY9BMKscAfaK19An9HdzFaJnBf,24000114031409,6,24000114.031409,2.4006328379795913537920899200 4 | Bsx5tvzXheD7v25pbumYo1VWBZd3R9eNLusLZyUxhSES,18687328904666,6,18687328.904666,1.869216761385223365171237900 5 | 9UVCyi4ppGK4L9g9soqAcEEhqhB1ftoe8ojxJRdeoNZk,15835018518549,6,15835018.518549,1.5839118679142350058046816700 6 | HXzTvbuKKPyNMmLKJb8vaSUaRZsVS2J2AAsDuDm36rNC,15496202650526,6,15496202.650526,1.5500215081542725799431531400 7 | DGWxPL7hmG3nMLSXBVKjoJAksxhvbHpQiJixyQm78Z11,13830531271262,6,13830531.271262,1.383411241006755918459061500 8 | FjFsrTEcBGX5SE8BiHdjS1QiuRWUWF21dBDN6ULoSJe2,13300000000000,6,13300000,1.3303443768368674612697933800 9 | Dfr714h41mf1nvc4qZ9d9WdLFV5QUuVAGhZydFjSgQ8Z,11554281320793,6,11554281.320793,1.155727307030693344804288200 10 | CLsQvTqtRTXwGQiqMwTM6y7Ky4QrhiKtsL8BbrfcE22E,10580320809484,6,10580320.809484,1.0583060371448972238523940100 11 | CFKxU872aU7RhG829mL9qbHA1FfmEUd7m21gm37ToyAV,10304404000000,6,10304404,1.0307072118838589785998724800 12 | 5CDTpgAdpV7RBE3iV6SFEwA6v9t1fdoAmMeLn8jJQtqA,10279050537175,6,10279050.537175,1.0281712091242663988804171500 13 | 5ZgCioMEvJyd5U7XcisXi5WNp28mvom7M6vsTMdfdMfb,10138111124734,6,10138111.124734,1.0140736185366290763166309900 14 | B7vFc2LMzT8dFzUWuAqsb44ppsf8BhsbkBFVGobf1ovn,10000000000000,6,10000000,1.0002589299525319257667619400 15 | GKSdtgFxmzsXfkYXb2QpKJ3gCuJLTG3o3kr71UaoaFKU,10000000000000,6,10000000,1.0002589299525319257667619400 16 | 5n92DTvkmrwjo1h2xCs44jEyAdwgeFBd7f2RTg21epoR,10000000000000,6,10000000,1.0002589299525319257667619400 17 | CDQhWbHqVFoQJtP2NZomBU88ru6jv82M2XVmh6GbcGsU,9998214998477,6,9998214.998477,1.0000803835811959637862419600 18 | GCk1aJiRwJaQhix56BE79GTfLPKVpXR3w7QkwrG7dcXP,9958971989667,6,9958971.989667,0.9961550665811551254617748300 19 | 5MDNmwK32ajwwDoehSQaraS5sPniiADJY2kYwsYDoTRV,9849601391198,6,9849601.391198,0.9852151748018681288134808500 20 | 9KG9knRqDzgLsvph9e1x5FHg51LNwpopfXmLxNUePJSd,9616540433127,6,9616540.433127,0.9619030443484870918963392300 21 | 8yeJT39at479BeeUDCZVMTkBicNMQvN1w3cgcFgUdiwo,9233458426386,6,9233458.426386,0.9235849245338049636966791900 22 | -------------------------------------------------------------------------------- /data/8dtiEtPqkHJgeN5fy7iVg2gVAQMMzdSjjzvdjkjbM32j/2024-03-06/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | 9hRuDnUJeG4Ba6LMuyrLZVAGXRj77nRXg8Ydv42hA2mi,2725131790526712,9,2725131.790526712,2.7252135095360814375276377400 3 | 25aL4PVL3rwSijYFNEBkL7iqFASWwPHjBbPDwZ1zxYsd,2449128160314130,9,2449128.16031413,2.4492016027537857278686458100 4 | BvPZGzSav3JVRwJkfoGz1tEBb85nuruLoshbYJ56xjSn,2386139100832610,9,2386139.10083261,2.3862106544081895623987763700 5 | 2bK2iT54B3MrwD7Emog4AhoMMNhLvxuYzTYV34S191CY,1661610254500729,9,1661610.254500729,1.6616600815015470610115457700 6 | E1A9hrnof8vXrebw8eogvVsgiiDhHPZ4fnWV1ccGJ7kz,1328827140519650,9,1328827.14051965,1.328866988294324229555475600 7 | 3rBxrKKPCP7tVAYtviSKWUA3scBhEeuTfH3wUG17xe8n,1284832289504392,9,1284832.289504392,1.2848708179976814463280594900 8 | BfyRBwoQ2zQMXxP7a5WMCwEiqxtUz2VqVmCZTuJTzHcW,1271066828177050,9,1271066.82817705,1.2711049438830140427846198900 9 | 5rVMMs8NWxq57UF5coQoMTGXBR2L4XohpsmiELhQd28L,1051569187352598,9,1051569.187352598,1.0516007209439540604868510300 10 | 51oGY78synBCV4SAAtHThCEeWN5FXHurLDBJR9FsQMJ4,980895678493191,9,980895.678493191,0.980925092785527200369728100 11 | 9zaCK7LYFC7S3jf2nw2cTRFevHgTYNN32kFpt3Zv9RSL,975814952063271,9,975814.952063271,0.9758442139989644231287462500 12 | 9eXgWbCBKkauVYE3wasnfRJ7KnTjWTNUwgWTeSayfUjV,974712540031972,9,974712.540031972,0.9747417689094407061173629900 13 | 7htmUUJVYPHn3AHCeQqMEgzg5C4mtMxj8aRQTpFJowAH,819531851478042,9,819531.851478042,0.8195564269247349991726573500 14 | 9tVbCeNvmobKnuoBjAgG7VTLps1AU6t4DFnYT4jdEJDH,800499485456412,9,800499.485456412,0.8005234901761762499624436600 15 | Fwb6w1s4tTuu7iutm8R6fBoHaMv45xgZP6aDiqRnuKsR,758504905396003,9,758504.905396003,0.758527650816861651393355300 16 | GZ8qtRhRC2qHwrEowWP3toSvKZcsahj6483BsojRji4L,705003470778165,9,705003.470778165,0.7050246118420336474645053400 17 | J6upcVCKHistZeQ532pAn9CmaqSKFYSf6zcPeubFAEv8,627601578883238,9,627601.578883238,0.6276203988828737315225724100 18 | CATzFqgu2bAR89HiJADgCXXyZMDc7sy5FunnpkmtmGiZ,590292889563983,9,590292.889563983,0.5903105907813482225586325200 19 | G6xo5ez3gxQvbcvW2xw57swdaEkC1La4FEqB89vFefhe,583463286422685,9,583463.286422685,0.5834807828395319381198669500 20 | 3dZSZyw58fpL1CvXqF4Hfrtcs9MFnQ51U9QDTNyfL6JT,577800804074888,9,577800.804074888,0.5778181306898744767360986100 21 | DVPC1WA14LztVn1cTarwChw4fPR1CNqo8J38Bs55TYdw,575227816024269,9,575227.816024269,0.5752450654826073663907533700 22 | -------------------------------------------------------------------------------- /data/A3eME5CetyZPBoWbRUwY3tSe25S6tb18ba9ZPbWk9eFJ/2024-03-06/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | 2g5q7fBGKZm2CXix8JjK4ZFdBTHQ1LerxkseBTqWuDdD,2156016064260,6,2156016.06426,2.1560164188198969377542110700 3 | EPgu5VBTUFqtf4eBqLpWXSvBmjcDXzuZnzpkMkP8mxtR,1366415393642,6,1366415.393642,1.3664156183509477555228662100 4 | 8WVUuDk5gZaC73yoafsTwCetmeGwmwZBukhmkiYQaKFy,1184941418277,6,1184941.418277,1.1849416131422953500927381100 5 | 8qPLp94Y8ahD4ovEUWW55NwrS5btoqPEHhbZaTKWU9Ub,1001215344000,6,1001215.344,1.0012155086512820872603645300 6 | HYczm247BiqoMJUYq7b1kRf1JT3rEUBzWHS8PVuLnzbP,1001166896000,6,1001166.896,1.0011670606433147450074040200 7 | CiFXoHPYKXWB6A3cXiFSaZWMmQYUu4GzcqMZySDEY1o1,1000279896000,6,1000279.896,1.0002800604974463380891417500 8 | 8w16tYQwQVjhUWz5uSUxBz2nNycZakkqvyaY6NCncfST,1000263896000,6,1000263.896,1.0002640604948151154164335200 9 | GNDRYxg8Qq4qLySXJZkHqe8GYF5KSgKL36J1gBrQq3p3,1000000000000,6,1000000,1.000000164451417044264120400 10 | FmtrjYuaQs1fSbRYMkpmaXbaQyvfDFX7KZZETTPWiYMW,1000000000000,6,1000000,1.000000164451417044264120400 11 | 8haxQ2BQZx7uFMPiv2ZoKTwGd7yXN5m4nxAK68VrW8CG,1000000000000,6,1000000,1.000000164451417044264120400 12 | BcUrTTAWW373FVT1Sivj8nAsKnpMFxgu43BFoVqEEwrP,1000000000000,6,1000000,1.000000164451417044264120400 13 | 5D8PLukugShJ2bVFAJEWCGigNQQJXDAq6F2Jdmtdo6Rm,999439640804,6,999439.640804,0.9994398051632651804281358600 14 | 36RNALNGV6TXx2H8CryJATRQjva5Rs59BhtCL8ZtifiM,822368421053,6,822368.421053,0.822368556292652174619896900 15 | dUQ4EqP3UTvhxBr7Xm91X3tUJQZJRZj5MvP6FNgh1y5,548849814323,6,548849.814323,0.5488499045821297098985999500 16 | B8vPjFgtuvZp4XfmXyUx2yXttSUdbpriMfBDR8HGUK4T,533372400093,6,533372.400093,0.5333724878068470075940419200 17 | H1gRPD6ctAaeRm1RL1tuqviJGQsQRPcpqa7MCgKrANLV,522000000000,6,522000,0.5220000858436396971058708500 18 | 2AhhtJ3xbxUakCFFTkFDxd5bYZ4G4DTxQzTcKsgqkzVD,500886448000,6,500886.448,0.5008865303714861518681140400 19 | 4fk7KcNEE9R8yj2msiijQLnzEY3e44xSjhe8FbxahnPv,500230448000,6,500230.448,0.5002305302636060222870767800 20 | D2v5reTGgQAfjikcB8KEYy23XTFtsuv2k6KTZjbi65VW,500000000000,6,500000,0.500000082225708522132060200 21 | 8bdL4VhzGNRcAXAJVhSRbJ954rroPGtTindS1XqMPM47,500000000000,6,500000,0.500000082225708522132060200 22 | -------------------------------------------------------------------------------- /data/A6rSPi9JmJgVkW6BatsA6MjFYLseizPM2Fnt92coFjf4/2024-03-08/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | 9uWrDHUwNqpPk8MyUy1XDbVwnJbFSUCF2jgVQC5DRMqo,10000000000000000,8,100000000,10.526752703948971967066592060 3 | GUVkL2jvi3HTv1anv8p1nfKLQPFXmMSRDEBvDB4iu1GY,7251089107859803,8,72510891.07859803,7.6330421872738120069358366300 4 | 3QJ1unQMiifmdU83w2q7aGRvH9hYrE714oiYEGm1Zkwm,7171799016449221,8,71717990.16449221,7.5495754688585414844040430400 5 | CiL2Be2WxJVgzU2dypxHL6TCCTfssPN57RKGTaPLF7RP,2342796699185736,8,23427966.99185736,2.4662041487956172729107814800 6 | EAFetLGYYENLWvpdidECafMoivrBByEcPERv43QfGdJn,2011197338976109,8,20111973.38976109,2.1171377026241732563099880700 7 | AYo7fbcaLDM7cBL8RGSXq7o3eoawuyfoUa7BhMyYGVfR,1969412332828264,8,19694123.32828264,2.0731516599790380792246835600 8 | 7eq1or15G13H1Ly9ctpEcestMz1tZH7KijRnVeUpdjpG,1375778202039288,8,13757782.02039288,1.4482476908351130012832026400 9 | H3Ygr9zKfuCJUceB4fnjrLrXRkcaP1Sxfp7Gz6CjYRe3,1149551083245754,8,11495510.83245754,1.2101039973884710685484316900 10 | 8K2cgJUDR2gcuUq5UjjADQMEyR1tzxTvexKBHHkKqqys,1100000000000000,8,11000000,1.1579427974343869163773251300 11 | 4RskmUYA465MpkJQCGTXDBKvExdJWqcmVs7i2ecyxyQZ,1050722355538108,8,10507223.55538108,1.1060694397260411469081765500 12 | CUuyU7BW4D3phaKWeoyWWF8ZvYFrEpv5KKLuTtj1Yu2v,1025789633644765,8,10257896.33644765,1.0798233799652855311986186500 13 | 4GJzGSiPGBwQ7f6N2Qtvn7PKqKvgBdS67VzcGV7yLLuy,1004088406449996,8,10040884.06449996,1.0569790347601309777516800500 14 | ELusnTofqCfVkGzLvu3p7s8kzN18aDRE2yvadCiZzQb4,979652223623010,8,9796522.2362301,1.0312556693953143468188718700 15 | Gwuw8ztx9fmX4XHT76Z85JE6GQUn2n6zuxceaAdxXeCV,977008196121197,8,9770081.96121197,1.0284723670299118025058806100 16 | 6Wuo4Nvr6q4yvDugzPtuFHmM9hyTJLte9ko9rgYJv4YK,921406617318938,8,9214066.17318938,0.9699419600298606254694741200 17 | 2rAJhVftiU7LMigxTZQu9QDsNdRpoCsS2BRFZaQLEZ6p,821768428722700,8,8217684.287227,0.8650553029076580264736303200 18 | 3r3urr3gJKUHFG2L3PTJUvsJQchS9R2jYcPyE8oPFtGs,777276106682092,8,7772761.06682092,0.8182193357730641558606643100 19 | 8qQrDWCJ3RUnHdxzrSYc5Q9XWrppyTxABXPsQjQUHawV,739811854163330,8,7398118.5416333,0.778781643622733659148554800 20 | 91Tq7A5X1jJNwFNVg33Be3cQiYtVcixNBQum82hPQUPf,709424516680801,8,7094245.16680801,0.7467936449217314494034949100 21 | T7G1rZLD2K7Xi67qX2wXcKqfje293mhD3CPUJc1n1hJ,679944324306344,8,6799443.24306344,0.7157605754426563404980588100 22 | -------------------------------------------------------------------------------- /data/AZsHEMXd36Bj1EMNXhowJajpUXzrKcK57wW4ZGXVa7yR/2024-03-06/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | ExiHKVLFjZC9zWaZHKjSFUKkjodRoFVHWqsm177ZUK3W,729013146509727528,5,7290131465097.27528,7.3643167471810138621613711700 3 | AXy462mcfJFzujc5BfdAhUYj4nNPVvfGEmHPAEUvp7pc,382097878358061617,5,3820978783580.61617,3.8598615376506932855875984700 4 | qsWnMAzSGse3acGHciUGwwxjX15pvAxaMgsr3jAcRfg,257900000000000000,5,2579000000000,2.6052442239087123086833213300 5 | 2EDT3tK2kj3gCphsWmS6QVDAbcDHg9coDKPYDaxU4spW,251169467916997676,5,2511694679169.97676,2.5372539957851214655488255700 6 | 6wRSM7ijqh7rkf3rDN8EPrBzbZt8ZzEtGhGR9CCMMsuR,231810227330039154,5,2318102273300.39154,2.3416915695794882790841131200 7 | MHkJFcEjUz9yNtVYyEbBnU3BGLavoudJT4GJ4uFaFuy,219898456888912400,5,2198984568889.124,2.2213617086323304276405141300 8 | beJvU32WLVeyykT5UnWE3EPsKqnqS7mAW1oUC3Xw4hp,207039211221535251,5,2070392112215.35251,2.0914606791683620220073675100 9 | EVAEnTVXfXixcw9WKSQGZMumtpczyu67Ua4ZqizEwor2,178869203251834682,5,1788692032518.34682,1.8068939845172383329618574100 10 | TShZ6rXNNVqbQW3f7MppXXwyu9YaWf5cVm4XpDrFo2a,178860151440554528,5,1788601514405.54528,1.8068025452808927225106108200 11 | EdVKFriq7dKDMFpH85h1nEc6Tvuqget6t9oDgVwz2X9H,178860144082829381,5,1788601440828.29381,1.8068024709549100147570971100 12 | 3XDsS5N9YyuqSFeYXWpWPss3ECGz5zKkxYhMKKiFXZTf,178857550900395481,5,1788575509003.95481,1.8067762752451098615537002700 13 | 6HfVRGSQ21x1RvVTxUELoFuhoR9iuULEuRwqF2SQVVhr,128762017672801271,5,1287620176728.01271,1.3007231593675728803799197300 14 | 6XDducjQCZaYUY8VqhqdVCcJyDF7C1NLZaTyqi6AeAee,128426310687576999,5,1284263106875.76999,1.2973319275560903598023124300 15 | EWntoh754Pumdv4eVEH56KyTBnSdVUZ63JrPfawfn67S,127455687018336518,5,1274556870183.36518,1.2875269189951060883857577300 16 | Bgf2eN9J1fMqxNGKqcqdv6Ee7uVRisRimGZR2UwNrKgc,127396879843203336,5,1273968798432.03336,1.2869328629526861176025471700 17 | Cyi5huDps3H2aVx11h6ePj4wRiBjf9N1UcXGiDatN4ZD,125010000000000000,5,1250100000000,1.2628211726670342214366110900 18 | Gi7T4ZRaMLRimLrz7jDWfM59u1VapQ4HoEQJumFFXpWw,109358136626157175,5,1093581366261.57175,1.1047097858965326702815486300 19 | 4n1fK139SAYjqFTcnPXbtJ3xuJUfQtBgGbEDFYxeJCk1,105119739480803842,5,1051197394808.03842,1.0618945098920233808394544700 20 | 4VPeF21EjGAnCWEaepYw2YBm5qzrUdwkJqiAi9g63Quq,100000000000046409,5,1000000000000.46409,1.0101761240441726934762292100 21 | BCZ7LfaLzykvc4TeuHZMQg3H8TGc3RYCgrZqEpWr2c9q,100000000000000000,5,1000000000000,1.0101761240437038808388217600 22 | -------------------------------------------------------------------------------- /data/CY2E69dSG9vBsMoaXDvYmMDSMEP4SZtRY1rqVQ9tkNDu/2024-03-06/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | HoFJexihHX8cTj9J3idEHaVFeWFEPBFe4PasrCQSUeCR,49494542502470281,8,494945425.02470281,88.66352479953446803866390996 3 | JCy2pqZFHUc9ekse5iQKN64mXyT5D4vEd87hVTpfRqPf,598365363963823,8,5983653.63963823,1.0718996399318368352914521900 4 | 7Njn1vf1SWBq46XDxC9xuJiMQkRpz6J3ZKhoPBDw1fk2,586476787535064,8,5864767.87535064,1.050602683990283783219083400 5 | DEniwvcdx7Yh6US1mVhGRSdxczzhLiUonUSx8F4rDW8o,332141516410833,8,3321415.16410833,0.5949916109594725463089176900 6 | EwGxFjNzJojEaGPuBa6DgMHffgHXnf9mgQjoLks7eLm1,321172740438000,8,3211727.40438,0.5753423669960744130311090600 7 | Cn6QTwd49hTLPFiArLw36MuEk8Rw9emdSKjCYFprS2r2,319376969082858,8,3193769.69082858,0.5721254584233165242712622800 8 | HWDBNWPvBiHvNgSEvkHJSDPrXwvsik5SEhTX2h27gRdK,288021948255922,8,2880219.48255922,0.5159567067566008014290217200 9 | 9WrNzWuJauwUWPw4YCcngjYvYFsunbgAsrDbbZmM54U5,267933974364905,8,2679339.74364905,0.4799715156384145256606130600 10 | HcVRuhUhUCEzHHJX12b2BHYjiCoJZyR9XN8JQtZb4MHu,252524624732309,8,2525246.24732309,0.4523675176172956688131572800 11 | A4jAvCwPCzxbAvbiFaWJYjjTqZz9eW883Rjig2hXt5yW,245208978833221,8,2452089.78833221,0.4392624171596840394555361200 12 | 2knU1PfaT5Xry6TVzrwK14PABZwLbzJX9ywruNEaNJEA,221812812907661,8,2218128.12907661,0.3973509975794064893114017100 13 | BMoMA8CiBZ4EnuEGWYGRwLKS98VbNuxZCArKoE7S1bcZ,187287037008498,8,1872870.37008498,0.3355022192518600889058284500 14 | 24xfiNchqtfN1YEaepwHD8CckZrYSDUJLrFzfFmoXsf3,186018835694769,8,1860188.35694769,0.3332303890066365609856693800 15 | AMKLHdWpVRJAyZbPy3Dthq2zfCpecuKqUsMiqWMfXifw,156557673674001,8,1565576.73674001,0.2804542577933599720300695300 16 | 4RXAWbiEix7dmFnMxRQQt4ZL79sS6sFbgurGUMUhSgJo,119173431532912,8,1191734.31532912,0.2134848807146080488021556800 17 | Gaim6eHk5kzg5993feiHcuXf5BcqQUaatrg3N29fV4t6,118704447625278,8,1187044.47625278,0.2126447524050471636131639100 18 | N5k8ZzpWRVtjnd7M7Hai4J2qm8x5kJAK9AiAb9L5Usy,114429811546376,8,1144298.11546376,0.2049872555815984756059706900 19 | 5J3cKhAh1KHf7bWYGeAr82pnz5THGCgPyf1AaYeHp6Q5,110515382975114,8,1105153.82975114,0.1979750272195164856937132300 20 | CUhLZbyCwwgrAQtE4JoztztzX6cAQj571euMSTfzjeEg,99784412676901,8,997844.12676901,0.1787517835434851475331238500 21 | 6aB7eR6R6MrSog8F6HWedmA2LMnzzSsVq3HfyJAngoWy,97421345696684,8,974213.45696684,0.1745186330341542665697895400 22 | -------------------------------------------------------------------------------- /data/EEfuxw7vmtoqG3EWzJAo6Txb5z1ci5wvMw2wrHwdQSq1/2024-03-06/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | DpZhd2Vqzuevub13iawzqSLc5YnxVrMZKtJzEmFXD48Y,47860756807,6,47860.756807,4.786084486300964769049214900 3 | B8ij3nDW1DXKqYm2hVLicM8AZ8TjVon5Ue8GDo2eQPg5,30504514697,6,30504.514697,3.0504570820346550681475726500 4 | HZMwGLxCcGZyUr8gt5dRaBeAkMv4CwUCTibYGBN6HBda,15756728093,6,15756.728093,1.5756757082817410708288013100 5 | EH7JwvoDK1JgfuKc7qryzybVdb56TwB7BNnfRf9HP7z5,14035357961,6,14035.357961,1.4035383783776288440263866200 6 | 5zQfLFaoaD4U2kHwjekH7RD5FTJsRXuLYoQhtsY9LkuQ,11349135418,6,11349.135418,1.1349156298563629411516554700 7 | DzptE28BEeTzSom6szaKh2o7R4siXr959wrnHvprdEwC,10846754914,6,10846.754914,1.0846774870265196659497830800 8 | Csb7Rsh8bTT2brCyjBR5VGXGokxB4fj5yJURXYA21MCM,8796417141,6,8796.417141,0.8796433324977110025936247500 9 | 8nNzfGwWKLR4x5QkCuKHwC84WtaQbi7ADEjAqEztLfVo,8461356815,6,8461.356815,0.8461372382520596022398262300 10 | 8tUx7vWjo2LeQj4yGgyv1xwnB6eHGyDXPHRbCXqUBarL,8145654017,6,8145.654017,0.814566900367878571435503600 11 | 49NukQhctgDU1UpkY6NUHbf7mmmerg8KGLapVH9jzB3n,8018828643,6,8018.828643,0.8018843396340721263134564400 12 | 6AuHMgvjfQDRgdjb7P38QCXSWdRygV4sdo9FU162naUp,7529811590,6,7529.81159,0.7529825443628865253469674800 13 | 2okXEfNnx4Z4uGo75tHnPv1PVJiBgMjD7j6DBPrKnSvT,7526179051,6,7526.179051,0.7526192897945584198815958800 14 | 2cLhCwkqTrYnHznhd88kNMjDp9UimgL6W5NKK4D2smk6,7459401131,6,7459.401131,0.745941485508507049058558100 15 | 9DbuUb1pVTobbqTmPTxZ4Nm3CNV1e7Ure6MngDxh5arA,7192319680,6,7192.31968,0.7192332912698632101975094700 16 | Gw5Vweib526wwYeSQ7Z99wU7gqwJ1421x946ySe5reyN,6830296249,6,6830.296249,0.6830308815634389503881378500 17 | GsK9DSEBdBiAvFXvbWwHvPSqWU8H64wsoGohGDFksySn,6772556011,6,6772.556011,0.6772568471401741012950774800 18 | 7Z1XWrrDsPeKVP6kNdwbNE4nnWTZss1VMadXm2WWiSJ,6641244342,6,6641.244342,0.6641256560809623300612475700 19 | Hzvfzkb6MmKsF9tH1qYCptmaQkqyTvXhPTZziD9taxBj,6571691883,6,6571.691883,0.657170397384440901405441400 20 | 4D5t4a6UzKFFkdSLeWsMuJX355We9i7pZ8NdZD3QaGoq,6561293437,6,6561.293437,0.6561305508712959317398792800 21 | tddcHZuBvW9EhUPBykXk6Dy6jHYrKAahmRnrrvJZrhU,6557240257,6,6557.240257,0.6557252321255767225283237200 22 | -------------------------------------------------------------------------------- /data/EEfuxw7vmtoqG3EWzJAo6Txb5z1ci5wvMw2wrHwdQSq1/analysis_march6.tx: -------------------------------------------------------------------------------- 1 | ### Distribution and Holder Analysis 2 | 3 | - **Holders**: With 3,234 holders, PIP has a moderate level of decentralization. However, the ecosystem's health would depend on the broader spread of tokens among participants. 4 | - **Whale Concentration**: The presence of whales with significant ownership percentages, such as one holding 4.79% of the supply, indicates a concentration of power among a few. This could pose risks of price manipulation or sell-off impacts. 5 | 6 | ### Market Data Analysis 7 | 8 | - **Price and Market Dynamics**: PIP's price of $0.8691 and a market cap of $861K suggest a smaller but active market. The liquidity of $88K against an FDV of $861K highlights a tight market with a substantial portion of the total supply not actively traded or possibly locked. 9 | 10 | - **Performance Volatility**: The dramatic 24-hour performance increase of 41,434% is extraordinary, suggesting either a very recent launch with initial trading volatility, a specific event driving demand, or possibly speculative trading patterns. However, the short-term performances, with significant drops (-37.55% in 1 hour, -54.42% in 6 hours), indicate extreme volatility that could represent high risk for investors. 11 | 12 | - **Transaction Activity**: The high number of transactions (47,472) and the balanced buy and sell volumes ($10.7M each) reflect a highly active market. This level of activity, combined with the number of buyers (7,026) and sellers (5,014), indicates a lively trading environment, which is a positive sign of market engagement. 13 | 14 | - **Liquidity Pool**: The liquidity pool information, with pooled PIP and SOL roughly equal in value ($44K each), provides insight into the market's depth for PIP/SOL trading pairs. A balanced liquidity pool is essential for facilitating smooth trades without causing significant price impacts. 15 | 16 | ### Key Insights 17 | 18 | - **Market Engagement**: The high transaction volume and active maker and buyer/seller participation demonstrate robust market engagement, which is vital for the token's long-term viability. 19 | 20 | - **Volatility and Risk**: The extreme short-term volatility highlights the speculative nature of the PIP market. Potential investors should be cautious, as such volatility can lead to significant price swings, affecting investment value rapidly. 21 | 22 | - **Potential for Growth**: The significant interest, as evidenced by transaction volumes and performance metrics, suggests potential for growth, especially if the project behind PIP can capitalize on this momentum and increase utility or adoption of the token. 23 | 24 | - **Concerns**: The concentration among top holders and the extreme price volatility are primary concerns. Strategies to improve token distribution and stabilize the market might benefit the long-term health of the PIP ecosystem. 25 | 26 | In conclusion, PIP presents an intriguing case of a cryptocurrency with high market activity and extreme volatility. While the token shows signs of an engaged community and active trading, potential investors should approach with caution due to the risks associated with whale concentration and price fluctuations. The future prospects for PIP would significantly depend on developments that address these concerns and enhance its underlying value proposition. -------------------------------------------------------------------------------- /data/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN/2024-03-06/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | 26ddLrqXDext6caX1gRxARePN4kzajyGiAUz9JmzmTGQ,4090152115064232,6,4090152115.064232,40.901604035676385655897017970 3 | 6G4XDSge4txj5tBkA5gZqefXXE3BRxqA37Yb1pmrHv6N,3500000000000000,6,3500000000,35.000070925875387703014252070 4 | 7bEL9XkPGNg7a3oSiUERKsZtFoJMqKh6VhGDCmMCPqdg,650000000000000,6,650000000,6.5000131719482862877026468100 5 | K6U4dQ8jANMEqQQycXYiDcf3172NGefpQBzdDbavQbA,500000000000000,6,500000000,5.0000101322679125290020360100 6 | GDijXRGfd1jhk5GkRPp1TDnEH2FfHURSRZeB5um3pUWk,190039583545000,6,190039583.545,1.9003996865139489267560693200 7 | 4QDgg3MSLiy3E5x4NB9JAJTLhkB2ZZznyeokmr3cAPaj,81871900000000,6,81871900,0.8187206590960506155664035800 8 | CeLRs4kchQbWEcqZf7ctGrYhXwbU89fVxRs4Q34XeiHq,57593092134117,6,57593092.134117,0.5759320884384488276802851100 9 | E6FLdjDBtHvzhMGihcbec34KfcAqSYW67kfJV8RAKH1f,41567030634882,6,41567030.634882,0.4156711486854014418498406900 10 | B3NG3yTDwf3sL1oCUvf39HtdzWbtYoinNWqKDoRJTeuH,34113345604606,6,34113345.604606,0.3411341473371741171232849700 11 | 649KG5AEPic6MDVdbXjLPWj3vy6Lc466qGjV9d5Y3VyZ,21841695041612,6,21841695.041612,0.21841739302793165073814700 12 | HCSDiPfAvrZzzSp7KUxFJS1b5Dud8qxQw2VETWDFNV2R,20224818977065,6,20224818.977065,0.2022485996172187160466677100 13 | 4AWAjssbahfbzS7Qs3DJ7588CsCKyNPYcVw9ztENtv23,16526790004458,6,16526790.004458,0.1652682349523081185496718300 14 | HnYpR2c6RaKMiYuofaQEctt5HZDQqwgKboW3ttZkqeFU,14285228974748,6,14285228.974748,0.142852579231014327937467800 15 | 2V9CYburvmC2X2w96HFreUd9iEcW7BdWTFTJCt46tuai,13102800665992,6,13102800.665992,0.1310282721820935045887589600 16 | 594567HZxxmCNxLshew2svWndJHwjkwfMnX71q65fY17,11730314801303,6,11730314.801303,0.117303385722414530212806200 17 | HZ9TkcmJYGbyg4QgHWQpjpujKhQkEziEr8oYFWEfUaiK,9904137451700,6,9904137.4517,0.0990415752197482062733374500 18 | FoH4i1gYQM7LsZvmCURxkLrKoV8kCqBirJK5ib3mFVSF,9182774027600,6,9182774.0276,0.0918279263606532557123770600 19 | 24hHmHyTMDg331fK58jyiro4G3hm1NKJihfmzV4tDoJ7,9013535200304,6,9013535.200304,0.0901355346581469769817806400 20 | 4w49W4fNDn778wsBa6TNq9hvebZKU17ymsptrEZ8zrsm,8682184984209,6,8682184.984209,0.0868220257825386522839418700 21 | 5FHvWYqRgBAgEPsSnhwMjRAVfYFTJioHWJpJkZQD386b,7159461565413,6,7159461.565413,0.0715947607372953804375973400 22 | -------------------------------------------------------------------------------- /data/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN/2024-03-07/whales/whale_details.csv: -------------------------------------------------------------------------------- 1 | address,amount,decimals,ui_amount_string,owned_percentage 2 | 26ddLrqXDext6caX1gRxARePN4kzajyGiAUz9JmzmTGQ,4090152115064232,6,4090152115.064232,40.901604067995671635705296300 3 | 6G4XDSge4txj5tBkA5gZqefXXE3BRxqA37Yb1pmrHv6N,3500000000000000,6,3500000000,35.000070953531449811978766700 4 | 7bEL9XkPGNg7a3oSiUERKsZtFoJMqKh6VhGDCmMCPqdg,650000000000000,6,650000000,6.5000131770844121079389138200 5 | K6U4dQ8jANMEqQQycXYiDcf3172NGefpQBzdDbavQbA,500000000000000,6,500000000,5.0000101362187785445683952400 6 | GDijXRGfd1jhk5GkRPp1TDnEH2FfHURSRZeB5um3pUWk,190039583545000,6,190039583.545,1.9003996880155907912367181100 7 | 4QDgg3MSLiy3E5x4NB9JAJTLhkB2ZZznyeokmr3cAPaj,81871900000000,6,81871900,0.818720659742980430246098400 8 | CeLRs4kchQbWEcqZf7ctGrYhXwbU89fVxRs4Q34XeiHq,60403628718631,6,60403628.718631,0.6040375117151014200430556200 9 | E6FLdjDBtHvzhMGihcbec34KfcAqSYW67kfJV8RAKH1f,41402094060578,6,41402094.060578,0.4140217799271465757985301100 10 | B3NG3yTDwf3sL1oCUvf39HtdzWbtYoinNWqKDoRJTeuH,32414785624585,6,32414785.624585,0.3241485133725675008300064900 11 | 649KG5AEPic6MDVdbXjLPWj3vy6Lc466qGjV9d5Y3VyZ,22723196623255,6,22723196.623255,0.227232426887134642395714400 12 | HCSDiPfAvrZzzSp7KUxFJS1b5Dud8qxQw2VETWDFNV2R,20224818977065,6,20224818.977065,0.2022485997770298159816030800 13 | HnYpR2c6RaKMiYuofaQEctt5HZDQqwgKboW3ttZkqeFU,15518316123505,6,15518316.123505,0.1551834758291446049246653700 14 | 2V9CYburvmC2X2w96HFreUd9iEcW7BdWTFTJCt46tuai,13102800665992,6,13102800.665992,0.1310282722856283243087750600 15 | 4AWAjssbahfbzS7Qs3DJ7588CsCKyNPYcVw9ztENtv23,11851919510920,6,11851919.51092,0.1185194353764982167721216900 16 | HZ9TkcmJYGbyg4QgHWQpjpujKhQkEziEr8oYFWEfUaiK,9904137451700,6,9904137.4517,0.0990415752980080464161765200 17 | 594567HZxxmCNxLshew2svWndJHwjkwfMnX71q65fY17,8332577509944,6,8332577.509944,0.0833259440210972599450257800 18 | 4w49W4fNDn778wsBa6TNq9hvebZKU17ymsptrEZ8zrsm,7746502255214,6,7746502.255214,0.0774651795926232546759910200 19 | BrrJa8SznEzbAmuC1fkG3q94fdJx2wJhmxGfs8chdbVe,6749884000000,6,6749884,0.06749897683660190759505100 20 | DC7dKVhozaLN7D6QE3eAtwxSVmf7zbLAa5AFSMLTqfF2,6675044634131,6,6675044.634131,0.0667505816607355362036005100 21 | 3UeBkq2UVP7f6PUM4RtdHNvAYi9fjYGQ88zPjeo5hAEB,6674713515683,6,6674713.515683,0.0667472704695429581425202500 22 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | custom_type_derives = ["diesel::query_builder::QueryId"] 7 | 8 | [migrations_directory] 9 | dir = "migrations" 10 | -------------------------------------------------------------------------------- /migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvizardev/solana-rust-sniping-bot/003f77d6de8b9f93938e317d4d2fbd9437e62a39/migrations/.keep -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /migrations/2024-02-09-230011_01_schema_v1/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP SCHEMA IF EXISTS iri CASCADE; 3 | REVOKE ALL PRIVILEGES ON DATABASE iridb FROM iriuser; 4 | DROP DATABASE IF EXISTS iridb; 5 | DROP USER IF EXISTS iriuser; -------------------------------------------------------------------------------- /migrations/2024-02-09-230011_01_schema_v1/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE polygon.polygon_crypto_level2_book_data ( 2 | id SERIAL PRIMARY KEY, 3 | event_type VARCHAR NOT NULL, 4 | pair VARCHAR NOT NULL, 5 | timestamp BIGINT NOT NULL, 6 | received_timestamp BIGINT NOT NULL, 7 | exchange_id BIGINT NOT NULL, 8 | bid_prices JSONB NOT NULL, 9 | ask_prices JSONB NOT NULL 10 | ); 11 | 12 | 13 | CREATE TABLE polygon.polygon_crypto_quote_data ( 14 | id SERIAL PRIMARY KEY, 15 | event_type VARCHAR NOT NULL, 16 | pair VARCHAR NOT NULL, 17 | bid_price DOUBLE PRECISION NOT NULL, 18 | bid_size DOUBLE PRECISION NOT NULL, 19 | ask_price DOUBLE PRECISION NOT NULL, 20 | ask_size DOUBLE PRECISION NOT NULL, 21 | timestamp BIGINT NOT NULL, 22 | exchange_id BIGINT NOT NULL, 23 | received_timestamp BIGINT NOT NULL 24 | ); 25 | 26 | CREATE TABLE polygon.polygon_crypto_trade_data ( 27 | id SERIAL PRIMARY KEY, 28 | event_type VARCHAR NOT NULL, 29 | pair VARCHAR NOT NULL, 30 | price DOUBLE PRECISION NOT NULL, 31 | timestamp BIGINT NOT NULL, 32 | size DOUBLE PRECISION NOT NULL, 33 | conditions JSONB NOT NULL, 34 | trade_id VARCHAR, 35 | exchange_id BIGINT NOT NULL, 36 | received_timestamp BIGINT NOT NULL 37 | ); 38 | 39 | CREATE TABLE polygon.polygon_crypto_aggregate_data ( 40 | id SERIAL PRIMARY KEY, 41 | event_type VARCHAR NOT NULL, 42 | pair VARCHAR NOT NULL, 43 | open DOUBLE PRECISION NOT NULL, 44 | close DOUBLE PRECISION NOT NULL, 45 | high DOUBLE PRECISION NOT NULL, 46 | low DOUBLE PRECISION NOT NULL, 47 | volume DOUBLE PRECISION NOT NULL, 48 | timestamp BIGINT NOT NULL, 49 | end_time BIGINT NOT NULL, 50 | vw DOUBLE PRECISION NOT NULL, 51 | avg_trade_size BIGINT NOT NULL 52 | ); 53 | -------------------------------------------------------------------------------- /migrations/init-db.sql: -------------------------------------------------------------------------------- 1 | --EXECUTE ON PSQL SERVER 2 | -- Create superuser 3 | CREATE ROLE rust_iri WITH LOGIN SUPERUSER PASSWORD 'iri12345'; 4 | 5 | -- Create database 6 | CREATE DATABASE rust_iri_db OWNER rust_iri; 7 | 8 | -- Connect to the database \c rust_iri_db 9 | 10 | 11 | -- Create schema 12 | CREATE SCHEMA IF NOT EXISTS polygon; 13 | 14 | -- Install TimescaleDB extension 15 | CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/birdeye/amm_providers.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/raydium.svg", 5 | "website": "https://raydium.io", 6 | "source": "raydium", 7 | "program_ids": [ 8 | "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" 9 | ], 10 | "token_address": "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", 11 | "symbol": "RAY", 12 | "summary": true 13 | }, 14 | { 15 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/raydium.svg", 16 | "website": "https://raydium.io", 17 | "source": "raydium_clamm", 18 | "program_ids": [ 19 | "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK" 20 | ], 21 | "token_address": "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", 22 | "symbol": "RAY", 23 | "summary": true 24 | }, 25 | { 26 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/orca.svg", 27 | "website": "https://orca.so", 28 | "source": "orca", 29 | "program_ids": [ 30 | "9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP", 31 | "DjVE6JNiYqPL2QXyCUUh8rNjHrbz9hXHNYt99MQ59qw1" 32 | ], 33 | "token_address": "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE", 34 | "symbol": "ORCA", 35 | "summary": true 36 | }, 37 | { 38 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/serum.svg", 39 | "website": "https://projectserum.com/", 40 | "source": "serum", 41 | "program_ids": [ 42 | "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" 43 | ], 44 | "token_address": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", 45 | "symbol": "SRM", 46 | "summary": true 47 | }, 48 | { 49 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/serum.svg", 50 | "website": "https://projectserum.com/", 51 | "source": "serum_swap", 52 | "program_ids": [ 53 | "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" 54 | ], 55 | "token_address": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", 56 | "symbol": "SRM", 57 | "summary": false 58 | }, 59 | { 60 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/openbook.png", 61 | "website": "https://projectserum.com/", 62 | "source": "openbook", 63 | "program_ids": [ 64 | "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX" 65 | ], 66 | "token_address": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", 67 | "symbol": "SRM", 68 | "summary": true 69 | }, 70 | { 71 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/openbook.png", 72 | "website": "https://projectserum.com/", 73 | "source": "openbook_swap", 74 | "program_ids": [ 75 | "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX" 76 | ], 77 | "token_address": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", 78 | "symbol": "SRM", 79 | "summary": false 80 | }, 81 | { 82 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/aldrin.png", 83 | "website": "https://aldrin.com/", 84 | "source": "aldrin", 85 | "program_ids": [ 86 | "CURVGoZn8zycx6FXwwevgBTB2gVvdbGTEpvMJDbgs2t4", 87 | "AMM55ShdkoGRB5jVYPjWziwk8m5MpwyDgsMWHaMSQWH6", 88 | "TWAPR9s1DEhrr8tuFbwEPws5moHXebMotqU85wwVmvU" 89 | ], 90 | "token_address": "E5ndSkaB17Dm7CsD22dvcjfrYSDLCxFcMd6z8ddCk5wp", 91 | "symbol": "RIN", 92 | "summary": true 93 | }, 94 | { 95 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/step.svg", 96 | "website": "https://step.finance/", 97 | "source": "step", 98 | "program_ids": [ 99 | "SSwpMgqNDsyV7mAgN9ady4bDVu5ySjmmXejXvy2vLt1" 100 | ], 101 | "token_address": "StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT", 102 | "symbol": "STEP", 103 | "summary": true 104 | }, 105 | { 106 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/saros.svg", 107 | "website": "https://saros.finance/", 108 | "source": "saros", 109 | "program_ids": [ 110 | "SSwapUtytfBdBn1b9NUGG6foMVPtcWgpRU32HToDUZr" 111 | ], 112 | "token_address": "CUSDvqAQLbt7fRofcmV2EXfPA2t36kzj7FjzdmqDiNQL", 113 | "symbol": "CUSD", 114 | "summary": true 115 | }, 116 | { 117 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/cropper.svg", 118 | "website": "https://cropper.finance/", 119 | "source": "cropper", 120 | "program_ids": [ 121 | "CTMAxxk34HjKWxQ3QLZK1HpaLXmBveao3ESePXbiyfzh" 122 | ], 123 | "token_address": "DubwWZNWiNGMMeeQHPnMATNj77YZPZSAz2WVR5WjLJqz", 124 | "symbol": "CRP", 125 | "summary": true 126 | }, 127 | { 128 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/crema.svg", 129 | "website": "https://www.crema.finance/", 130 | "source": "crema", 131 | "program_ids": [ 132 | "6MLxLqiXaaSUpkgMnWDTuejNZEz3kE7k2woyHGVFw319" 133 | ], 134 | "token_address": "CRMaDAzKCWYbhUfsKYA8448vaA1qUzCETd7gNBDzQ1ks", 135 | "symbol": "CRM", 136 | "summary": true 137 | }, 138 | { 139 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/lifinity.svg", 140 | "website": "https://lifinity.io/", 141 | "source": "lifinity", 142 | "program_ids": [ 143 | "EewxydAPCCVuNEyrVN68PuSYdQ7wKn27V9Gjeoi8dy3S", 144 | "2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c" 145 | ], 146 | "token_address": "LFNTYraetVioAPnGJht4yNg2aUZFXR776cMeN9VMjXp", 147 | "symbol": "LFNTY", 148 | "summary": true 149 | }, 150 | { 151 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/saber.svg", 152 | "website": "https://saber.so/", 153 | "source": "saber", 154 | "program_ids": [ 155 | "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ" 156 | ], 157 | "token_address": "Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1", 158 | "symbol": "SBR", 159 | "summary": true 160 | }, 161 | { 162 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/sencha.webp", 163 | "website": "https://sencha.so/", 164 | "source": "sencha", 165 | "program_ids": [ 166 | "SCHAtsf8mbjyjiv4LkhLKutTf6JnZAbdJKFkXQNMFHZ" 167 | ], 168 | "token_address": "", 169 | "symbol": "", 170 | "summary": true 171 | }, 172 | { 173 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/mercurial.svg", 174 | "website": "https://www.mercurial.finance/", 175 | "source": "mercurial", 176 | "program_ids": [ 177 | "MERLuDFBMmsHnsBPZw2sDQZHvXFMwp8EdjudcU2HKky" 178 | ], 179 | "token_address": "MERt85fc5boKw3BW1eYdxonEuJNvXbiMbs6hvheau5K", 180 | "symbol": "MER", 181 | "summary": true 182 | }, 183 | { 184 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/pngfi.svg", 185 | "website": "https://app.png.fi/swap", 186 | "source": "penguin", 187 | "program_ids": [ 188 | "PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP" 189 | ], 190 | "token_address": "", 191 | "symbol": "", 192 | "summary": true 193 | }, 194 | { 195 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/solana.png", 196 | "website": "", 197 | "source": "swap_program", 198 | "program_ids": [ 199 | "SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8" 200 | ], 201 | "token_address": "", 202 | "symbol": "", 203 | "summary": true 204 | }, 205 | { 206 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/whirlpool.svg", 207 | "website": "", 208 | "source": "whirlpool", 209 | "program_ids": [ 210 | "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc" 211 | ], 212 | "token_address": "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE", 213 | "symbol": "ORCA", 214 | "summary": true 215 | }, 216 | { 217 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/1sol.png", 218 | "website": "", 219 | "source": "onemoon", 220 | "program_ids": [ 221 | "1MooN32fuBBgApc8ujknKJw5sef3BVwPGgz3pto1BAh" 222 | ], 223 | "token_address": "4ThReWAbAVZjNVgs5Ui9Pk3cZ5TYaD9u6Y89fp6EFzoF", 224 | "symbol": "1SOL", 225 | "summary": true 226 | }, 227 | { 228 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/cykura.svg", 229 | "website": "https://cykura.io", 230 | "source": "cykura", 231 | "program_ids": [ 232 | "cysPXAjehMpVKUapzbMCCnpFxUFFryEWEaLgnb9NrR8" 233 | ], 234 | "token_address": "BRLsMczKuaR5w9vSubF4j8HwEGGprVAyyVgS4EX7DKEg", 235 | "symbol": "CYS", 236 | "summary": true 237 | }, 238 | { 239 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/swim.svg", 240 | "website": "https://swim.io/", 241 | "source": "swim", 242 | "program_ids": [ 243 | "SWimmSE5hgWsEruwPBLBVAFi3KyVfe8URU2pb4w7GZs", 244 | "SWiMDJYFUGj6cPrQ6QYYYWZtvXQdRChSVAygDZDsCHC" 245 | ], 246 | "token_address": "", 247 | "symbol": "", 248 | "summary": true 249 | }, 250 | { 251 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/dooar.png", 252 | "website": "https://stepn.com/", 253 | "source": "dooar", 254 | "program_ids": [ 255 | "Dooar9JkhdZ7J3LHN3A7YCuoGRUggXhQaG4kijfLGU2j" 256 | ], 257 | "token_address": "7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx", 258 | "symbol": "GMT", 259 | "summary": true 260 | }, 261 | { 262 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/marinade.png", 263 | "website": "https://marinade.finance/", 264 | "source": "marinade", 265 | "program_ids": [ 266 | "MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD" 267 | ], 268 | "token_address": "MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey", 269 | "symbol": "MNDE", 270 | "summary": false 271 | }, 272 | { 273 | "icon": "https://raw.githubusercontent.com/birdeye-so/birdeye-ads/main/traders/mango.png", 274 | "website": "https://mango.markets/", 275 | "source": "mango", 276 | "program_ids": [ 277 | "mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68" 278 | ], 279 | "token_address": "MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac", 280 | "symbol": "MNGO", 281 | "summary": false 282 | }, 283 | { 284 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/symmetry.png", 285 | "website": "https://www.symmetry.fi/", 286 | "source": "symmetry", 287 | "program_ids": [ 288 | "2KehYt3KsEQR53jYcxjbQp2d2kCp4AkuQW68atufRwSr" 289 | ], 290 | "token_address": "", 291 | "symbol": "", 292 | "summary": false 293 | }, 294 | { 295 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/meteora.svg", 296 | "website": "https://www.mercurial.finance/", 297 | "source": "meteora", 298 | "program_ids": [ 299 | "5B23C376Kwtd1vzb5LCJHiHLPnoWSnnx661hhGGDEv8y", 300 | "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB" 301 | ], 302 | "token_address": "", 303 | "symbol": "", 304 | "summary": false 305 | }, 306 | { 307 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/invariant.png", 308 | "website": "https://invariant.app/", 309 | "source": "invariant", 310 | "program_ids": [ 311 | "HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt" 312 | ], 313 | "token_address": "", 314 | "symbol": "", 315 | "summary": true 316 | }, 317 | { 318 | "icon": "", 319 | "website": "", 320 | "source": "XuErbiqKKqpvN2X8qjkBNo2BwNvQp1WZKZTDgxKB95r", 321 | "program_ids": [ 322 | "XuErbiqKKqpvN2X8qjkBNo2BwNvQp1WZKZTDgxKB95r" 323 | ], 324 | "token_address": "", 325 | "symbol": "", 326 | "summary": false 327 | }, 328 | { 329 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/goosefx.png", 330 | "website": "https://goosefx.io/", 331 | "source": "goosefx", 332 | "program_ids": [ 333 | "7WduLbRfYhTJktjLw5FDEyrqoEv61aTTCuGAetgLjzN5" 334 | ], 335 | "token_address": "GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD", 336 | "symbol": "GOFX", 337 | "summary": false 338 | }, 339 | { 340 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/solanax.png", 341 | "website": "https://solanax.org/", 342 | "source": "solanax", 343 | "program_ids": [ 344 | "Hsn6R7N5avWAL4ScKHYgmwFyhnQ7ZEun94AmTiptPRdA" 345 | ], 346 | "token_address": "5v6tZ1SiAi7G8Qg4rBF1ZdAn4cn6aeQtefewMr1NLy61", 347 | "symbol": "SOLD", 348 | "summary": false 349 | }, 350 | { 351 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/deltafi.png", 352 | "website": "https://www.deltafi.trade/", 353 | "source": "deltafi", 354 | "program_ids": [ 355 | "GNExJhNUhc9LN2DauuQAUJnXoy6DJ6zey3t9kT9A2PF3" 356 | ], 357 | "token_address": "", 358 | "symbol": "", 359 | "summary": false 360 | }, 361 | { 362 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/dradex.jpeg", 363 | "website": "https://www.dradex.io/", 364 | "source": "dradex_swap", 365 | "program_ids": [ 366 | "dp2waEWSBy5yKmq65ergoU3G6qRLmqa6K7We4rZSKph" 367 | ], 368 | "token_address": "", 369 | "symbol": "", 370 | "summary": true 371 | }, 372 | { 373 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/crema.svg", 374 | "website": "https://www.crema.finance/", 375 | "source": "crema_clamm", 376 | "program_ids": [ 377 | "CLMM9tUoggJu2wagPkkqs9eFG4BWhVBZWkP1qv3Sp7tR" 378 | ], 379 | "token_address": "CRMaDAzKCWYbhUfsKYA8448vaA1qUzCETd7gNBDzQ1ks", 380 | "symbol": "CRM", 381 | "summary": true 382 | }, 383 | { 384 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/balansol.svg", 385 | "website": "https://hub.sentre.io/app/balansol/swap?bid_mint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&ask_mint=SENBBKVCM7homnf5RX9zqpf1GFe935hnbU4uVzY1Y6M", 386 | "source": "balansol", 387 | "program_ids": [ 388 | "D3BBjqUdCYuP18fNvvMbPAZ8DpcRi4io2EsYHQawJDag" 389 | ], 390 | "token_address": "", 391 | "symbol": "", 392 | "summary": false 393 | }, 394 | { 395 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/pool_providers/marcopolo.svg", 396 | "website": "https://marcopolo.so/", 397 | "source": "marcopolo", 398 | "program_ids": [ 399 | "9tKE7Mbmj4mxDjWatikzGAtkoWosiiZX9y6J4Hfm2R8H" 400 | ], 401 | "token_address": "", 402 | "symbol": "", 403 | "summary": true 404 | }, 405 | { 406 | "icon": "https://cdn.jsdelivr.net/gh/birdeye-so/birdeye-ads/platforms/jupiter.svg", 407 | "website": "https://jup.ag/", 408 | "source": "jupiter_limit_order", 409 | "program_ids": [ 410 | "jupoNjAxXgZ4rjzxzPMP4oxduvQsQtZzyknqvzYNrNu" 411 | ], 412 | "token_address": "", 413 | "symbol": "", 414 | "summary": false 415 | } 416 | ], 417 | "success": true 418 | } -------------------------------------------------------------------------------- /run-migrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Wait for PostgreSQL to become available. 4 | while ! pg_isready -h localhost -p 5432 -U iriuser; do 5 | echo "Waiting for PostgreSQL to become available..." 6 | sleep 2 7 | done 8 | 9 | # Ensure the Rust environment is sourced for this session 10 | source $HOME/.cargo/env 11 | 12 | # Run Diesel migrations 13 | diesel migration run --database-url postgres://iriuser:iripw12345@localhost/iridb 14 | -------------------------------------------------------------------------------- /src/db/db_session_manager.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use diesel::r2d2::ConnectionManager; 3 | use r2d2::{Error, PooledConnection}; 4 | 5 | type DbPool = r2d2::Pool>; 6 | 7 | pub struct DbSessionManager { 8 | pool: DbPool, 9 | } 10 | 11 | impl DbSessionManager { 12 | pub fn new(database_url: &str) -> Self { 13 | let manager = ConnectionManager::::new(database_url); 14 | let pool = r2d2::Pool::builder() 15 | .build(manager) 16 | .expect("Failed to create pool."); 17 | DbSessionManager { pool } 18 | } 19 | 20 | pub fn get_connection(&self) -> Result>, Error> { 21 | self.pool.get() 22 | } 23 | 24 | 25 | // pub fn persist_event(&self, event: &PolygonEventTypes) -> Result { 26 | // 27 | // let mut conn = self.get_connection().unwrap(); // Assume this method fetches a DB connection 28 | // 29 | // // Example conversion for one variant; implement for others as needed 30 | // // let new_event = match event { 31 | // // PolygonEventTypes::XtTrade(trade_data) => { 32 | // // NewPolygonCryptoTradeData { 33 | // // event_type: "XT".to_string(), 34 | // // pair: trade_data.pair.clone(), 35 | // // price: trade_data.price, 36 | // // timestamp: trade_data.timestamp, 37 | // // size: trade_data.size, 38 | // // conditions: serde_json::Value::Array(Vec::new()), 39 | // // trade_id: trade_data.trade_id.clone(), 40 | // // exchange_id: trade_data.exchange_id, 41 | // // received_timestamp: trade_data.received_timestamp, 42 | // // } 43 | // // }, 44 | // // // Handle other variants... 45 | // // _ => unimplemented!(), 46 | // // }; 47 | // 48 | // // Insert the new event into the database 49 | // // diesel::insert_into(polygon_crypto_trade_data) 50 | // // .values(&new_event) 51 | // // .execute(&mut conn) 52 | // } 53 | 54 | } -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_session_manager; -------------------------------------------------------------------------------- /src/decoder/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tx_decoder; -------------------------------------------------------------------------------- /src/decoder/tx_decoder.rs: -------------------------------------------------------------------------------- 1 | // 2 | // use 3 | // anchor_client::{anchor_lang::prelude::*, Client as AnchorClient}; 4 | // use solana_sdk::{signature::Keypair, signer::Signer}; 5 | // 6 | // use std::collections::HashMap; 7 | // use std::env; 8 | // use std::error::Error; 9 | // use std::str::FromStr; 10 | // use actix_web::{web, HttpResponse, Responder}; 11 | // use diesel::serialize::IsNull::No; 12 | // use eyre::anyhow; 13 | // use log::Level::Debug; 14 | // use reqwest::{Client as RpcClient, Client, header}; 15 | // use serde::{Serialize, Deserialize}; 16 | // use serde_json::{json, Value}; 17 | // use solana_sdk::bs58; 18 | // use rust_decimal::{prelude::FromPrimitive, Decimal}; 19 | // use rust_decimal::prelude::{One, Zero}; 20 | // 21 | // // https://solana.stackexchange.com/questions/2635/fetch-onchain-idl-from-program-id 22 | // 23 | // // Decodes transaction instruction data for transactions on the Solana Blockchain 24 | // 25 | // use solana_sdk::commitment_config::CommitmentConfig; 26 | // 27 | // async fn get_program_idl(program_id_str: &str) -> Result { 28 | // let program_pubkey = Pubkey::from_str(program_id_str)?; 29 | // 30 | // // Set up the Solana RPC client. You need to replace `http://localhost:8899` with the RPC endpoint you're using. 31 | // let rpc_url = "https://api.mainnet-beta.solana.com";//env::var("PRIVATE_SOLANA_QUICKNODE").expect("PRIVATE_SOLANA_QUICKNODE must be set"); 32 | // 33 | // println!("Fetching IDL for program {:#?}", signatures); 34 | // let mut transactions: Vec= Vec::new(); 35 | // let client = Client::new(); 36 | // 37 | // // Calculate the PDA for the IDL account. 38 | // let seeds = &[b"anchor:idl".as_ref(), program_pubkey.as_ref()]; 39 | // let (idl_address, _) = Pubkey::find_program_address(seeds, &program_pubkey); 40 | // 41 | // // Fetch the account data from the blockchain. 42 | // let account_data = rpc_client.get_account_data(&idl_address) 43 | // .map_err(|e| anyhow!("Failed to get account data: {}", e))?; 44 | // 45 | // // The IDL data is compressed using zstd. First, skip the 8-byte discriminator. 46 | // let idl_data = &account_data[8..]; 47 | // 48 | // // Decompress the IDL data. 49 | // let decompressed_idl = decode_all(idl_data) 50 | // .map_err(|e| anyhow!("Failed to decompress IDL data: {}", e))?; 51 | // 52 | // // Deserialize the JSON IDL. 53 | // let idl_json: Value = serde_json::from_slice(&decompressed_idl) 54 | // .map_err(|e| anyhow!("Failed to deserialize IDL: {}", e))?; 55 | // 56 | // Ok(idl_json) 57 | // } -------------------------------------------------------------------------------- /src/http/base_http_client.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{Client as ReqwestClient, Response}; 2 | use serde::de::DeserializeOwned; 3 | use std::collections::HashMap; 4 | use crate::http::http_client_error::HttpClientError; 5 | 6 | pub struct BaseHttpClient { 7 | client: ReqwestClient, 8 | default_headers: HashMap, 9 | } 10 | 11 | impl BaseHttpClient { 12 | pub fn new() -> Self { 13 | Self { 14 | client: ReqwestClient::new(), 15 | default_headers: HashMap::new(), 16 | } 17 | } 18 | 19 | pub fn add_default_header(mut self, key: &str, value: &str) -> Self { 20 | self.default_headers.insert(key.to_string(), value.to_string()); 21 | self 22 | } 23 | 24 | async fn send_request(&self, method: reqwest::Method, url: &str, headers: Option>, body: Option) -> Result> { 25 | let mut request = self.client.request(method, url); 26 | 27 | // Apply default headers 28 | for (key, value) in &self.default_headers { 29 | request = request.header(key, value); 30 | } 31 | 32 | // Apply additional headers 33 | if let Some(hdrs) = headers { 34 | for (key, value) in hdrs { 35 | request = request.header(key, value); 36 | } 37 | } 38 | 39 | // Set JSON body if present 40 | if let Some(b) = body { 41 | request = request.body(b); 42 | } 43 | 44 | let response: Response = request.send().await?; 45 | let status = response.status(); 46 | let result = response.json::().await?; 47 | 48 | if status.is_success() { 49 | Ok(result) 50 | } else { 51 | Err(format!("Request failed with status: {}", status).into()) 52 | } 53 | } 54 | 55 | pub async fn get(&self, url: &str, headers: Option>) -> Result> { 56 | self.send_request(reqwest::Method::GET, url, headers, None).await 57 | } 58 | 59 | pub async fn post(&self, url: &str, headers: Option>, body: String) -> Result> { 60 | self.send_request(reqwest::Method::POST, url, headers, Some(body)).await 61 | } 62 | 63 | pub async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result { 64 | let request = self.client.get(url); 65 | 66 | // Apply default headers 67 | let mut request = request; 68 | for (key, value) in &self.default_headers { 69 | request = request.header(key, value); 70 | } 71 | 72 | // Apply query parameters 73 | let request = request.query(query); 74 | 75 | // Send the request 76 | let response = request.send().await.map_err(HttpClientError::from)?; 77 | 78 | // Check for HTTP success status 79 | if !response.status().is_success() { 80 | let error = match response.status() { 81 | reqwest::StatusCode::UNAUTHORIZED => HttpClientError::Unauthorized, 82 | reqwest::StatusCode::BAD_REQUEST => HttpClientError::BadRequest("Bad request".into()), 83 | reqwest::StatusCode::NOT_FOUND => HttpClientError::NotFound, 84 | _ => HttpClientError::Other(response.error_for_status().unwrap_err()), // Convert to reqwest::Error for detailed error 85 | }; 86 | return Err(error); 87 | } 88 | 89 | // Deserialize the response body into the expected type 90 | response.json::().await.map_err(HttpClientError::from) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/http/http_client_error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug)] 4 | pub enum HttpClientError { 5 | Unauthorized, 6 | BadRequest(String), 7 | NotFound, 8 | Other(reqwest::Error), 9 | } 10 | 11 | impl fmt::Display for HttpClientError { 12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | match *self { 14 | HttpClientError::Unauthorized => write!(f, "Unauthorized: Invalid API key or lack of permissions."), 15 | HttpClientError::BadRequest(ref message) => write!(f, "Bad Request: {}", message), 16 | HttpClientError::NotFound => write!(f, "Not Found: The requested resource could not be found."), 17 | HttpClientError::Other(ref e) => write!(f, "Other Error: {}", e), 18 | } 19 | } 20 | } 21 | 22 | impl std::error::Error for HttpClientError {} 23 | 24 | impl From for HttpClientError { 25 | fn from(error: reqwest::Error) -> Self { 26 | // Map specific reqwest errors to your custom errors 27 | if error.is_status() { 28 | match error.status() { 29 | Some(reqwest::StatusCode::UNAUTHORIZED) => HttpClientError::Unauthorized, 30 | Some(reqwest::StatusCode::BAD_REQUEST) => HttpClientError::BadRequest("Bad request".into()), 31 | Some(reqwest::StatusCode::NOT_FOUND) => HttpClientError::NotFound, 32 | _ => HttpClientError::Other(error), 33 | } 34 | } else { 35 | HttpClientError::Other(error) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod base_http_client; 2 | pub mod http_client_error; 3 | pub mod moralis_http_client; 4 | -------------------------------------------------------------------------------- /src/http/moralis_http_client.rs: -------------------------------------------------------------------------------- 1 | use crate::http::base_http_client::BaseHttpClient; 2 | use crate::http::http_client_error::HttpClientError; 3 | use crate::models::moralis::token_price_response::ERC20TokenPriceResponse; 4 | 5 | 6 | pub struct MoralisHttpClient { 7 | base_client: BaseHttpClient, 8 | base_url: String, 9 | } 10 | 11 | impl MoralisHttpClient { 12 | pub fn new(api_key: &str) -> Self { 13 | let base_client = BaseHttpClient::new() 14 | .add_default_header("X-API-Key", api_key) 15 | .add_default_header("accept", "application/json"); 16 | 17 | Self { 18 | base_client, 19 | base_url: "https://deep-index.moralis.io/api/v2".to_string(), 20 | } 21 | } 22 | 23 | pub async fn get_token_price(&self, address: &str, chain: &str, include_percent_change: bool) -> Result { 24 | // Construct the full endpoint URL 25 | let endpoint = format!("erc20/{}/price", address); 26 | let url = format!("{}/{}", &self.base_url, endpoint); 27 | 28 | let query = [ 29 | ("chain", chain), 30 | ("include", if include_percent_change { "percent_change" } else { "" }), 31 | ]; 32 | 33 | // Use the base HTTP client to send the request 34 | self.base_client.get_with_query::(&url, &query).await 35 | } 36 | 37 | // Add other Moralis-specific methods here... 38 | } 39 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod solana; 2 | pub mod moralis; 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/models/moralis/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token_price_response; -------------------------------------------------------------------------------- /src/models/moralis/token_price_response.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct ERC20TokenPriceResponse { 5 | token_name: String, 6 | token_symbol: String, 7 | token_logo: String, 8 | token_decimals: String, 9 | native_price: NativePrice, 10 | pub usd_price: f64, 11 | usd_price_formatted: String, 12 | exchange_name: String, 13 | exchange_address: String, 14 | token_address: String, 15 | price_last_changed_at_block: String, 16 | verified_contract: bool, 17 | #[serde(rename = "24hr_percent_change")] 18 | hr24_percent_change: String, 19 | } 20 | 21 | #[derive(Debug, Serialize, Deserialize)] 22 | pub struct NativePrice { 23 | value: String, 24 | decimals: u8, 25 | name: String, 26 | symbol: String, 27 | address: String, 28 | } 29 | -------------------------------------------------------------------------------- /src/models/solana/alchemy/get_program_accounts.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | pub struct ProgramAccountsResponse { 5 | jsonrpc: String, 6 | result: Vec, 7 | id: u64, 8 | } 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | pub struct Account { 12 | pubkey: String, 13 | account: AccountData, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Debug)] 17 | pub struct AccountData { 18 | lamports: u64, 19 | owner: String, 20 | data: Vec, // or another structure depending on the encoding 21 | executable: bool, 22 | rent_epoch: u64, 23 | } -------------------------------------------------------------------------------- /src/models/solana/alchemy/get_token_account_balance.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvizardev/solana-rust-sniping-bot/003f77d6de8b9f93938e317d4d2fbd9437e62a39/src/models/solana/alchemy/get_token_account_balance.rs -------------------------------------------------------------------------------- /src/models/solana/alchemy/get_token_accounts_by_owner.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | struct TokenAccountsByOwnerResponse { 5 | jsonrpc: String, 6 | result: TokenAccountsByOwnerResult, 7 | id: u64, 8 | } 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | struct TokenAccountsByOwnerResult { 12 | context: Context, 13 | value: Vec, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Debug)] 17 | struct Context { 18 | slot: u64, 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Debug)] 22 | struct TokenAccount { 23 | pubkey: String, 24 | account: AccountDetail, 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Debug)] 28 | struct AccountDetail { 29 | data: AccountData, 30 | executable: bool, 31 | lamports: u64, 32 | owner: String, 33 | rentEpoch: u64, 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug)] 37 | struct AccountData { 38 | program: String, 39 | parsed: ParsedData, 40 | space: u64, 41 | } 42 | 43 | #[derive(Serialize, Deserialize, Debug)] 44 | struct ParsedData { 45 | accountType: String, 46 | info: TokenAccountInfo, 47 | #[serde(rename = "type")] 48 | data_type: String, // Use `data_type` because `type` is a reserved keyword in Rust. 49 | } 50 | 51 | #[derive(Serialize, Deserialize, Debug)] 52 | struct TokenAccountInfo { 53 | tokenAmount: TokenAmount, 54 | delegate: Option, // This can be optional based on the account state. 55 | delegatedAmount: Option, // This can also be optional. 56 | state: String, 57 | isNative: bool, 58 | mint: String, 59 | owner: String, 60 | } 61 | 62 | #[derive(Serialize, Deserialize, Debug)] 63 | struct TokenAmount { 64 | amount: String, 65 | decimals: u8, 66 | uiAmount: f64, 67 | uiAmountString: String, 68 | } 69 | -------------------------------------------------------------------------------- /src/models/solana/alchemy/get_token_largest_accounts.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvizardev/solana-rust-sniping-bot/003f77d6de8b9f93938e317d4d2fbd9437e62a39/src/models/solana/alchemy/get_token_largest_accounts.rs -------------------------------------------------------------------------------- /src/models/solana/alchemy/get_token_supply.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvizardev/solana-rust-sniping-bot/003f77d6de8b9f93938e317d4d2fbd9437e62a39/src/models/solana/alchemy/get_token_supply.rs -------------------------------------------------------------------------------- /src/models/solana/alchemy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_program_accounts; 2 | pub mod get_token_largest_accounts; 3 | pub mod get_token_accounts_by_owner; 4 | pub mod get_token_supply; 5 | pub mod get_token_account_balance; -------------------------------------------------------------------------------- /src/models/solana/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod solana_logs_notification; 2 | pub mod solana_account_notification; 3 | pub mod solana_program_notification; 4 | pub mod solana_event_types; 5 | pub mod solana_block_notification; 6 | pub mod alchemy; 7 | pub mod solana_transaction; -------------------------------------------------------------------------------- /src/models/solana/solana_account_notification.rs: -------------------------------------------------------------------------------- 1 | // https://solana.com/docs/rpc/websocket/accountsubscribe 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct SolanaAccountNotification { 6 | jsonrpc: String, 7 | method: String, 8 | params: Params, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone)] 12 | pub struct Params { 13 | result: Result, 14 | subscription: u64, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Clone)] 18 | pub struct Result { 19 | context: Context, 20 | value: ResultValue, 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug, Clone)] 24 | pub struct Context { 25 | slot: u64, 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Debug, Clone)] 29 | pub struct ResultValue { 30 | data: Data, 31 | executable: bool, 32 | lamports: u64, 33 | owner: String, 34 | rent_epoch: u64, 35 | space: u64, 36 | } 37 | 38 | #[derive(Serialize, Deserialize, Debug, Clone)] 39 | pub struct Data { 40 | program: String, 41 | parsed: Parsed, 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Debug, Clone)] 45 | pub struct Parsed { 46 | #[serde(rename = "type")] 47 | type_field: String, 48 | info: Info, 49 | } 50 | 51 | #[derive(Serialize, Deserialize, Debug, Clone)] 52 | pub struct Info { 53 | authority: String, 54 | blockhash: String, 55 | fee_calculator: FeeCalculator, 56 | } 57 | 58 | #[derive(Serialize, Deserialize, Debug, Clone)] 59 | pub struct FeeCalculator { 60 | lamports_per_signature: u64, 61 | } 62 | 63 | impl Default for SolanaAccountNotification { 64 | fn default() -> Self { 65 | Self { 66 | jsonrpc: "".to_string(), 67 | method: "".to_string(), 68 | params: Params::default(), 69 | } 70 | } 71 | } 72 | 73 | impl Default for Params { 74 | fn default() -> Self { 75 | Self { 76 | result: Result::default(), 77 | subscription: 0, 78 | } 79 | } 80 | } 81 | 82 | impl Default for Result { 83 | fn default() -> Self { 84 | Self { 85 | context: Context::default(), 86 | value: ResultValue::default(), 87 | } 88 | } 89 | } 90 | 91 | impl Default for Context { 92 | fn default() -> Self { 93 | Self { 94 | slot: 0, 95 | } 96 | } 97 | } 98 | 99 | impl Default for ResultValue { 100 | fn default() -> Self { 101 | Self { 102 | data: Data::default(), 103 | executable: false, 104 | lamports: 0, 105 | owner: "".to_string(), 106 | rent_epoch: 0, 107 | space: 0, 108 | } 109 | } 110 | } 111 | 112 | impl Default for Data { 113 | fn default() -> Self { 114 | Self { 115 | program: "".to_string(), 116 | parsed: Parsed::default(), 117 | } 118 | } 119 | } 120 | 121 | impl Default for Parsed { 122 | fn default() -> Self { 123 | Self { 124 | type_field: "".to_string(), 125 | info: Info::default(), 126 | } 127 | } 128 | } 129 | 130 | impl Default for Info { 131 | fn default() -> Self { 132 | Self { 133 | authority: "".to_string(), 134 | blockhash: "".to_string(), 135 | fee_calculator: FeeCalculator::default(), 136 | } 137 | } 138 | } 139 | 140 | impl Default for FeeCalculator { 141 | fn default() -> Self { 142 | Self { 143 | lamports_per_signature: 0, 144 | } 145 | } 146 | } 147 | 148 | use std::fmt; 149 | 150 | impl fmt::Display for SolanaAccountNotification { 151 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 152 | // Simplified example: Customize according to what you want to display 153 | write!(f, "Method: {}, Subscription: {}", self.method, self.params.subscription) 154 | } 155 | } -------------------------------------------------------------------------------- /src/models/solana/solana_block_notification.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | #[derive(Serialize, Deserialize, Debug)] 5 | struct BlockNotificationResponse { 6 | jsonrpc: String, 7 | method: String, 8 | params: BlockNotificationParams, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug)] 12 | struct BlockNotificationParams { 13 | result: BlockNotificationResult, 14 | subscription: u64, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug)] 18 | struct BlockNotificationResult { 19 | context: Context, 20 | value: BlockValue, 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug)] 24 | struct Context { 25 | slot: u64, 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Debug)] 29 | struct BlockValue { 30 | slot: u64, 31 | block: Block, 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Debug)] 35 | struct Block { 36 | previous_blockhash: String, 37 | blockhash: String, 38 | parent_slot: u64, 39 | transactions: Vec, 40 | block_time: Option, 41 | block_height: Option, 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Debug)] 45 | struct Transaction { 46 | transaction: Vec, // This could be further detailed based on the actual structure 47 | meta: TransactionMeta, 48 | } 49 | 50 | #[derive(Serialize, Deserialize, Debug)] 51 | struct TransactionMeta { 52 | err: Option, 53 | status: TransactionStatus, 54 | fee: u64, 55 | pre_balances: Vec, 56 | post_balances: Vec, 57 | inner_instructions: Vec, 58 | // Add more fields as necessary 59 | log_messages: Option>, 60 | pre_token_balances: Option>, 61 | post_token_balances: Option>, 62 | rewards: Option>, 63 | } 64 | 65 | #[derive(Serialize, Deserialize, Debug)] 66 | struct TransactionStatus { 67 | Ok: Option, 68 | } 69 | 70 | #[derive(Serialize, Deserialize, Debug)] 71 | struct InnerInstruction { 72 | index: u8, 73 | instructions: Vec, 74 | } 75 | 76 | #[derive(Serialize, Deserialize, Debug)] 77 | struct InstructionDetail { 78 | program_id_index: u8, 79 | accounts: Vec, 80 | data: String, 81 | } 82 | 83 | #[derive(Serialize, Deserialize, Debug)] 84 | struct TokenBalance { 85 | account_index: u8, 86 | mint: String, 87 | ui_token_amount: UiTokenAmount, 88 | owner: String, 89 | program_id: String, 90 | } 91 | 92 | #[derive(Serialize, Deserialize, Debug)] 93 | struct UiTokenAmount { 94 | ui_amount: Option, 95 | decimals: u8, 96 | amount: String, 97 | ui_amount_string: String, 98 | } 99 | -------------------------------------------------------------------------------- /src/models/solana/solana_event_types.rs: -------------------------------------------------------------------------------- 1 | use crate::models::solana::solana_logs_notification::SolanaLogsNotification; 2 | use crate::models::solana::solana_program_notification::SolanaProgramNotification; 3 | use crate::models::solana::solana_account_notification::SolanaAccountNotification; 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub enum SolanaEventTypes { 9 | LogNotification(SolanaLogsNotification), 10 | AccountNotification(SolanaAccountNotification), 11 | ProgramNotification(SolanaProgramNotification) 12 | } 13 | -------------------------------------------------------------------------------- /src/models/solana/solana_logs_notification.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct SolanaLogsNotification { 5 | pub jsonrpc: String, 6 | pub method: String, 7 | pub params: NotificationParams, 8 | } 9 | 10 | #[derive(Serialize, Deserialize, Debug, Clone)] 11 | pub struct NotificationParams { 12 | pub result: NotificationResult, 13 | pub subscription: u64, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Debug, Clone)] 17 | pub struct NotificationResult { 18 | pub context: NotificationContext, 19 | pub value: NotificationValue, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug, Clone)] 23 | pub struct NotificationContext { 24 | pub slot: u64, 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Debug, Clone)] 28 | pub struct NotificationValue { 29 | pub signature: String, 30 | pub err: Option, 31 | pub logs: Vec, 32 | } 33 | 34 | impl Default for SolanaLogsNotification { 35 | fn default() -> Self { 36 | SolanaLogsNotification { 37 | jsonrpc: "".to_string(), 38 | method: "".to_string(), 39 | params: NotificationParams::default(), 40 | } 41 | } 42 | } 43 | 44 | impl Default for NotificationParams { 45 | fn default() -> Self { 46 | NotificationParams { 47 | result: NotificationResult::default(), 48 | subscription: 0, 49 | } 50 | } 51 | } 52 | 53 | impl Default for NotificationResult { 54 | fn default() -> Self { 55 | NotificationResult { 56 | context: NotificationContext::default(), 57 | value: NotificationValue::default(), 58 | } 59 | } 60 | } 61 | 62 | impl Default for NotificationContext { 63 | fn default() -> Self { 64 | NotificationContext { 65 | slot: 0, 66 | } 67 | } 68 | } 69 | 70 | impl Default for NotificationValue { 71 | fn default() -> Self { 72 | NotificationValue { 73 | signature: "".to_string(), 74 | err: None, 75 | logs: vec![], 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/models/solana/solana_program_notification.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; // For handling flexible data structures 3 | 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct SolanaProgramNotification { 6 | pub jsonrpc: String, 7 | pub method: String, 8 | pub params: ProgramNotificationParams, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone)] 12 | pub struct ProgramNotificationParams { 13 | pub result: ProgramNotificationResult, 14 | pub subscription: u64, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Clone)] 18 | pub struct ProgramNotificationResult { 19 | pub context: ProgramNotificationContext, 20 | pub value: ProgramNotificationValue, 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug, Clone)] 24 | pub struct ProgramNotificationContext { 25 | pub slot: u64, 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Debug, Clone)] 29 | pub struct ProgramNotificationValue { 30 | pub pubkey: String, 31 | pub account: ProgramAccount, 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Debug, Clone)] 35 | #[serde(untagged)] // Allows for different types of `data` field representations 36 | pub enum ProgramAccountData { 37 | Base58(Vec), // For base58 encoding: ["data", "base58"] 38 | ParsedJson { 39 | program: String, 40 | parsed: Value, // Flexible to accommodate any structure 41 | }, 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Debug, Clone)] 45 | pub struct ProgramAccount { 46 | pub data: ProgramAccountData, 47 | pub executable: bool, 48 | pub lamports: u64, 49 | pub owner: String, 50 | pub rentEpoch: u64, 51 | pub space: u64, 52 | } 53 | 54 | impl Default for SolanaProgramNotification { 55 | fn default() -> Self { 56 | SolanaProgramNotification { 57 | jsonrpc: "".to_string(), 58 | method: "".to_string(), 59 | params: ProgramNotificationParams::default(), 60 | } 61 | } 62 | } 63 | 64 | impl Default for ProgramNotificationParams { 65 | fn default() -> Self { 66 | ProgramNotificationParams { 67 | result: ProgramNotificationResult::default(), 68 | subscription: 0, 69 | } 70 | } 71 | } 72 | 73 | impl Default for ProgramNotificationResult { 74 | fn default() -> Self { 75 | ProgramNotificationResult { 76 | context: ProgramNotificationContext::default(), 77 | value: ProgramNotificationValue::default(), 78 | } 79 | } 80 | } 81 | 82 | impl Default for ProgramNotificationContext { 83 | fn default() -> Self { 84 | ProgramNotificationContext { 85 | slot: 0, 86 | } 87 | } 88 | } 89 | 90 | impl Default for ProgramNotificationValue { 91 | fn default() -> Self { 92 | ProgramNotificationValue { 93 | pubkey: "".to_string(), 94 | account: ProgramAccount::default(), 95 | } 96 | } 97 | } 98 | 99 | impl Default for ProgramAccount { 100 | fn default() -> Self { 101 | ProgramAccount { 102 | data: ProgramAccountData::Base58(vec![]), // Default to one of the possible types 103 | executable: false, 104 | lamports: 0, 105 | owner: "".to_string(), 106 | rentEpoch: 0, 107 | space: 0, 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/models/solana/solana_transaction.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::error::Error; 3 | use std::fmt; 4 | use reqwest::Client; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | 8 | #[derive(Debug, Serialize, Deserialize, Clone)] 9 | pub struct TransactionSummary { 10 | pub signature: String, 11 | pub program_id: String, 12 | pub amount_delta: f64, 13 | pub mint: String, 14 | pub owner: String, 15 | // Token name and symbol are placeholders for now 16 | pub token_name: String, 17 | pub token_symbol: String, 18 | pub transaction_type: String, 19 | } 20 | 21 | impl fmt::Display for TransactionSummary { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | write!(f, "Transaction {} Summary:\n\ 24 | Program ID: {}\n\ 25 | Amount Delta: {}\n\ 26 | Mint: {}\n\ 27 | Owner: {}\n\ 28 | Token Name: {}\n\ 29 | Token Symbol: {}\n\ 30 | Transaction Type: {}", 31 | self.signature, 32 | self.program_id, 33 | self.amount_delta, 34 | self.mint, 35 | self.owner, 36 | self.token_name, 37 | self.token_symbol, 38 | self.transaction_type 39 | ) 40 | } 41 | } 42 | 43 | 44 | #[derive(Debug, Serialize, Deserialize, Clone)] 45 | pub struct TxCheckedSummary { 46 | pub signature: String, 47 | pub transaction_type: String, 48 | pub source: String, // the public key of the account from which the tokens are being transferred 49 | pub destination: String, // the public key of the account that will receive the tokens 50 | pub mint: String, // The public key of the token's mint. This identifies which token is being transferred. 51 | pub token_name: String, 52 | pub token_symbol: String, 53 | pub token_amount: Option, // use uiAmount -> field that presents the amount in a format that's ready for display, taking into account the decimals. It represents the amount of tokens being transferred as a floating-point number. 54 | pub detail: String, // use uiAmount -> field that presents the amount in a format that's ready for display, taking into account the decimals. It represents the amount of tokens being transferred as a floating-point number. 55 | } 56 | 57 | 58 | impl fmt::Display for TxCheckedSummary { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | write!(f, "Transaction Checked - {} - Summary:\n\ 61 | Transaction Type: {}\n\ 62 | Source: {}\n\ 63 | Destination: {}\n\ 64 | Mint: {}\n\ 65 | Token Name: {}\n\ 66 | Token Symbol: {}\n\ 67 | Token Amount: {}\n\ 68 | Detail: {}", 69 | self.signature, 70 | self.transaction_type, 71 | self.source, 72 | self.destination, 73 | self.mint, 74 | self.token_name, 75 | self.token_symbol, 76 | if self.token_amount.is_some() { self.token_amount.unwrap() } else { 0.0 }, 77 | self.detail 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | 2 | use diesel::table; 3 | use serde_json::Value; 4 | 5 | table! { 6 | polygon.polygon_crypto_level2_book_data { 7 | id -> Serial, 8 | event_type -> Varchar, 9 | pair -> Varchar, 10 | timestamp -> Bigint, 11 | received_timestamp -> Bigint, 12 | exchange_id -> Bigint, 13 | bid_prices -> Jsonb, 14 | ask_prices -> Jsonb, 15 | } 16 | } 17 | 18 | table! { 19 | polygon.polygon_crypto_aggregate_data { 20 | id -> Serial, 21 | event_type -> Varchar, 22 | pair -> Varchar, 23 | open -> Double, 24 | close -> Double, 25 | high -> Double, 26 | low -> Double, 27 | volume -> Double, 28 | timestamp -> Bigint, 29 | end_time -> Bigint, 30 | vw -> Double, 31 | avg_trade_size -> Bigint, 32 | } 33 | } 34 | 35 | table! { 36 | polygon.polygon_crypto_quote_data { 37 | id -> Serial, 38 | event_type -> Varchar, 39 | pair -> Varchar, 40 | bid_price -> Double, 41 | bid_size -> Double, 42 | ask_price -> Double, 43 | ask_size -> Double, 44 | timestamp -> Bigint, 45 | exchange_id -> Bigint, 46 | received_timestamp -> Bigint, 47 | } 48 | } 49 | 50 | table! { 51 | polygon.polygon_crypto_trade_data { 52 | id -> Serial, 53 | event_type -> Varchar, 54 | pair -> Varchar, 55 | price -> Double, 56 | timestamp -> Bigint, 57 | size -> Double, 58 | conditions -> Jsonb, 59 | trade_id -> Nullable, 60 | exchange_id -> Bigint, 61 | received_timestamp -> Bigint, 62 | } 63 | } 64 | 65 | 66 | 67 | // Define the Diesel models for each table 68 | #[derive(Queryable)] 69 | pub struct PolygonCryptoLevel2BookData { 70 | pub id: i32, 71 | pub event_type: String, 72 | pub pair: String, 73 | pub timestamp: i64, 74 | pub received_timestamp: i64, 75 | pub exchange_id: i64, 76 | pub bid_prices: Value, 77 | pub ask_prices: Value, 78 | } 79 | 80 | #[derive(Insertable)] 81 | #[table_name = "polygon_crypto_level2_book_data"] 82 | pub struct NewPolygonCryptoLevel2BookData { 83 | pub event_type: String, 84 | pub pair: String, 85 | pub timestamp: i64, 86 | pub received_timestamp: i64, 87 | pub exchange_id: i64, 88 | pub bid_prices: Value, 89 | pub ask_prices: Value, 90 | } 91 | 92 | 93 | #[derive(Queryable)] 94 | pub struct PolygonCryptoAggregateData { 95 | pub id: i32, 96 | pub event_type: String, 97 | pub pair: String, 98 | pub open: f64, 99 | pub close: f64, 100 | pub high: f64, 101 | pub low: f64, 102 | pub volume: f64, 103 | pub timestamp: i64, 104 | pub end_time: i64, 105 | pub vw: f64, 106 | pub avg_trade_size: i64, 107 | } 108 | 109 | #[derive(Insertable)] 110 | #[table_name = "polygon_crypto_aggregate_data"] 111 | pub struct NewPolygonCryptoAggregateData { 112 | pub event_type: String, 113 | pub pair: String, 114 | pub open: f64, 115 | pub close: f64, 116 | pub high: f64, 117 | pub low: f64, 118 | pub volume: f64, 119 | pub timestamp: i64, 120 | pub end_time: i64, 121 | pub vw: f64, 122 | pub avg_trade_size: i64, 123 | } 124 | 125 | #[derive(Queryable)] 126 | pub struct PolygonCryptoQuoteData { 127 | pub id: i32, 128 | pub event_type: String, 129 | pub pair: String, 130 | pub bid_price: f64, 131 | pub bid_size: f64, 132 | pub ask_price: f64, 133 | pub ask_size: f64, 134 | pub timestamp: i64, 135 | pub exchange_id: i64, 136 | pub received_timestamp: i64, 137 | } 138 | 139 | #[derive(Insertable)] 140 | #[table_name = "polygon_crypto_quote_data"] 141 | pub struct NewPolygonCryptoQuoteData { 142 | pub event_type: String, 143 | pub pair: String, 144 | pub bid_price: f64, 145 | pub bid_size: f64, 146 | pub ask_price: f64, 147 | pub ask_size: f64, 148 | pub timestamp: i64, 149 | pub exchange_id: i64, 150 | pub received_timestamp: i64, 151 | } 152 | 153 | #[derive(Queryable)] 154 | pub struct PolygonCryptoTradeData { 155 | pub id: i32, 156 | pub event_type: String, 157 | pub pair: String, 158 | pub price: f64, 159 | pub timestamp: i64, 160 | pub size: f64, 161 | pub conditions: Value, 162 | pub trade_id: Option, 163 | pub exchange_id: i64, 164 | pub received_timestamp: i64, 165 | } 166 | 167 | #[derive(Insertable)] 168 | #[table_name = "polygon_crypto_trade_data"] 169 | pub struct NewPolygonCryptoTradeData { 170 | pub event_type: String, 171 | pub pair: String, 172 | pub price: f64, 173 | pub timestamp: i64, 174 | pub size: f64, 175 | pub conditions: Value, 176 | pub trade_id: Option, 177 | pub exchange_id: i64, 178 | pub received_timestamp: i64, 179 | } 180 | 181 | -------------------------------------------------------------------------------- /src/scraper/birdeye_scraper.rs: -------------------------------------------------------------------------------- 1 | //url https://birdeye.so/leaderboard/today?chain=solana 2 | //leaderboard //*[@id="root"]/div[2]/div[2]/div 3 | //example wallet xpath /html/body/div[1]/div[2]/div[2]/div/div[2]/div[2]/div/div/div/div/div/div/div/div[2]/table/tbody/tr[3]/td[2]/div/a 4 | 5 | use fantoccini::{Client, ClientBuilder, error::CmdError, Locator}; 6 | 7 | ///Implementation is ok, chromedriver works, but Birdeye is guarded against automated systems 8 | pub async fn scrape_wallet_addresses() -> Result, CmdError> { 9 | let client = ClientBuilder::native() 10 | .connect("http://localhost:9515") 11 | .await 12 | .expect("[[SCRAPER]] Failed to connect to chromedriver"); 13 | 14 | let mut wallets = Vec::new(); 15 | 16 | client.goto("https://birdeye.so/leaderboard/today?chain=solana").await 17 | .expect("[[SCRAPER]] Failed to navigate to Birdeye top wallets"); 18 | 19 | // Ensure you have the correct XPath for the "TODAY" button; this is just an example 20 | let button = client.find(Locator::XPath("/html/body/div/div[2]/div[2]/div/div[1]/div[2]/div/label[2]/span[2]")).await?; 21 | button.click().await 22 | .expect("[[SCRAPER]] Failed to navigate to Birdeye top wallets TODAY's wallets"); 23 | 24 | // Adjust sleep time based on how long the page takes to load dynamically loaded content 25 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 26 | 27 | // This CSS selector is a placeholder; ensure it matches the actual elements you want to scrape 28 | let wallet_elements = client.find_all(Locator::Css("div.leaderboard-address")).await 29 | .expect("[[SCRAPER]] Failed to find leaderboard wallet addresses"); 30 | 31 | for wallet_element in wallet_elements { 32 | if let Ok(address) = wallet_element.text().await { 33 | println!("Wallet Address: {:?}", &address); 34 | wallets.push(address); 35 | } else { 36 | eprintln!("Failed to extract wallet address from element {:?}", wallet_element) 37 | } 38 | 39 | } 40 | 41 | client.close().await.expect("[[SCRAPER]] Failed close client"); 42 | Ok(wallets) 43 | } -------------------------------------------------------------------------------- /src/scraper/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod birdeye_scraper; -------------------------------------------------------------------------------- /src/server/endpoints/accounts.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::error::Error; 4 | use std::str::FromStr; 5 | use actix_web::{web, HttpResponse, Responder}; 6 | use diesel::serialize::IsNull::No; 7 | use log::Level::Debug; 8 | use reqwest::{Client, header}; 9 | use serde::{Serialize, Deserialize}; 10 | use serde_json::{json, Value}; 11 | use solana_sdk::bs58; 12 | use rust_decimal::{prelude::FromPrimitive, Decimal}; 13 | use rust_decimal::prelude::{One, Zero}; 14 | use solana_sdk::pubkey::Pubkey; 15 | use crate::server::endpoints::holders; 16 | 17 | use crate::server::endpoints::whales::get_token_supply; 18 | 19 | pub fn init_routes(cfg: &mut web::ServiceConfig){ 20 | cfg.service(web::resource("/accounts") 21 | .route(web::post().to(find_accounts)) 22 | ); 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Debug)] 26 | struct FindAccountsRequest { 27 | account_addresses: Vec 28 | } 29 | 30 | pub async fn find_accounts(request: web::Json) -> impl Responder { 31 | let account_data = process_account_addresses(request.account_addresses.clone()).await; 32 | match account_data { 33 | Ok(data) => HttpResponse::Ok().json(data), 34 | Err(_) => HttpResponse::InternalServerError().finish(), 35 | } 36 | } 37 | 38 | pub async fn process_account_addresses(addresses: Vec) -> Result, Box> { 39 | let rpc_url = "https://api.mainnet-beta.solana.com"; 40 | let mut accounts: Vec = Vec::new(); 41 | let client = Client::new(); 42 | 43 | println!("Fetching accounts: {:#?}", addresses); 44 | 45 | for address in addresses { 46 | let params = json!([ 47 | address, 48 | { 49 | "encoding": "base64" 50 | } 51 | ]); 52 | let rpc_request = json!({ 53 | "jsonrpc": "2.0", 54 | "id": 1, 55 | "method": "getAccountInfo", 56 | "params": params 57 | }); 58 | 59 | let response = client.post(rpc_url) 60 | .header("Content-Type", "application/json") 61 | .json(&rpc_request) 62 | .send() 63 | .await?; 64 | 65 | if response.status().is_success() { 66 | let response_text = response.text().await?; 67 | let value: serde_json::Value = serde_json::from_str(&response_text)?; 68 | println!("Got Account: {:#?}", value); 69 | 70 | if let Ok(account_info) = serde_json::from_str::(&response_text) { 71 | if let Some(result) = account_info.result { 72 | accounts.push(AccountDetail { 73 | address: address.clone(), 74 | lamports: result.value.lamports, 75 | owner: result.value.owner, 76 | executable: result.value.executable, 77 | rent_epoch: result.value.rent_epoch, 78 | }); 79 | } 80 | } 81 | } 82 | } 83 | 84 | Ok(accounts) 85 | } 86 | 87 | 88 | ///To get the IDL (Interface Definition Language) for a program, 89 | /// involves a bit more specific steps than just fetching account information because the IDL is stored in a specific account associated with the program. The IDL account is a particular account that Anchor uses to store the program's IDL, making it accessible for clients to understand how to interact with the program. 90 | pub async fn process_idl(idl_address: String) -> Result, Box> { 91 | // Calculate the PDA for the IDL account. 92 | println!("Fetching IDL for program {:#?}", idl_address); 93 | let program_pubkey = Pubkey::from_str(&*idl_address)?; 94 | let seeds = &[b"anchor:idl".as_ref(), program_pubkey.as_ref()]; 95 | let (idl_address, _) = Pubkey::find_program_address(seeds, &program_pubkey); 96 | 97 | // The IDL data is compressed using zstd. First, skip the 8-byte discriminator. 98 | let account_data = process_account_addresses(vec![idl_address.to_string()]).await?; 99 | let idl_data = &account_data[8..]; 100 | 101 | Ok(vec![AccountDetail { address: "val".to_string(), lamports: 1, owner: "val".to_string(), executable: true, rent_epoch: 1 }]) 102 | } 103 | 104 | /** 105 | Returns transaction details for a confirmed transaction. Params: 106 | - Transaction address vector, as base-58 encoded strings 107 | */ 108 | #[derive(Serialize, Debug)] 109 | struct AccountDetail { 110 | address: String, 111 | lamports: u64, 112 | owner: String, 113 | executable: bool, 114 | rent_epoch: u64, 115 | } 116 | 117 | #[derive(Serialize, Deserialize, Debug)] 118 | struct AccountInfoResponse { 119 | jsonrpc: String, 120 | result: Option, 121 | id: u8, 122 | } 123 | 124 | #[derive(Serialize, Deserialize, Debug)] 125 | struct AccountInfoResult { 126 | value: AccountValue, 127 | } 128 | 129 | #[derive(Serialize, Deserialize, Debug)] 130 | struct AccountValue { 131 | data: Vec, 132 | executable: bool, 133 | lamports: u64, 134 | owner: String, 135 | rent_epoch: u64, 136 | } 137 | 138 | 139 | 140 | 141 | // 142 | // async fn get_account_info(address: &str) -> Result> { 143 | // let rpc_url = "https://api.mainnet-beta.solana.com"; 144 | // let request_headers = { 145 | // let mut headers = header::HeaderMap::new(); 146 | // headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); 147 | // headers 148 | // }; 149 | // 150 | // let client = Client::new(); 151 | // let params = json!([ 152 | // idl_address, 153 | // { 154 | // "encoding": "base64" 155 | // } 156 | // ]); 157 | // let rpc_request = json!({ 158 | // "jsonrpc": "2.0", 159 | // "id": 1, 160 | // "method": "getAccountInfo", 161 | // "params": params 162 | // }); 163 | // 164 | // let response = client 165 | // .post(rpc_url) 166 | // .headers(request_headers) 167 | // .json(&rpc_request) 168 | // .send() 169 | // .await?; 170 | // 171 | // if response.status().is_success() { 172 | // let response_text = response.text().await?; 173 | // // TODO most useful print ever 174 | // let value: serde_json::Value = serde_json::from_str(&response_text)?; 175 | // println!("{:#?}", value); 176 | // let account_info: AccountInfoResponse = serde_json::from_str(&response_text)?; 177 | // Ok(account_info) 178 | // } else { 179 | // Err(()) 180 | // } 181 | // } 182 | 183 | -------------------------------------------------------------------------------- /src/server/endpoints/birdeye/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token_prices; -------------------------------------------------------------------------------- /src/server/endpoints/birdeye/token_prices.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | use std::env; 5 | use std::error::Error; 6 | use actix_web::{HttpResponse, Responder, web}; 7 | 8 | // https://docs.birdeye.so/reference/post_defi-multi-price 9 | #[derive(Debug, Serialize, Deserialize)] 10 | struct MultiPriceRequest { 11 | list_address: String, // comma separated addresses 12 | } 13 | 14 | #[derive(Debug, Serialize, Deserialize)] 15 | struct MultiPriceResponse { 16 | data: HashMap, //signature - data 17 | success: bool, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | struct TokenDataWithId { 22 | program_id: String, 23 | value: f64, 24 | #[serde(rename = "updateUnixTime")] 25 | update_unix_time: f64, 26 | #[serde(rename = "updateHumanTime")] 27 | update_human_time: String, 28 | #[serde(rename = "priceChange24h")] 29 | price_change_24_h: f64, 30 | liquidity: f64, 31 | } 32 | 33 | #[derive(Debug, Serialize, Deserialize)] 34 | struct TokenData { 35 | value: f64, 36 | #[serde(rename = "updateUnixTime")] 37 | update_unix_time: f64, 38 | #[serde(rename = "updateHumanTime")] 39 | update_human_time: String, 40 | #[serde(rename = "priceChange24h")] 41 | price_change_24_h: f64, 42 | liquidity: f64, 43 | } 44 | 45 | pub fn init_routes(cfg: &mut web::ServiceConfig){ 46 | cfg.service(web::resource("/token-prices") 47 | .route(web::post().to(find_token_prices)) 48 | ); 49 | } 50 | 51 | pub async fn find_token_prices(request: web::Json) -> impl Responder { 52 | let price_data = fetch_multi_token_prices(request).await; 53 | match price_data { 54 | Ok(data) => HttpResponse::Ok().json(data), 55 | Err(e) => { 56 | eprintln!("Error while fetching prices: {:?}", e); 57 | HttpResponse::InternalServerError().finish() 58 | } 59 | } 60 | } 61 | 62 | 63 | 64 | pub async fn fetch_multi_token_prices(request: web::Json) -> Result, Box> { 65 | let mut token_data: Vec = Vec::new(); 66 | 67 | let client = reqwest::Client::new(); 68 | 69 | //always bring liquidity by default 70 | let api_url = "https://public-api.birdeye.so/defi/multi_price?include_liquidity=true"; 71 | 72 | let mut headers = HeaderMap::new(); 73 | headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 74 | let birdeye_api_key = env::var("BIRDEYE_API_KEY").expect("BIRDEYE_API_KEY must be set"); 75 | headers.insert("X-API-KEY", HeaderValue::from_str(&birdeye_api_key)?); 76 | 77 | 78 | let response = client 79 | .post(api_url) 80 | .headers(headers) 81 | .json(&request) 82 | .send() 83 | .await?; 84 | 85 | 86 | if response.status().is_success() { 87 | let response_text = response.text().await?; 88 | let value: serde_json::Value = serde_json::from_str(&response_text)?; 89 | println!("Got token data: {:#?}", value); 90 | 91 | match serde_json::from_value::(value) { 92 | Ok(multi_price_response) => { 93 | for (id, data) in multi_price_response.data { 94 | token_data.push(TokenDataWithId { 95 | program_id: id, 96 | value: data.value, 97 | update_unix_time: data.update_unix_time, 98 | update_human_time: data.update_human_time, 99 | price_change_24_h: data.price_change_24_h, 100 | liquidity: data.liquidity, 101 | }) 102 | } 103 | } 104 | Err(e) => { 105 | eprintln!("[[TOKEN PRICE]] Failed to deserialize response: {:?}", e); 106 | } 107 | } 108 | 109 | } 110 | 111 | Ok(token_data) 112 | } 113 | -------------------------------------------------------------------------------- /src/server/endpoints/holders.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::error::Error; 4 | use std::str::FromStr; 5 | use actix_web::{web, HttpResponse, Responder}; 6 | use diesel::serialize::IsNull::No; 7 | use log::Level::Debug; 8 | use reqwest::{Client, header}; 9 | use serde::{Serialize, Deserialize}; 10 | use serde_json::{json, Value}; 11 | use solana_sdk::bs58; 12 | use rust_decimal::{prelude::FromPrimitive, Decimal}; 13 | use rust_decimal::prelude::{One, Zero}; 14 | use crate::server::endpoints::holders; 15 | 16 | use crate::server::endpoints::whales::get_token_supply; 17 | 18 | 19 | pub fn init_routes(cfg: &mut web::ServiceConfig) { 20 | cfg.service( 21 | web::resource("/holders") 22 | .route(web::post().to(find_holders)) 23 | ); 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Debug)] 27 | struct HolderStats { 28 | mint_address: String, 29 | token_supply: Option, 30 | initialized_accounts: usize, 31 | holder_accounts: usize, 32 | holder_ratio: f64, 33 | categories: Option, 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug)] 37 | struct HolderCategories { 38 | micro: usize, 39 | small: usize, 40 | medium: usize, 41 | large: usize, 42 | major: usize, 43 | whale: usize, 44 | } 45 | 46 | #[derive(Serialize, Deserialize, Debug)] 47 | struct CategoryDetail { 48 | holders: usize, 49 | max_supply_percentage: f64, 50 | token_amount_range: String, 51 | } 52 | 53 | #[derive(Serialize, Deserialize, Debug)] 54 | struct HolderDetailedStats { 55 | mint_address: String, 56 | token_supply: Option, 57 | initialized_accounts: usize, 58 | holder_accounts: usize, 59 | holder_ratio: f64, 60 | categories: HashMap, 61 | } 62 | async fn find_holders(request: web::Json) -> impl Responder { 63 | let holder_stats = process_mint_addresses(request.token_mint_addresses.clone()).await; 64 | match holder_stats { 65 | Ok(data) => HttpResponse::Ok().json(data), 66 | Err(_) => HttpResponse::InternalServerError().finish(), 67 | } 68 | } 69 | 70 | async fn process_mint_addresses(mint_addresses: Vec) -> Result, Box> { 71 | println!("Finding holders for {:#?}", mint_addresses); 72 | let mut results: Vec = Vec::new(); 73 | let client = Client::new(); 74 | 75 | for mint_address in mint_addresses { 76 | let mint_address_base58 = bs58::encode(&mint_address).into_string(); 77 | 78 | let token_supply_result = get_token_supply(&client, mint_address.as_str()).await; 79 | let supply: Option = if let Ok(token_supply) = token_supply_result { 80 | Some(Decimal::from_f64(token_supply.ui_amount).unwrap_or_else(|| Decimal::zero())) 81 | } else { 82 | None 83 | }; 84 | println!("GOT TOKEN SUPPLY ::: {:#?}", supply); 85 | 86 | 87 | let params = json!([ 88 | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 89 | { 90 | "encoding": "jsonParsed", 91 | "filters": [ 92 | { 93 | "dataSize": 165 // Expected size of a SPL Token account 94 | }, 95 | { 96 | "memcmp": { 97 | "offset": 0, // Offset for the mint address in the account data 98 | "bytes": mint_address // The mint address you're interested in 99 | } 100 | } 101 | ] 102 | } 103 | ]); 104 | 105 | let rpc_request = json!({ 106 | "jsonrpc": "2.0", 107 | "id": 1, 108 | "method": "getProgramAccounts", 109 | "params": params 110 | }); 111 | 112 | // Construct headers 113 | let mut headers = header::HeaderMap::new(); 114 | headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); 115 | let rpc_url = env::var("PRIVATE_SOLANA_QUICKNODE").expect("PRIVATE_SOLANA_QUICKNODE must be set"); 116 | let response = client 117 | .post(rpc_url) 118 | .headers(headers) 119 | .json(&rpc_request) 120 | .send() 121 | .await?; 122 | 123 | if response.status().is_success() { 124 | let response_text = response.text().await?; 125 | 126 | // TODO most useful print ever 127 | //let value: serde_json::Value = serde_json::from_str(&response_text)?; 128 | // println!("{:#?}", value); 129 | 130 | match serde_json::from_str::(&response_text) { 131 | Ok(response_json) => { 132 | let initialized_count = response_json.result.len(); 133 | let mut holder_category_count = HolderCategories { 134 | micro: 0, 135 | small: 0, 136 | medium: 0, 137 | large: 0, 138 | major: 0, 139 | whale: 0, 140 | }; 141 | 142 | 143 | let mut non_empty_wallet_count = 0; // Track wallets with more than 0 tokens 144 | 145 | response_json.result.iter().for_each(|account| { 146 | let ui_amount = account.account.data.parsed.info.token_amount 147 | .as_ref() 148 | .map_or(0.0, |token_amount| token_amount.ui_amount); 149 | 150 | if ui_amount > 0.0 { 151 | non_empty_wallet_count += 1; // Only increment for non-empty wallets 152 | } 153 | 154 | let ui_amount_decimal = Decimal::from_f64(ui_amount).unwrap_or(Decimal::zero()); 155 | 156 | let percentage_of_total_supply = supply 157 | .map(|s| if !s.is_zero() { (ui_amount_decimal / s) * Decimal::from(100) } else { Decimal::zero() }) 158 | .unwrap_or(Decimal::zero()) 159 | .round_dp(4); // Ensure rounding for clarity 160 | 161 | // Category assignment based on the percentage of total supply 162 | match percentage_of_total_supply { 163 | _ if percentage_of_total_supply > Decimal::from_f64(1.0).unwrap() => holder_category_count.whale += 1, 164 | _ if percentage_of_total_supply > Decimal::from_f64(0.1).unwrap() => holder_category_count.major += 1, 165 | _ if percentage_of_total_supply > Decimal::from_f64(0.05).unwrap() => holder_category_count.large += 1, 166 | _ if percentage_of_total_supply > Decimal::from_f64(0.01).unwrap() => holder_category_count.medium += 1, 167 | _ if percentage_of_total_supply > Decimal::from_f64(0.001).unwrap() => holder_category_count.small += 1, 168 | _ if percentage_of_total_supply <= Decimal::from_f64(0.0001).unwrap() => holder_category_count.micro += 1, 169 | _ => (), 170 | }; 171 | }); 172 | 173 | //todo market cap: token price * circulating supply 174 | 175 | let mut category_detail: HashMap; 176 | 177 | if let Some(supply) = supply { 178 | category_detail = HashMap::from([ 179 | ("micro".to_string(), CategoryDetail { 180 | holders: holder_category_count.micro, 181 | max_supply_percentage: 0.0001, 182 | token_amount_range: format!("{:.0} - {:.0} tokens", 183 | Decimal::zero(), 184 | supply * Decimal::from_f64(0.000001).unwrap()), 185 | }), 186 | ("small".to_string(), CategoryDetail { 187 | holders: holder_category_count.small, 188 | max_supply_percentage: 0.001, 189 | token_amount_range: format!("{:.0} - {:.0} tokens", 190 | supply * Decimal::from_f64(0.00001).unwrap() + Decimal::one(), 191 | supply * Decimal::from_f64(0.0001).unwrap()), 192 | }), 193 | ("medium".to_string(), CategoryDetail { 194 | holders: holder_category_count.medium, 195 | max_supply_percentage: 0.01, 196 | token_amount_range: format!("{:.0} - {:.0} tokens", 197 | supply * Decimal::from_f64(0.0001).unwrap(), 198 | supply * Decimal::from_f64(0.001).unwrap() - Decimal::one()), 199 | }), 200 | ("large".to_string(), CategoryDetail { 201 | holders: holder_category_count.large, 202 | max_supply_percentage: 0.05, 203 | token_amount_range: format!("{:.0} - {:.0} tokens", 204 | supply * Decimal::from_f64(0.001).unwrap(), 205 | supply * Decimal::from_f64(0.01).unwrap() - Decimal::one()), 206 | }), 207 | ("major".to_string(), CategoryDetail { 208 | holders: holder_category_count.major, 209 | max_supply_percentage: 0.1, 210 | token_amount_range: format!("{:.0} - {:.0} tokens", 211 | supply * Decimal::from_f64(0.01).unwrap(), 212 | supply * Decimal::from_f64(0.1).unwrap() - Decimal::one()), 213 | }), 214 | ("whale".to_string(), CategoryDetail { 215 | holders: holder_category_count.whale, 216 | max_supply_percentage: 1.00, 217 | token_amount_range: format!(">{:.0} tokens", 218 | supply * Decimal::from_f64(0.1).unwrap()), 219 | }), 220 | ]); 221 | } else { 222 | category_detail = HashMap::new() 223 | } 224 | 225 | 226 | // Now, calculate holder_ratio based on non-empty wallets 227 | let holder_ratio = if initialized_count.clone() > 0 { 228 | non_empty_wallet_count.clone() as f64 / initialized_count.clone() as f64 229 | } else { 230 | 0.0 // Avoid division by zero 231 | }; 232 | 233 | //todo -> store non_empty_wallet_count 234 | //todo -> reshoot this call after n minutes 235 | //todo -> fetch previous non_empty_wallet_count 236 | //todo -> calculate delta = new_non_empty_wallet_count - previous_nonempty_wallet_count 237 | //todo -> calculate calcaute % and store 238 | //todo -> alert 239 | 240 | let stats = HolderDetailedStats { 241 | mint_address: mint_address.clone(), 242 | token_supply: Some(supply.unwrap_or(Decimal::zero())), 243 | initialized_accounts: initialized_count, 244 | holder_accounts: non_empty_wallet_count, 245 | holder_ratio: holder_ratio, 246 | categories: category_detail, 247 | }; 248 | println!("{:#?}", stats); 249 | results.push(stats); 250 | } 251 | Err(e) => { 252 | eprintln!("Failed to parse response JSON: {:?}", e); 253 | } 254 | } 255 | } else { 256 | // If the response status is not successful, log the error response body 257 | let error_text = response.text().await?; 258 | eprintln!("Error fetching data for mint address: {}", error_text); 259 | } 260 | } 261 | 262 | Ok(results) 263 | } 264 | 265 | async fn pretty_print_response(response: SolanaRpcResponse) -> Result<(), Box> { 266 | // Iterate through the accounts and print the details 267 | println!("Parsed Response:"); 268 | for account in response.result.clone() { 269 | println!("Public Key: {}", &account.pubkey); 270 | println!(" Owner: {}", &account.account.data.parsed.info.owner); 271 | println!(" Mint: {}", &account.account.data.parsed.info.mint); 272 | // println!(" Token Amount: {}", account.account.data.parsed.info.token_amount 273 | // .as_ref() // Convert Option to Option<&TokenAmount> 274 | // .map_or("0".to_string(), |token_amount| token_amount.ui_amount_string.clone())); 275 | 276 | println!(" State: {}", account.account.data.parsed.info.state); 277 | println!("-------------------------------------------------"); 278 | } 279 | 280 | Ok(()) 281 | } 282 | 283 | 284 | #[derive(Serialize, Deserialize, Clone, Debug)] 285 | struct FindHoldersRequest { 286 | pub token_mint_addresses: Vec, 287 | } 288 | 289 | /// Requet Structure 290 | #[derive(Serialize, Deserialize, Clone, Debug)] 291 | struct SolanaRpcFilter { 292 | pub memcmp: Option, 293 | pub dataSize: Option, 294 | } 295 | 296 | #[derive(Serialize, Deserialize, Clone, Debug)] 297 | struct SolanaRpcMemcmp { 298 | pub offset: u64, 299 | pub bytes: String, 300 | } 301 | 302 | #[derive(Serialize, Deserialize, Clone, Debug)] 303 | struct SolanaRpcRequestParams { 304 | pub filters: Vec, 305 | } 306 | 307 | #[derive(Serialize, Deserialize, Clone, Debug)] 308 | struct SolanaRpcRequest { 309 | pub jsonrpc: String, 310 | pub id: u8, 311 | pub method: String, 312 | pub params: Vec, // The second parameter can have a complex structure 313 | } 314 | 315 | #[derive(Serialize, Deserialize, Debug, Clone)] 316 | struct SolanaRpcResponse { 317 | jsonrpc: String, 318 | result: Vec, 319 | id: u8, 320 | } 321 | 322 | #[derive(Serialize, Deserialize, Debug, Clone)] 323 | struct AccountInfo { 324 | pubkey: String, 325 | account: AccountData, 326 | } 327 | 328 | #[derive(Serialize, Deserialize, Debug, Clone)] 329 | struct AccountData { 330 | data: AccountDataDetails, 331 | executable: bool, 332 | lamports: u64, 333 | owner: String, 334 | rentEpoch: u64, 335 | space: u64, 336 | } 337 | 338 | #[derive(Serialize, Deserialize, Debug, Clone)] 339 | #[serde(rename_all = "camelCase")] 340 | struct AccountDataDetails { 341 | parsed: ParsedAccountData, 342 | program: String, 343 | space: u64, 344 | } 345 | 346 | #[derive(Serialize, Deserialize, Debug, Clone)] 347 | struct ParsedAccountData { 348 | info: TokenAccountInfo, 349 | #[serde(rename = "type")] 350 | account_type: String, 351 | } 352 | 353 | #[derive(Serialize, Deserialize, Debug, Clone)] 354 | struct TokenAccountInfo { 355 | #[serde(rename = "isNative")] 356 | is_native: bool, 357 | mint: String, 358 | owner: String, 359 | state: String, 360 | #[serde(rename = "tokenAmount")] 361 | token_amount: Option, 362 | } 363 | 364 | #[derive(Serialize, Deserialize, Debug, Clone)] 365 | struct TokenAmount { 366 | amount: String, 367 | decimals: u8, 368 | #[serde(rename = "uiAmount")] 369 | ui_amount: f64, 370 | #[serde(rename = "uiAmountString")] 371 | ui_amount_string: String, 372 | } -------------------------------------------------------------------------------- /src/server/endpoints/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod holders; 2 | pub mod whales; 3 | pub mod new_spls; 4 | pub mod signatures_for_address; 5 | pub mod transactions; 6 | pub mod accounts; 7 | pub mod birdeye; 8 | -------------------------------------------------------------------------------- /src/server/endpoints/new_spls.rs: -------------------------------------------------------------------------------- 1 | // use actix_web::web::block; 2 | // use actix_web::{web, HttpResponse, Responder}; 3 | // use reqwest::{Client, header}; 4 | // use serde::{Serialize, Deserialize}; 5 | // use serde_json::{json, Value}; 6 | // use solana_sdk::bs58; 7 | // use rust_decimal::{prelude::FromPrimitive, Decimal}; 8 | // 9 | // use lazy_static::lazy_static; 10 | // use hashbrown::HashMap; 11 | // use std::os::macos::fs; 12 | // use std::sync::Mutex; 13 | // 14 | // use std::{env, thread}; 15 | // 16 | // use chrono::Local; 17 | // use csv::Writer; 18 | // use std::fs::File; 19 | // use std::fs::create_dir_all; 20 | // use std::path::{Path, PathBuf}; 21 | // use std::error::Error; 22 | // use std::str::FromStr; 23 | // use solana_client::rpc_client; 24 | // 25 | // use solana_client::rpc_client::RpcClient; 26 | // use solana_client::rpc_config::RpcTransactionConfig; 27 | // use solana_sdk::signature::{Signature, Signer}; 28 | // use solana_sdk::transaction::Transaction; 29 | // use solana_sdk::commitment_config::CommitmentConfig; 30 | // use solana_sdk::program_option::COption; 31 | // use solana_sdk::pubkey::Pubkey; 32 | // 33 | // 34 | // /** 35 | // Detect New SPL Tokens: Incorporate a monitoring mechanism to scan the network for new token mint transactions. 36 | // This could involve tracking the createAccount and initializeMint instructions within the Token Program. 37 | // 38 | // Resources: 39 | // https://solana.stackexchange.com/questions/9228/how-to-find-a-new-token-in-the-block 40 | // https://solana.com/es/docs/rpc/http/getsignaturesforaddress 41 | // https://github.com/solana-labs/solana-program-library/blob/master/token/program/src/instruction.rs#L39-L45 42 | // 43 | // */ 44 | // 45 | // pub enum TokenInstruction { 46 | // InitializeMint { 47 | // decimals: u8, 48 | // mint_authority: Pubkey, 49 | // freeze_authority: COption, 50 | // }, 51 | // } 52 | // 53 | // #[derive(Serialize, Deserialize, Debug)] 54 | // struct InitializeMintData { 55 | // decimals: u8, 56 | // mint_authority: String, // Using String to represent Pubkey for simplicity in JSON 57 | // freeze_authority: Option, // Optional String 58 | // } 59 | // 60 | // #[derive(Serialize, Deserialize, Debug)] 61 | // struct GetSignaturesResponse { 62 | // jsonrpc: String, 63 | // result: Vec, 64 | // id: u64, 65 | // } 66 | // 67 | // #[derive(Serialize, Deserialize, Debug)] 68 | // struct SignatureResult { 69 | // err: Option, // Use more specific type if error structure is known 70 | // memo: Option, 71 | // signature: String, 72 | // slot: u64, 73 | // blockTime: Option, 74 | // } 75 | // 76 | // pub fn init_routes(cfg: &mut web::ServiceConfig) { 77 | // cfg.service( 78 | // web::resource("/new-spls") 79 | // .route(web::get().to(find_new_spls)) 80 | // ); 81 | // } 82 | // 83 | // #[derive(Serialize, Deserialize, Debug)] 84 | // struct NewSplToken { 85 | // mint_address: String, 86 | // // Additional fields can be added as necessary 87 | // } 88 | // 89 | // async fn find_new_spls() -> impl Responder { 90 | // let client = Client::new(); 91 | // let solana_rpc_url = env::var("SOLANA_RPC_URL").expect("SOLANA_RPC_URL must be set"); 92 | // let token_program_id = env::var("SOLANA_TOKEN_PROGRAM").expect("SOLANA_TOKEN_PROGRAM must be set"); 93 | // 94 | // // Fetch the recent transaction signatures for the given address 95 | // let resp = client.post(&solana_rpc_url) 96 | // .json(&serde_json::json!({ 97 | // "jsonrpc": "2.0", 98 | // "id": 1, 99 | // "method": "getSignaturesForAddress", 100 | // "params": [ 101 | // token_program_id, 102 | // { 103 | // "limit": 10 // Adjust limit as needed 104 | // } 105 | // ] 106 | // })) 107 | // .send() 108 | // .await?; 109 | // 110 | // let signatures: Vec = resp.json().await?; // Parse the response to extract transaction signatures 111 | // // 112 | // // let mut new_spls = Vec::new(); 113 | // // 114 | // // for signature in signatures { 115 | // // // Fetch transaction details 116 | // // let transaction_resp = client.post(&solana_rpc_url) 117 | // // .json(&serde_json::json!({ 118 | // // "jsonrpc": "2.0", 119 | // // "id": 1, 120 | // // "method": "getTransaction", 121 | // // "params": [signature, "jsonParsed"] 122 | // // })) 123 | // // .send() 124 | // // .await?; 125 | // // 126 | // // let transaction: Value = transaction_resp.json().await?; // Parse the response into your Transaction struct 127 | // // 128 | // // // Analyze the transaction to find InitializeMint instructions 129 | // // for instruction in transaction.message.instructions { 130 | // // if is_initialize_mint(&instruction) { 131 | // // new_spls.push(NewSplToken { 132 | // // mint_address: instruction.program_id.clone(), 133 | // // // Populate other fields as needed 134 | // // }); 135 | // // } 136 | // // } 137 | // // } 138 | // 139 | // Ok(()) 140 | // } 141 | // 142 | // fn is_initialize_mint(instruction: &Value) -> bool { 143 | // // Implement logic to check if the instruction is an InitializeMint instruction 144 | // // This involves checking the program_id and the instruction data format 145 | // true 146 | // } 147 | // 148 | -------------------------------------------------------------------------------- /src/server/endpoints/signatures_for_address.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use std::collections::HashMap; 3 | use std::env; 4 | use std::error::Error; 5 | use actix_web::{web, HttpResponse, Responder}; 6 | use chrono::Utc; 7 | use reqwest::{Client, header}; 8 | use serde_json::{json, Value}; 9 | use solana_sdk::bs58; 10 | 11 | 12 | #[derive(Serialize, Deserialize, Clone, Debug)] 13 | struct SignaturesResponse { 14 | pub signatures: Vec, 15 | pub count: usize 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug)] 19 | struct GetSignaturesResponse { 20 | jsonrpc: String, 21 | result: Vec, 22 | id: u64, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Debug)] 26 | struct SignatureResult { 27 | err: Option, // Use more specific type if error structure is known 28 | memo: Option, 29 | signature: String, 30 | slot: u64, 31 | blockTime: Option, 32 | } 33 | 34 | pub fn init_routes(cfg: &mut web::ServiceConfig) { 35 | cfg.service( 36 | web::resource("/signatures") 37 | .route(web::post().to(find_signatures_for_address)) 38 | ); 39 | } 40 | 41 | #[derive(Serialize, Deserialize, Clone, Debug)] 42 | struct FindSignaturesForAddressRequest { 43 | pub address: String, 44 | } 45 | 46 | async fn find_signatures_for_address(request: web::Json) -> impl Responder { 47 | let address = &request.address; 48 | let signature_data = get_signatures(address).await; 49 | match signature_data { 50 | Ok((signatures, count)) => HttpResponse::Ok().json(SignaturesResponse { signatures, count }), 51 | Err(_) => HttpResponse::InternalServerError().finish(), 52 | } 53 | } 54 | 55 | 56 | async fn get_signatures(address: &String) -> Result<(Vec, usize), Box> { 57 | println!("Getting signatures for {:#?}", address); 58 | let mut signatures = Vec::new(); 59 | let client = Client::new(); 60 | let solana_rpc_url = env::var("PRIVATE_SOLANA_QUICKNODE").expect("PRIVATE_SOLANA_QUICKNODE must be set"); 61 | let current_timestamp = Utc::now().timestamp_millis(); 62 | 63 | let mut headers = header::HeaderMap::new(); 64 | headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); 65 | 66 | let rpc_request = json!({ 67 | "jsonrpc": "2.0", 68 | "id": 1, 69 | "method": "getSignaturesForAddress", 70 | "params": [ 71 | address, 72 | { 73 | "limit": 1000, 74 | "before": "3uLQNx9U85TnmaBWLrjQHEqauADbjhQEQriVtVY2yDzHWYNwZ1BBMMtZcjTQvkkT7bV7nT7z9B8h3zJ3xz9RYU5w" 75 | } 76 | ] 77 | }); 78 | 79 | let response= client 80 | .post(&solana_rpc_url) 81 | .headers(headers) 82 | .json(&rpc_request) 83 | .send() 84 | .await?; 85 | 86 | if response.status().is_success(){ 87 | let response_text = response.text().await?; 88 | let value: serde_json::Value = serde_json::from_str(&response_text)?; 89 | println!("Got signatures for address {:#?}: {:#?}", address, value); 90 | match serde_json::from_str::(&response_text) { 91 | Ok(response_json) => { 92 | for sig in response_json.result { 93 | signatures.push(sig.signature); 94 | } 95 | 96 | } 97 | Err(e) => { 98 | eprintln!("Failed to parse response JSON: {:?}", e); 99 | } 100 | } 101 | } else { 102 | // If the response status is not successful, log the error response body 103 | let error_text = response.text().await?; 104 | eprintln!("Error fetching data for mint address: {}", error_text); 105 | } 106 | let count = signatures.len(); 107 | let result = (signatures, count); 108 | Ok(result) 109 | } -------------------------------------------------------------------------------- /src/server/endpoints/transactions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::error::Error; 4 | use std::str::FromStr; 5 | use actix_web::{web, HttpResponse, Responder}; 6 | use diesel::serialize::IsNull::No; 7 | use log::Level::Debug; 8 | use reqwest::{Client, header}; 9 | use serde::{Serialize, Deserialize}; 10 | use serde_json::{json, Value}; 11 | use solana_sdk::bs58; 12 | use rust_decimal::{prelude::FromPrimitive, Decimal}; 13 | use rust_decimal::prelude::{One, Zero}; 14 | use crate::server::endpoints::holders; 15 | 16 | use crate::server::endpoints::whales::get_token_supply; 17 | 18 | pub fn init_routes(cfg: &mut web::ServiceConfig){ 19 | cfg.service(web::resource("/transactions") 20 | .route(web::post().to(find_transactions)) 21 | ); 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug)] 25 | struct FindTransactionsRequest { 26 | transaction_signatures: Vec 27 | } 28 | 29 | async fn find_transactions(request: web::Json) -> impl Responder { 30 | let transaction_data = process_transaction_signatures(request.transaction_signatures.clone()).await; 31 | match transaction_data { 32 | Ok(data) => HttpResponse::Ok().json(data), 33 | Err(_) => HttpResponse::InternalServerError().finish(), 34 | } 35 | } 36 | 37 | /** 38 | Returns transaction details for a confirmed transaction. Params: 39 | - Transaction signature vector, as base-58 encoded strings 40 | */ 41 | async fn process_transaction_signatures(signatures: Vec) -> Result, Box> { 42 | 43 | let mut request_headers = header::HeaderMap::new(); 44 | request_headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); 45 | let rpc_url = "https://api.mainnet-beta.solana.com";//env::var("PRIVATE_SOLANA_QUICKNODE").expect("PRIVATE_SOLANA_QUICKNODE must be set"); 46 | 47 | println!("Finding transactions for signatures {:#?}", signatures); 48 | let mut transactions: Vec= Vec::new(); 49 | let client = Client::new(); 50 | 51 | for signature in signatures { 52 | let params = json!([ 53 | signature, 54 | { 55 | "encoding": "jsonParsed" 56 | } 57 | ]); 58 | 59 | let rpc_request = json!({ 60 | "jsonrpc": "2.0", 61 | "id": 1, 62 | "method": "getTransaction", 63 | "params": params 64 | }); 65 | 66 | 67 | let response = client 68 | .post(rpc_url.clone()) 69 | .headers(request_headers.clone()) 70 | .json(&rpc_request) 71 | .send() 72 | .await?; 73 | 74 | if response.status().is_success() { 75 | let response_text = response.text().await?; 76 | // TODO most useful print ever 77 | let value: serde_json::Value = serde_json::from_str(&response_text)?; 78 | println!("{:#?}", value); 79 | 80 | //TODO FINISH THIS 81 | } 82 | } 83 | 84 | Ok(transactions) 85 | } 86 | 87 | #[derive(Serialize, Deserialize, Clone, Debug)] 88 | struct TransactionResponse { 89 | jsonrpc: String, 90 | result: TransactionResult, 91 | block_time: Option, 92 | id: u8, 93 | } 94 | 95 | #[derive(Serialize, Deserialize, Clone, Debug)] 96 | struct TransactionResult { 97 | meta: TransactionMeta, 98 | slot: u64, 99 | transaction: TransactionDetails, 100 | } 101 | 102 | #[derive(Serialize, Deserialize, Clone, Debug)] 103 | struct TransactionMeta { 104 | err: Option, 105 | fee: u64, 106 | inner_instructions: Vec, 107 | post_balances: Vec, 108 | post_token_balances: Vec, 109 | pre_balances: Vec, 110 | pre_token_balances: Vec, 111 | rewards: Vec, 112 | status: serde_json::Value, 113 | } 114 | 115 | #[derive(Serialize, Deserialize, Clone, Debug)] 116 | struct TransactionDetails { 117 | message: TransactionMessage, 118 | signatures: Vec, 119 | } 120 | 121 | #[derive(Serialize, Deserialize, Clone, Debug)] 122 | struct TransactionMessage { 123 | account_keys: Vec, 124 | header: TransactionHeader, 125 | instructions: Vec, 126 | recent_blockhash: String, 127 | } 128 | 129 | #[derive(Serialize, Deserialize, Clone, Debug)] 130 | struct TransactionHeader { 131 | num_readonly_signed_accounts: u8, 132 | num_readonly_unsigned_accounts: u8, 133 | num_required_signatures: u8, 134 | } 135 | 136 | #[derive(Serialize, Deserialize, Clone, Debug)] 137 | struct TransactionInstruction { 138 | accounts: Vec, 139 | data: String, 140 | program_id_index: u8, 141 | } 142 | -------------------------------------------------------------------------------- /src/server/endpoints/whales.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::block; 2 | use actix_web::{web, HttpResponse, Responder}; 3 | use reqwest::{Client, header}; 4 | use serde::{Serialize, Deserialize}; 5 | use serde_json::{json, Value}; 6 | use solana_sdk::bs58; 7 | use rust_decimal::{prelude::FromPrimitive, Decimal}; 8 | 9 | use lazy_static::lazy_static; 10 | use hashbrown::HashMap; 11 | use std::os::macos::fs; 12 | use std::sync::Mutex; 13 | 14 | use std::{env, thread}; 15 | 16 | use chrono::Local; 17 | use csv::Writer; 18 | use std::fs::File; 19 | use std::fs::create_dir_all; 20 | use std::path::{Path, PathBuf}; 21 | use std::error::Error; 22 | 23 | 24 | //https://solana.com/es/docs/rpc/http/gettokenlargestaccounts 25 | type TokenSupplyMap = HashMap<(String, u8), TokenSupply>; 26 | 27 | 28 | lazy_static! { 29 | static ref TOKEN_SUPPLIES: Mutex = Mutex::new({ 30 | let mut m = TokenSupplyMap::new(); 31 | m.insert( 32 | ("RETARDIO".to_string(), 6), 33 | TokenSupply { 34 | amount: "999741137074833".to_string(), 35 | decimals: 6, 36 | ui_amount: 999741137.074833, 37 | ui_amount_string: "999741137.074833".to_string(), 38 | } 39 | ); 40 | m 41 | }); 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Clone, Debug)] 45 | struct FindWhalesRequest { 46 | pub token_mint_addresses: Vec, 47 | } 48 | 49 | 50 | #[derive(Serialize, Deserialize, Debug)] 51 | pub struct TokenSupplyRpcResponse { 52 | pub jsonrpc: String, 53 | pub result: TokenSupplyRpcResult, 54 | pub id: u64, 55 | } 56 | 57 | #[derive(Serialize, Deserialize, Debug)] 58 | pub struct TokenSupplyRpcResult { 59 | pub context: RpcContext, 60 | pub value: TokenSupply, 61 | } 62 | 63 | #[derive(Serialize, Deserialize, Debug)] 64 | pub struct RpcContext { 65 | pub slot: u64, 66 | } 67 | 68 | #[derive(Serialize, Deserialize, Debug)] 69 | pub struct TokenSupply { 70 | pub amount: String, 71 | pub decimals: u8, 72 | #[serde(rename = "uiAmount")] 73 | pub ui_amount: f64, 74 | #[serde(rename = "uiAmountString")] 75 | pub ui_amount_string: String, 76 | } 77 | 78 | #[derive(Serialize, Deserialize, Debug)] 79 | pub struct WhaleAccountsRpcResponse { 80 | pub jsonrpc: String, 81 | pub result: WhaleAccountRpcResult, 82 | pub id: u64, 83 | } 84 | 85 | 86 | #[derive(Serialize, Deserialize, Debug)] 87 | pub struct WhaleAccountRpcResult { 88 | pub context: RpcContext, 89 | pub value: Vec, 90 | } 91 | 92 | 93 | #[derive(Serialize, Deserialize, Debug, Clone)] 94 | struct WhaleAccount { 95 | address: String, 96 | amount: String, 97 | decimals: u8, 98 | #[serde(rename = "uiAmount")] 99 | ui_amount: f64, 100 | #[serde(rename = "uiAmountString")] 101 | ui_amount_string: String, 102 | } 103 | 104 | #[derive(Serialize, Deserialize, Debug, Clone)] 105 | struct WhaleDetail { 106 | pub address: String, 107 | pub amount: String, 108 | pub decimals: u8, 109 | pub ui_amount_string: String, 110 | pub owned_percentage: Decimal 111 | } 112 | 113 | 114 | pub fn init_routes(cfg: &mut web::ServiceConfig) { 115 | cfg.service( 116 | web::resource("/whales") 117 | .route(web::post().to(find_whales)) 118 | ); 119 | } 120 | 121 | async fn find_whales(request: web::Json) -> impl Responder { 122 | let whale_data = get_largest_accounts_for_mints(request.token_mint_addresses.clone()).await; 123 | match whale_data { 124 | Ok(data) => HttpResponse::Ok().json(data), 125 | Err(_) => HttpResponse::InternalServerError().finish(), 126 | } 127 | } 128 | 129 | async fn get_largest_accounts_for_mints(mint_addresses: Vec) -> Result, Box> { 130 | println!("Finding whales for {:#?}", mint_addresses); 131 | let solana = env::var("PRIVATE_SOLANA_QUICKNODE").expect("PRIVATE_SOLANA_QUICKNODE must be set"); 132 | let client = Client::new(); 133 | let mut all_whales: Vec = Vec::new(); 134 | 135 | for mint_address in &mint_addresses { 136 | // Fetch the total supply for the mint address 137 | if let Ok(supply) = get_token_supply(&client, &mint_address).await { 138 | let total_supply: Decimal = Decimal::from_f64(supply.ui_amount).unwrap_or_else(|| Decimal::new(0, 0)); 139 | 140 | println!("Total supply for {:#?} is {:#?}", mint_address.clone(), total_supply.clone()); 141 | 142 | let rpc_request = json!({ 143 | "jsonrpc": "2.0", 144 | "id": 1, 145 | "method": "getTokenLargestAccounts", 146 | "params": [mint_address] 147 | }); 148 | 149 | println!("Requesting whales for {:#?} ::: {:#?}", mint_address, rpc_request); 150 | 151 | let response = client 152 | .post(solana.clone()) 153 | .header("Content-Type", "application/json") 154 | .json(&rpc_request) 155 | .send() 156 | .await?; 157 | 158 | if response.status().is_success() { 159 | let response_text = response.text().await?; 160 | let value: serde_json::Value = serde_json::from_str(&response_text)?; 161 | println!("Largest account holders for {:#?} are: {:#?}", mint_address, value); 162 | let largest_accounts: WhaleAccountsRpcResponse = serde_json::from_str(&response_text)?; 163 | 164 | for account in largest_accounts.result.value { 165 | let ui_amount: Decimal = account.ui_amount_string.parse::()?; 166 | 167 | let owned_percentage = (ui_amount / total_supply) * Decimal::from(100); 168 | 169 | println!("WHALE {:#?} own {:#?} % of {:#?}", account.address.clone(), owned_percentage, mint_address ); 170 | 171 | 172 | all_whales.push(WhaleDetail { 173 | address: account.address, 174 | amount: account.amount, 175 | decimals: account.decimals, 176 | ui_amount_string: account.ui_amount_string, 177 | owned_percentage, // Include the ownership percentage 178 | }); 179 | } 180 | } else { 181 | println!("Error fetching data for mint address: {}", mint_address); 182 | } 183 | } 184 | } 185 | 186 | println!("ALL THE WHALES FOR {:#?}: {:#?}", &mint_addresses, all_whales); 187 | 188 | if let Some(a) = mint_addresses.get(0) { 189 | let result = write_whales_to_csv(a, &all_whales).await; 190 | if let Err(error) = result { 191 | println!("Failed to write whale details to CSV: {:#?}", error); 192 | } 193 | } else { 194 | println!("No mint addresses found."); 195 | } 196 | Ok(all_whales) 197 | } 198 | 199 | 200 | pub async fn get_token_supply(client: &Client, mint_address: &str) -> Result> { 201 | println!("Finding token supply for {:#?}", mint_address); 202 | 203 | let rpc_request = json!({ 204 | "jsonrpc": "2.0", 205 | "id": 1, 206 | "method": "getTokenSupply", 207 | "params": [mint_address] 208 | }); 209 | 210 | let response = client 211 | .post(env::var("PRIVATE_SOLANA_QUICKNODE").expect("PRIVATE_SOLANA_QUICKNODE must be set")) 212 | .header("Content-Type", "application/json") 213 | .json(&rpc_request) 214 | .send() 215 | .await?; 216 | 217 | match response.status() { 218 | reqwest::StatusCode::OK => { 219 | let response_text = response.text().await?; 220 | println!("Token supply for {:#?}: {:#?}", mint_address, response_text); 221 | let supply_response: TokenSupplyRpcResponse = serde_json::from_str(&response_text)?; 222 | println!("Deserialized token response ::: {:#?}", supply_response); 223 | Ok(supply_response.result.value) 224 | }, 225 | _ => { 226 | let error_message = format!("Error fetching token supply for mint address: {}", mint_address); 227 | eprintln!("{}", error_message); 228 | Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, error_message))) 229 | }, 230 | } 231 | } 232 | 233 | pub async fn write_whales_to_csv(program_address: &str, whales: &[WhaleDetail]) -> Result<(), Box> { 234 | let date = Local::now().format("%Y-%m-%d").to_string(); 235 | let dir_path = PathBuf::from(format!("data/{}/{}/whales", program_address, date)); 236 | 237 | // Ensure the directory exists 238 | std::fs::create_dir_all(&dir_path)?; 239 | 240 | let file_path = dir_path.join("whale_details.csv"); 241 | // Handle the Result returned by File::create using `?` to propagate errors 242 | let file = File::create(&file_path)?; 243 | let mut wtr = Writer::from_writer(file); 244 | 245 | for whale in whales { 246 | // Here, `serialize` writes each whale detail to the CSV. 247 | // If an error occurs, it will be converted to Box and returned 248 | wtr.serialize(whale)?; 249 | } 250 | 251 | // Explicitly handle flush to ensure data is written 252 | wtr.flush()?; 253 | Ok(()) 254 | } -------------------------------------------------------------------------------- /src/server/http_server.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{App, HttpServer, web}; 2 | use std::sync::Arc; 3 | 4 | use crate::server::endpoints::accounts; 5 | use crate::server::endpoints::transactions; 6 | use crate::server::endpoints::signatures_for_address; 7 | use crate::server::endpoints::holders; 8 | use crate::server::endpoints::whales; 9 | use crate::server::endpoints::new_spls; 10 | 11 | use crate::server::endpoints::birdeye::token_prices; 12 | 13 | 14 | pub async fn run_server() -> std::io::Result<()> { 15 | HttpServer::new(|| { 16 | App::new() 17 | .service(web::scope("/api") 18 | .configure(signatures_for_address::init_routes) 19 | .configure(holders::init_routes) 20 | .configure(whales::init_routes) 21 | .configure(transactions::init_routes) 22 | .configure(accounts::init_routes) 23 | .configure(token_prices::init_routes) 24 | ) 25 | }) 26 | .bind("127.0.0.1:8080")? 27 | .run() 28 | .await 29 | } 30 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ws_server; 2 | pub mod http_server; 3 | pub mod endpoints; -------------------------------------------------------------------------------- /src/server/ws_server.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{SinkExt, StreamExt}; 2 | use std::sync::Arc; 3 | use tokio::net::{TcpListener, TcpStream}; 4 | use tokio::sync::{mpsc, Mutex}; 5 | use tokio_tungstenite::{accept_async, tungstenite::protocol::Message}; 6 | use tokio::time::{interval, Duration}; 7 | 8 | pub struct WebSocketServer { 9 | host: String, 10 | port: String, 11 | } 12 | 13 | impl WebSocketServer { 14 | pub fn new(host: String, port: String) -> Self { 15 | WebSocketServer { host, port } 16 | } 17 | 18 | pub async fn run(&self) { 19 | let addr = format!("{}:{}", self.host, self.port); 20 | let listener = TcpListener::bind(&addr).await.expect("Failed to bind"); 21 | println!("WebSocket server listening on: {}", addr); 22 | 23 | let connection_id = Arc::new(Mutex::new(0u64)); // Counter for assigning IDs to connections 24 | 25 | while let Ok((stream, _)) = listener.accept().await { 26 | let id = { 27 | let mut lock = connection_id.lock().await; 28 | *lock += 1; 29 | *lock 30 | }; 31 | 32 | println!("New connection: {}", id); 33 | tokio::spawn(handle_connection(stream, id)); 34 | } 35 | } 36 | } 37 | 38 | async fn handle_connection(stream: TcpStream, id: u64) { 39 | println!("Handling connection {}", id); 40 | let ws_stream = accept_async(stream).await.expect("Error during the websocket handshake"); 41 | 42 | // Split the WebSocket stream into a sender and receiver 43 | let (mut ws_sender, mut ws_receiver) = ws_stream.split(); 44 | 45 | // Create a channel for sending "heartbeat" messages 46 | let (tx, mut rx) = mpsc::channel(100); 47 | 48 | // Spawn a task to read from the channel and send messages 49 | tokio::spawn(async move { 50 | while let Some(msg) = rx.recv().await { 51 | ws_sender.send(msg).await.expect("Failed to send message"); 52 | } 53 | }); 54 | 55 | // Spawn a task to send a heartbeat message every 10 seconds 56 | tokio::spawn(async move { 57 | let mut interval = interval(Duration::from_secs(10)); 58 | loop { 59 | interval.tick().await; 60 | println!("Sending heartbeat to connection {}", id); 61 | if tx.send(Message::Text(format!("heartbeat to {}", id))).await.is_err() { 62 | println!("Connection {} closed, stopping heartbeat.", id); 63 | break; 64 | } 65 | } 66 | }); 67 | 68 | // Optionally handle incoming messages 69 | while let Some(msg) = ws_receiver.next().await { 70 | match msg { 71 | Ok(msg) => println!("Received a message from connection {}: {:?}", id, msg), 72 | Err(e) => { 73 | eprintln!("Error receiving message from connection {}: {:?}", id, e); 74 | break; 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/strategy.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvizardev/solana-rust-sniping-bot/003f77d6de8b9f93938e317d4d2fbd9437e62a39/src/strategy.md -------------------------------------------------------------------------------- /src/subscriber/consume_stream.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use crossbeam_channel::Sender; 3 | use futures_util::StreamExt; 4 | use serde_json::Value; 5 | use std::error::Error as StdError; 6 | use tokio::net::TcpStream; 7 | use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, WebSocketStream, MaybeTlsStream}; 8 | use tungstenite::Error; 9 | 10 | use crate::subscriber::websocket_event_types::WebsocketEventTypes; 11 | 12 | 13 | pub async fn consume_stream( 14 | ws_stream: &mut WebSocketStream>, 15 | tx: Sender, 16 | ) { 17 | while let Some(message) = ws_stream.next().await { 18 | // println!("Got message message: {:?}", message); 19 | match message { 20 | 21 | Ok(Message::Text(text)) => { 22 | // if text.contains("initialize2") { 23 | // println!("[[CONSUM STREAM]] GOT MESSAGE: {}", text); 24 | if let Err(e) = process_text_message(text, &tx).await { 25 | // eprintln!("Failed to process text message: {:?}", e); 26 | } 27 | // } 28 | 29 | 30 | } 31 | Err(e) => eprintln!("Error receiving message: {:?}", e), 32 | _ => {} 33 | } 34 | } 35 | } 36 | 37 | async fn process_text_message( 38 | text: String, 39 | tx: &Sender, 40 | ) -> Result<(), Box> { 41 | let event_jsons: Result = serde_json::from_str(&text); 42 | match event_jsons { 43 | Ok(events) => { 44 | process_json_events(events, tx)?; 45 | } 46 | Err(e) => { 47 | eprintln!("Error parsing JSON: {:?}", e); 48 | } 49 | } 50 | Ok(()) 51 | } 52 | 53 | fn process_json_events( 54 | events: Value, 55 | tx: &Sender, 56 | ) -> Result<(), Box> { 57 | if events.is_array() { 58 | for event in events.as_array().unwrap() { 59 | process_single_event(event, tx)?; 60 | } 61 | } else { 62 | process_single_event(&events, tx)?; 63 | } 64 | Ok(()) 65 | } 66 | 67 | fn process_single_event( 68 | event: &Value, 69 | sender: &Sender, 70 | ) -> Result<(), Box> { 71 | match T::deserialize_event(event) { 72 | Ok(event) => { 73 | sender.send(event).map_err(|e| e.into()) 74 | } 75 | Err(e) => { 76 | // eprintln!("consume_stream.process_single_event: Error deserializing message: {:?}", e); 77 | Err(e.into()) 78 | } 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/subscriber/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod websocket_subscriber; 2 | pub mod consume_stream; 3 | pub mod websocket_event_types; -------------------------------------------------------------------------------- /src/subscriber/websocket_event_types.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use serde_json::Value; 3 | use crate::models::solana::solana_event_types::SolanaEventTypes; 4 | use crate::models::solana::solana_logs_notification::SolanaLogsNotification; 5 | use crate::models::solana::solana_account_notification::SolanaAccountNotification; 6 | use crate::models::solana::solana_program_notification::SolanaProgramNotification; 7 | use crate::util::serde_helper::deserialize_into; 8 | 9 | ///Trait used for event deserialization 10 | pub trait WebsocketEventTypes: Sized { 11 | // Returns a descriptive name or type of the event. 12 | fn event_type(&self) -> String; 13 | 14 | // Method to deserialize a JSON value into a specific event type. 15 | fn deserialize_event(value: &Value) -> Result>; 16 | } 17 | 18 | impl WebsocketEventTypes for SolanaEventTypes { 19 | fn event_type(&self) -> String { 20 | match self { 21 | SolanaEventTypes::LogNotification(_) => "LogNotification".to_string(), 22 | SolanaEventTypes::ProgramNotification(_) => "ProgramNotification".to_string(), 23 | SolanaEventTypes::AccountNotification(_) =>"AccountNotification".to_string() 24 | } 25 | } 26 | 27 | fn deserialize_event(value: &Value) -> Result> { 28 | // println!("{}", format!("Attempting to deserialize SOLANA event:: {:?}", value)); 29 | 30 | let method = value["method"].as_str().ok_or_else(|| "Missing method in event")?; 31 | let result = match method { 32 | "logsNotification" => { 33 | let log_notification = deserialize_into::(value)?; 34 | // println!("Deserialized log for TX with signature: {}", log_notification.params.result.value.signature); 35 | Ok(SolanaEventTypes::LogNotification(log_notification)) 36 | }, 37 | 38 | "accountNotification" => { 39 | let account_notification = deserialize_into::(value)?; 40 | println!("Signature: {}", account_notification); 41 | Ok(SolanaEventTypes::AccountNotification(account_notification)) 42 | }, 43 | 44 | "programNotification" => { 45 | deserialize_into::(&value) 46 | .map(SolanaEventTypes::ProgramNotification) 47 | }, 48 | _ => Err(format!("Unsupported event type: {}", method).into()), 49 | }; 50 | 51 | // match &result { 52 | // Ok(deserialized) => println!("[[DESERIALIZER]] DESERIALIZED SOLANA EVENT: {:?}", deserialized), 53 | // Err(e) => println!("Error deserializing Solana event: {:?}", e), 54 | // } 55 | 56 | result 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/subscriber/websocket_subscriber.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use futures_util::SinkExt; 4 | use futures_util::StreamExt; 5 | use serde_json::json; 6 | use tokio::net::TcpStream; 7 | use tokio_tungstenite::{connect_async, MaybeTlsStream, tungstenite::protocol::Message, WebSocketStream}; 8 | use url::Url; 9 | 10 | pub trait SubscriptionBuilder { 11 | fn build_subscription_messages(params: &[(&str, Vec)]) -> Vec; 12 | } 13 | 14 | pub enum AuthMethod { 15 | None, 16 | QueryParam, 17 | Message, 18 | } 19 | 20 | pub struct WebSocketSubscriber { 21 | ws_url: String, 22 | api_key: Option, 23 | auth_method: AuthMethod, 24 | builder: B, 25 | } 26 | 27 | impl WebSocketSubscriber { 28 | pub fn new(ws_url: String, api_key: Option, auth_method: AuthMethod, builder: B) -> Self { 29 | Self { ws_url, api_key, auth_method, builder } 30 | } 31 | 32 | pub async fn connect(&self) -> Result>, Box> { 33 | 34 | // println!("CONNECTING {:?}", self.ws_url); 35 | let final_url = match self.auth_method { 36 | AuthMethod::QueryParam => { 37 | if let Some(ref key) = self.api_key { 38 | format!("{}{}", self.ws_url, key) 39 | } else { 40 | self.ws_url.clone() 41 | } 42 | } 43 | _ => self.ws_url.clone(), 44 | }; 45 | 46 | let url = Url::parse(&final_url)?; 47 | let (mut ws_stream, _) = connect_async(url).await?; 48 | println!("Connected to WebSocket :: {}", final_url); 49 | 50 | if let AuthMethod::Message = self.auth_method { 51 | self.authenticate(&mut ws_stream).await?; 52 | } 53 | 54 | Ok(ws_stream) 55 | } 56 | 57 | async fn authenticate(&self, ws_stream: &mut WebSocketStream>) -> Result<(), Box> { 58 | if let Some(ref api_key) = self.api_key { 59 | let auth_message = Message::Text(json!({ 60 | "action": "auth", 61 | "params": api_key 62 | }).to_string()); 63 | 64 | ws_stream.send(auth_message).await?; 65 | println!("Authentication message sent"); 66 | } 67 | 68 | Ok(()) 69 | } 70 | 71 | 72 | pub async fn subscribe(&self, ws_stream: &mut WebSocketStream>, params: &[(&str, Vec)]) -> Result<(), Box> { 73 | let messages = B::build_subscription_messages(params); 74 | for message in messages { 75 | println!("Subscribing to {} with provided messages :: {:?}", self.ws_url, message); 76 | ws_stream.send(message).await?; 77 | } 78 | 79 | Ok(()) 80 | } 81 | } 82 | 83 | pub struct SolanaSubscriptionBuilder; 84 | 85 | impl SubscriptionBuilder for SolanaSubscriptionBuilder { 86 | fn build_subscription_messages(params: &[(&str, Vec)]) -> Vec { 87 | params.iter().map(|&(method, ref args)| { 88 | let message = match method { 89 | "accountSubscribe" => { 90 | let pubkey = args.get(0).expect("Account pubkey is required"); 91 | json!({ 92 | "jsonrpc": "2.0", 93 | "id": 1, 94 | "method": method, 95 | "params": [ 96 | pubkey, 97 | { 98 | "encoding": "jsonParsed", 99 | "commitment": "finalized" 100 | } 101 | ] 102 | }) 103 | } 104 | "logsSubscribe" => { 105 | if args.is_empty() || args[0] == "all" { 106 | // Subscribe to all transactions except for simple vote transactions 107 | json!({ 108 | "jsonrpc": "2.0", 109 | "id": 1, 110 | "method": "logsSubscribe", 111 | "params": ["all"] 112 | }) 113 | } else if args[0] == "allWithVotes" { 114 | // Subscribe to all transactions, including simple vote transactions 115 | json!({ 116 | "jsonrpc": "2.0", 117 | "id": 1, 118 | "method": "logsSubscribe", 119 | "params": ["allWithVotes"] 120 | }) 121 | } else { 122 | println!("[[SUBSCRIBER]] SUBSCRIBING TO LOGS"); 123 | json!({ 124 | "jsonrpc": "2.0", 125 | "id": 1, 126 | "method": "logsSubscribe", 127 | "params": [ 128 | { 129 | "mentions": [args[0]] 130 | }, 131 | { 132 | "commitment": "finalized" 133 | } 134 | ] 135 | }) 136 | } 137 | } 138 | 139 | "programSubscribe" => { 140 | let program_id = args.get(0).expect("Program ID is required"); 141 | let encoding = "jsonParsed"; 142 | let commitment = "finalized"; 143 | 144 | let filters = args.iter().skip(3).map(|filter| { 145 | serde_json::from_str::(filter) 146 | .expect("Filter must be a valid JSON") 147 | }).collect::>(); 148 | 149 | json!({ 150 | "jsonrpc": "2.0", 151 | "id": 1, 152 | "method": method, 153 | "params": [ 154 | program_id, 155 | { 156 | "encoding": encoding, 157 | "commitment": "finalized", 158 | "filters": filters 159 | } 160 | ] 161 | }) 162 | } 163 | _ => panic!("Unsupported subscription method: {}", method), 164 | }; 165 | Message::Text(message.to_string()) 166 | }).collect() 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /src/trackers/binance/depth_tracker.rs: -------------------------------------------------------------------------------- 1 | // use std::hash::Hash; 2 | // use crate::models::binance::binance_event_types::BinanceEventTypes; 3 | // use crate::models::binance::diff_depth::DiffDepth; 4 | // use crate::models::binance::kline::Kline; 5 | // use crate::models::binance::partial_book_depth::PartialBookDepth; 6 | // use crate::models::binance::trade::Trade; 7 | // use std::collections::{BTreeMap, HashMap, VecDeque}; 8 | // use ordered_float::OrderedFloat; 9 | // 10 | // 11 | // /** 12 | // 13 | // Responsibility: Monitor and analyze the order book depth data (bids and asks) for various symbols. 14 | // Functionality: Maintain a current state of the order book for each symbol, highlighting potential support and resistance levels based on the volume of bids and asks. 15 | // 16 | // 17 | // Analysis: 18 | // 19 | // Market Depth: Market depth refers to the volume of orders waiting to be executed at different price levels for a particular asset. It's visualized in what's often called an "order book". Depth data shows the demand (bids) and supply (asks) at different price points and the volume available at each level. 20 | // * Bids: Orders from buyers to purchase the asset at a certain price. They are listed in descending order with the highest bid at the top. 21 | // * Asks: Orders from sellers to sell the asset at a certain price. They are listed in ascending order with the lowest ask at the top. 22 | // 23 | // Depth data is crucial because it provides insight into potential resistance (ASKS) and support (BIDS) levels. 24 | // * High volume at a bid level suggests strong buying interest that could act as support 25 | // * High volume at an ask level indicates selling interest that could act as resistance. 26 | // 27 | // */ 28 | // #[derive()] 29 | // struct OrderLevel { 30 | // price: OrderedFloat, 31 | // quantity: f64, 32 | // } 33 | // 34 | // struct OrderBookState { 35 | // bids: BTreeMap, OrderLevel>, // Sorted by price in descending order 36 | // asks: BTreeMap, OrderLevel>, // Sorted by price in ascending order 37 | // total_volume_history: VecDeque<(f64, f64)>, // (total bid volume, total ask volume) 38 | // } 39 | // 40 | // impl OrderBookState { 41 | // // OB State maintains the last 100 updates 42 | // pub const HISTORY_CAPACITY: usize = 100; 43 | // 44 | // fn new() -> Self { 45 | // Self { 46 | // bids: BTreeMap::new(), 47 | // asks: BTreeMap::new(), 48 | // total_volume_history: VecDeque::with_capacity(Self::HISTORY_CAPACITY), 49 | // } 50 | // } 51 | // 52 | // /// Order Book State Thresholds 53 | // /// Threshold for what constitutes a "rapid" volume change 54 | // const VOLUME_CHANGE_THRESHOLD_PERCENT: f64 = 2.0; // 10% change 55 | // fn analyze_and_alert(&mut self) { 56 | // self.detect_rapid_volume_change(); 57 | // self.detect_disappearance_of_large_orders(); 58 | // self.detect_sharp_bid_ask_spread_change(); 59 | // self.detect_imbalance_between_bids_and_asks(); 60 | // } 61 | // 62 | // fn detect_rapid_volume_change(&mut self) { 63 | // // Calculate current total volume for bids and asks 64 | // let current_total_bid_volume: f64 = self.bids.values().map(|level| level.quantity).sum(); 65 | // let current_total_ask_volume: f64 = self.asks.values().map(|level| level.quantity).sum(); 66 | // 67 | // // Compare against the most recent historical volume to detect rapid change 68 | // if let Some(&(last_bid_volume, last_ask_volume)) = self.total_volume_history.back() { 69 | // let bid_volume_change_percent = ((¤t_total_bid_volume - last_bid_volume) / &last_bid_volume) * 100.0; 70 | // let ask_volume_change_percent = ((¤t_total_ask_volume - last_ask_volume) / &last_ask_volume) * 100.0; 71 | // 72 | // // Check if the change exceeds our threshold 73 | // if bid_volume_change_percent.abs() > Self::VOLUME_CHANGE_THRESHOLD_PERCENT || ask_volume_change_percent.abs() > Self::VOLUME_CHANGE_THRESHOLD_PERCENT { 74 | // println!("Alert: Significant volume change detected. Bid change: {:.2}%, Ask change: {:.2}%", bid_volume_change_percent, ask_volume_change_percent); 75 | // // Implement your alert mechanism here (e.g., logging, sending notifications) 76 | // } 77 | // } 78 | // 79 | // if self.total_volume_history.len() == 100 { //history capacity 80 | // self.total_volume_history.pop_front(); // Remove the oldest record to make room 81 | // } 82 | // self.total_volume_history.push_back((current_total_bid_volume, current_total_ask_volume)); 83 | // } 84 | // 85 | // fn detect_disappearance_of_large_orders(&self) { 86 | // // Implement logic to compare current large orders against previous state 87 | // // Alert if large orders disappear without corresponding trades 88 | // } 89 | // 90 | // fn detect_sharp_bid_ask_spread_change(&self) { 91 | // // Implement detection logic based on `bid_ask_spread_history` 92 | // // Alert if the spread changes significantly in a short period 93 | // } 94 | // 95 | // fn detect_imbalance_between_bids_and_asks(&self) { 96 | // // Calculate total bid and ask volumes and compare for significant imbalance 97 | // // Alert if an imbalance is detected 98 | // } 99 | // 100 | // // Additional methods for updating historical data... 101 | // } 102 | // 103 | // pub struct DepthTracker { 104 | // pub order_books: HashMap 105 | // } 106 | // 107 | // impl DepthTracker { 108 | // pub fn new() -> Self { 109 | // Self { 110 | // order_books: HashMap::new(), 111 | // } 112 | // } 113 | // 114 | // pub(crate) fn apply(&mut self, event: &BinanceEventTypes) { 115 | // println!("APPLYING EVENT {:?}", event); 116 | // match event { 117 | // // BinanceEventTypes::Trade(data) => self.process_binance_trade(data), 118 | // BinanceEventTypes::PartialBookDepth(data) => self.update_partial_depth_data(data), 119 | // BinanceEventTypes::DiffDepth(data) => self.update_diff_depth_data(data), 120 | // _ => {} 121 | // } 122 | // } 123 | // 124 | // /// The PartialBookDepth events provide snapshots of the top levels of the order book (both bids and asks) for a symbol. When such an event is received, the tracker should update the order book state for that symbol with the new top levels of bids and asks provided. 125 | // pub(crate) fn update_partial_depth_data(&mut self, depth: &PartialBookDepth) { 126 | // let order_book_state = self.order_books.entry("depth.symbol".to_string()).or_insert_with(OrderBookState::new); 127 | // 128 | // // Clear existing bids and asks to replace with the new snapshot 129 | // order_book_state.bids.clear(); 130 | // order_book_state.asks.clear(); 131 | // 132 | // // Update bids 133 | // for (price, quantity) in &depth.bids { 134 | // let price = price.parse::().expect("Invalid bid price"); 135 | // let quantity = quantity.parse::().expect("Invalid bid quantity"); 136 | // let ordered_float = ordered_float::OrderedFloat(price.clone()); 137 | // order_book_state.bids.insert(ordered_float::OrderedFloat(price), OrderLevel { price: ordered_float, quantity }); 138 | // } 139 | // 140 | // // Update asks 141 | // for (price, quantity) in &depth.asks { 142 | // let price = price.parse::().expect("Invalid ask price"); 143 | // let quantity = quantity.parse::().expect("Invalid ask quantity"); 144 | // let ordered_float = ordered_float::OrderedFloat(price.clone()); 145 | // order_book_state.asks.insert(OrderedFloat(price), OrderLevel { price: ordered_float, quantity }); 146 | // } 147 | // 148 | // // After updating the order book, analyze for volume changes 149 | // if let Some(order_book_state) = self.order_books.get_mut("depth.symbol") { 150 | // order_book_state.analyze_and_alert(); // This will call detect_rapid_volume_change among others 151 | // } 152 | // 153 | // } 154 | // 155 | // /// The DiffDepth events provide updates on changes to the order book for a symbol, including additions, updates, and deletions of orders at different price levels. When processing these events, the tracker should apply these differential changes to the existing state of the order book for the relevant symbol. 156 | // pub(crate) fn update_diff_depth_data(&mut self, depth: &DiffDepth) { 157 | // let order_book_state = self.order_books.entry(depth.symbol.clone()).or_insert_with(OrderBookState::new); 158 | // 159 | // // Update bids 160 | // for (price, quantity) in &depth.bids { 161 | // let price = price.parse::().expect("Invalid bid price"); 162 | // let quantity = quantity.parse::().expect("Invalid bid quantity"); 163 | // 164 | // if quantity == 0.0 { 165 | // // If quantity is 0, remove the level 166 | // order_book_state.bids.remove(&OrderedFloat(price)); 167 | // } else { 168 | // // Update or add the bid level 169 | // let ordered_float = ordered_float::OrderedFloat(price.clone()); 170 | // order_book_state.bids.insert(OrderedFloat(price), OrderLevel { price: ordered_float, quantity }); 171 | // } 172 | // } 173 | // 174 | // // Update asks 175 | // for (price, quantity) in &depth.asks { 176 | // let price = price.parse::().expect("Invalid ask price"); 177 | // let quantity = quantity.parse::().expect("Invalid ask quantity"); 178 | // 179 | // if quantity == 0.0 { 180 | // // If quantity is 0, remove the level 181 | // order_book_state.asks.remove(&OrderedFloat(price)); 182 | // } else { 183 | // // Update or add the ask level 184 | // let ordered_float = ordered_float::OrderedFloat(price.clone()); 185 | // order_book_state.asks.insert(ordered_float::OrderedFloat(price), OrderLevel { price: ordered_float, quantity }); 186 | // } 187 | // } 188 | // 189 | // // After updating the order book, analyze for volume changes 190 | // if let Some(order_book_state) = self.order_books.get_mut(&depth.symbol) { 191 | // order_book_state.analyze_and_alert(); // This includes detect_rapid_volume_change 192 | // } 193 | // } 194 | // } 195 | // 196 | -------------------------------------------------------------------------------- /src/trackers/binance/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod rsi_tracker; 2 | pub mod depth_tracker; -------------------------------------------------------------------------------- /src/trackers/binance/rsi_tracker.rs: -------------------------------------------------------------------------------- 1 | // use std::collections::HashMap; 2 | // use lazy_static::lazy_static; 3 | // use crate::models::binance::binance_event_types::BinanceEventTypes; 4 | // use crate::models::binance::diff_depth::DiffDepth; 5 | // use crate::models::binance::kline::Kline; 6 | // use crate::models::binance::partial_book_depth::PartialBookDepth; 7 | // use crate::models::binance::trade::Trade; 8 | // 9 | // /** 10 | // The purpose of this struct is to calculate RSI values for a given symbol over different intervals and aggregate depth data for insight. 11 | // 12 | // Market Depth: Market depth refers to the volume of orders waiting to be executed at different price levels for a particular asset. It's visualized in what's often called an "order book". Depth data shows the demand (bids) and supply (asks) at different price points and the volume available at each level. 13 | // Bids: Orders from buyers to purchase the asset at a certain price. They are listed in descending order with the highest bid at the top. 14 | // Asks: Orders from sellers to sell the asset at a certain price. They are listed in ascending order with the lowest ask at the top. 15 | // 16 | // Depth data is crucial because it provides insight into potential resistance (in the case of asks) and support (in the case of bids) levels. 17 | // - High volume at a bid level suggests strong buying interest that could act as support 18 | // - High volume at an ask level indicates selling interest that could act as resistance. 19 | // 20 | // Leveraging Depth Data and RSI for Smart Alerts 21 | // 22 | // * Identifying Overbought/Oversold Conditions: Use RSI to identify potential overbought (>70) or oversold (<30) conditions. These conditions suggest that an asset might be due for a reversal. 23 | // * Analyze Depth Data for Confirmation: Once an overbought or oversold condition is detected, examine the depth data for confirmation. 24 | // For example, in an oversold condition (RSI < 30), look for a significant volume of bids just below the current price, indicating potential support. 25 | // Conversely, in an overbought condition (RSI > 70), look for a substantial volume of asks just above the current price, indicating potential resistance. 26 | // 27 | // * Consider Volume Imbalance: A significant imbalance between bids and asks can indicate the direction of potential price movement. A high volume of bids compared to asks might indicate upward pressure on price, while the opposite suggests downward pressure. 28 | // 29 | // * Generate alerts when both RSI indicators and depth data align. For example: 30 | // 31 | // ! Buy Alert: If RSI is below 30 (oversold) and there's significant bid volume just below the current price, indicating strong support. 32 | // ! Sell Alert: If RSI is above 70 (overbought) and there's significant ask volume just above the current price, indicating strong resistance. 33 | // 34 | // */ 35 | // 36 | // 37 | // lazy_static! { 38 | // static ref INTERVAL_PERIODS: HashMap<&'static str, usize> = { 39 | // let mut m = HashMap::new(); 40 | // m.insert("1d", 14); 41 | // m.insert("15m", 14); 42 | // m.insert("5min", 14); 43 | // m.insert("1s", 14); 44 | // m 45 | // }; 46 | // } 47 | // 48 | // 49 | // // TODO for this to work properly, we need a process that collects relevant historical data - we need a starting point RSI for each of the symbol intervals being tracked. 50 | // pub struct RsiTracker { 51 | // interval_rsis: HashMap, 52 | // interval_price_changes: HashMap>, 53 | // pub(crate) symbol_intervals: HashMap>, 54 | // } 55 | // 56 | // impl RsiTracker { 57 | // pub fn new() -> Self { 58 | // Self { 59 | // interval_rsis: HashMap::new(), 60 | // interval_price_changes: HashMap::new(), 61 | // symbol_intervals: HashMap::new(), 62 | // } 63 | // } 64 | // 65 | // /// Retrieves the RSI value for the given symbol and interval. 66 | // pub fn get_rsi(&self, symbol: &str, interval: &str) -> Option { 67 | // let key = format!("{}_{}", symbol, interval); 68 | // // Use the get method of the HashMap to retrieve the RSI value for the given symbol and interval. 69 | // // The get method returns an Option, which will be None if the key is not found. 70 | // self.interval_rsis.get(&key).copied() 71 | // } 72 | // /// Sets the initial RSI value for the given symbol and interval. 73 | // pub fn set_initial_rsi(&mut self, symbol: &str, interval: &str, initial_rsi: f64) { 74 | // let key = format!("{}_{}", symbol, interval); 75 | // self.interval_rsis.insert(key, initial_rsi); 76 | // } 77 | // 78 | // /// Registers the intervals for which RSI will be tracked for a given symbol (1min, 15min, 1 day, etc...) 79 | // pub fn set_intervals_for_symbol(&mut self, symbol: &str, intervals: Vec<&str>) { 80 | // let intervals = intervals.into_iter().map(String::from).collect(); 81 | // self.symbol_intervals.insert(symbol.to_string(), intervals); 82 | // println!("[[RSI TRACKER]] Intervals set. Current intervals: {:?}", self.symbol_intervals); 83 | // } 84 | // 85 | // // Applies a kline to the tracker, updating price changes and calculating RSI 86 | // pub fn apply_kline(&mut self, kline: &Kline) { 87 | // let key = format!("{}_{}", kline.symbol, kline.kline.interval); 88 | // 89 | // // Check if the symbol's interval is being tracked 90 | // if let Some(intervals) = self.symbol_intervals.get(&kline.symbol) { 91 | // if intervals.contains(&kline.kline.interval) { 92 | // println!( 93 | // "APPLYING KLINE Symbol: {}, Close Price: {}, Volume: {}, Number of Trades: {}", 94 | // &key, kline.kline.close_price, kline.kline.volume, kline.kline.number_of_trades 95 | // ); 96 | // 97 | // let close_price: f64 = match kline.kline.close_price.parse() { 98 | // Ok(price) => price, 99 | // Err(err) => { 100 | // eprintln!("Error parsing close price: {:?}", err); 101 | // return; // Return early if parsing fails 102 | // } 103 | // }; 104 | // // Pass the period as a parameter 105 | // let period = INTERVAL_PERIODS.get(&*kline.kline.interval).unwrap().clone(); 106 | // self.update_price_change(&kline.symbol, &kline.kline.interval, close_price, period); 107 | // self.calculate_rsi(&key); 108 | // } else { 109 | // println!("Interval '{}' not found for symbol '{}'", kline.kline.interval, kline.symbol); 110 | // } 111 | // } else { 112 | // println!("Symbol '{}' not found in tracked symbols", kline.symbol); 113 | // } 114 | // } 115 | // 116 | // 117 | // /// Updates the price change for the specified symbol and interval. 118 | // fn update_price_change(&mut self, symbol: &str, interval: &str, new_price: f64, period: usize) { 119 | // let key = format!("{}_{}", symbol, interval); 120 | // 121 | // let price_changes = self.interval_price_changes.entry(key.clone()).or_insert_with(Vec::new); 122 | // 123 | // if let Some(&last_price) = price_changes.last() { 124 | // let price_change = new_price - last_price; 125 | // price_changes.push(price_change); 126 | // } 127 | // else { 128 | // // Calculate the initial price change (assuming a zero initial price) 129 | // let initial_price = 51751.15; // Adjust this based on your initial price assumption 130 | // let price_change = new_price - initial_price; 131 | // price_changes.push(price_change); 132 | // } 133 | // 134 | // if price_changes.len() > period + 1 { 135 | // price_changes.remove(0); 136 | // } 137 | // } 138 | // 139 | // /// Calculates the Relative Strength Index (RSI) for the specified symbol and interval. 140 | // fn calculate_rsi(&mut self, key: &str) { 141 | // // Retrieve price changes for the specified symbol and interval 142 | // let pc = self.interval_price_changes.get(key); 143 | // println!("CALCULATING RSI - Current price changes for {}, {:?}", key, pc); 144 | // if let Some(price_changes) = self.interval_price_changes.get(key) { 145 | // // Check if there are enough price changes to calculate RSI 146 | // if price_changes.len() <= 1 { 147 | // println!("CALCULATING RSI - Not enough data points for {}, {:?}", key, pc); 148 | // return; // Not enough data points, return early 149 | // } 150 | // 151 | // // Accumulate sum of gains, sum of losses, count of gains, and count of losses 152 | // let (sum_gains, sum_losses, count_gains, count_losses) = price_changes 153 | // // Iterate over pairs of adjacent elements in the price_changes slice using the `windows` method 154 | // .windows(2).fold( 155 | // (0.0, 0.0, 0, 0), // Initial tuple with sum_gains, sum_losses, count_gains, and count_losses 156 | // |(sum_gains, sum_losses, count_gains, count_losses), window| { 157 | // // Calculate price change between adjacent elements 158 | // let change = window[1].clone() - window[0].clone(); // Calculate price change 159 | // // Update accumulated values based on price change 160 | // if change > 0.0 { 161 | // (sum_gains + change, sum_losses, count_gains + 1, count_losses) 162 | // } else { 163 | // (sum_gains, sum_losses - change, count_gains, count_losses + 1) 164 | // } 165 | // }, 166 | // ); 167 | // 168 | // // Calculate average gain and average loss 169 | // let average_gain = if count_gains > 0 { sum_gains / count_gains as f64 } else { 0.0 }; 170 | // let average_loss = if count_losses > 0 { sum_losses / count_losses as f64 } else { 0.0 }; 171 | // 172 | // // Calculate Relative Strength (RS) 173 | // let rs = if average_loss == 0.0 { 174 | // if average_gain == 0.0 { 175 | // 1.0 // Avoid division by zero, set to 1.0 176 | // } else { 177 | // f64::INFINITY // Considered overbought, set to infinity 178 | // } 179 | // } else { 180 | // average_gain / average_loss // Normal RS calculation 181 | // }; 182 | // 183 | // // Calculate RSI using RS 184 | // let rsi = 100.0 - (100.0 / (1.0 + rs)); 185 | // 186 | // // Insert calculated RSI into the interval RSI map 187 | // println!("[[RSI TRACKER]] CALCULATED RSI FOR'{}' ::: {}", key, &rsi); 188 | // self.interval_rsis.insert(key.to_string(), rsi); 189 | // } 190 | // } 191 | // } 192 | -------------------------------------------------------------------------------- /src/trackers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod binance; 2 | pub mod raydium; 3 | -------------------------------------------------------------------------------- /src/trackers/raydium/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod new_token_tracker; -------------------------------------------------------------------------------- /src/trackers/raydium/new_token_tracker.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | use crate::models::solana::solana_event_types::SolanaEventTypes; 3 | use std::collections::{BTreeMap, HashMap, VecDeque}; 4 | use crate::models::solana::solana_logs_notification::SolanaLogsNotification; 5 | 6 | 7 | /*struct TrackerState { 8 | new_tokens: HashMap, // token tx signature / data 9 | } 10 | 11 | impl TrackerState { 12 | // OB State maintains the last 1000 new tokens 13 | pub const HISTORY_CAPACITY: usize = 100; 14 | 15 | fn new() -> Self { 16 | Self { 17 | new_tokens: HashMap::new(), 18 | } 19 | } 20 | 21 | /// Order Book State Thresholds 22 | /// Threshold for what constitutes a "rapid" volume change 23 | const VOLUME_CHANGE_THRESHOLD_PERCENT: f64 = 2.0; // 10% change 24 | fn analyze_and_alert(&mut self) { 25 | self.detect_rapid_volume_change(); 26 | self.detect_disappearance_of_large_orders(); 27 | self.detect_sharp_bid_ask_spread_change(); 28 | self.detect_imbalance_between_bids_and_asks(); 29 | } 30 | 31 | fn detect_rapid_volume_change(&mut self) { 32 | // Calculate current total volume for bids and asks 33 | let current_total_bid_volume: f64 = self.bids.values().map(|level| level.quantity).sum(); 34 | let current_total_ask_volume: f64 = self.asks.values().map(|level| level.quantity).sum(); 35 | 36 | // Compare against the most recent historical volume to detect rapid change 37 | if let Some(&(last_bid_volume, last_ask_volume)) = self.total_volume_history.back() { 38 | let bid_volume_change_percent = ((¤t_total_bid_volume - last_bid_volume) / &last_bid_volume) * 100.0; 39 | let ask_volume_change_percent = ((¤t_total_ask_volume - last_ask_volume) / &last_ask_volume) * 100.0; 40 | 41 | // Check if the change exceeds our threshold 42 | if bid_volume_change_percent.abs() > Self::VOLUME_CHANGE_THRESHOLD_PERCENT || ask_volume_change_percent.abs() > Self::VOLUME_CHANGE_THRESHOLD_PERCENT { 43 | println!("Alert: Significant volume change detected. Bid change: {:.2}%, Ask change: {:.2}%", bid_volume_change_percent, ask_volume_change_percent); 44 | // Implement your alert mechanism here (e.g., logging, sending notifications) 45 | } 46 | } 47 | 48 | if self.total_volume_history.len() == 100 { //history capacity 49 | self.total_volume_history.pop_front(); // Remove the oldest record to make room 50 | } 51 | self.total_volume_history.push_back((current_total_bid_volume, current_total_ask_volume)); 52 | } 53 | 54 | fn detect_disappearance_of_large_orders(&self) { 55 | // Implement logic to compare current large orders against previous state 56 | // Alert if large orders disappear without corresponding trades 57 | } 58 | 59 | fn detect_sharp_bid_ask_spread_change(&self) { 60 | // Implement detection logic based on `bid_ask_spread_history` 61 | // Alert if the spread changes significantly in a short period 62 | } 63 | 64 | fn detect_imbalance_between_bids_and_asks(&self) { 65 | // Calculate total bid and ask volumes and compare for significant imbalance 66 | // Alert if an imbalance is detected 67 | } 68 | 69 | // Additional methods for updating historical data... 70 | } 71 | */ 72 | 73 | pub struct NewTokenTracker { 74 | pub new_tokens: HashMap, // token tx signature / data 75 | } 76 | 77 | impl NewTokenTracker { 78 | pub fn new() -> Self { 79 | Self { 80 | new_tokens: HashMap::new(), 81 | } 82 | } 83 | 84 | pub(crate) fn apply(&mut self, event: &SolanaEventTypes) { 85 | println!("APPLYING NEW TOKEN TRACKER EVENT {:?}", event); 86 | match event { 87 | SolanaEventTypes::LogNotification(log) => self.handle_new_token_signature(log), 88 | _ => {} 89 | } 90 | } 91 | 92 | /// The PartialBookDepth events provide snapshots of the top levels of the order book (both bids and asks) for a symbol. When such an event is received, the tracker should update the order book state for that symbol with the new top levels of bids and asks provided. 93 | pub(crate) fn handle_new_token_signature(&mut self, log: &SolanaLogsNotification) { 94 | println!("[[NEW TOKEN TRACKER]] Processing signature: {:?}", log.params.result.value.signature) 95 | 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/util/event_filters.rs: -------------------------------------------------------------------------------- 1 | // use std::collections::HashMap; 2 | // use std::sync::Arc; 3 | // 4 | // // use crate::models::polygon::polygon_crypto_aggregate_data::PolygonCryptoAggregateData; 5 | // use serde::{Deserialize, Serialize}; 6 | // 7 | // 8 | // /// This class structure illustrates a highly modular and scalable approach to filtering real-time financial data streams. 9 | // 10 | // /// Enums with associated data (like Number and Text) allow for the representation of filter criteria values that can be of different types (e.g., numeric or textual), showcasing Rust's algebraic data types. 11 | // /// This enables the creation of flexible filters that can apply to various data fields. 12 | // #[derive(Serialize, Deserialize, Debug, Clone)] 13 | // pub enum FilterValue { 14 | // Number(f64), 15 | // Text(String), 16 | // } 17 | // 18 | // #[derive(Serialize, Deserialize, Debug, Clone)] 19 | // pub struct FilterCriteria { 20 | // pub field: String, 21 | // pub operation: String, 22 | // pub value: FilterValue, 23 | // } 24 | // 25 | // /** 26 | // The ParameterizedFilter structure in Rust is a powerful and flexible mechanism for applying custom filters to data streams, 27 | // particularly useful in the context of processing real-time financial data streams 28 | // 29 | // Custom Criteria Mapping: 30 | // The ParameterizedFilter struct uses a HashMap to associate cryptocurrency pairs with a vector of FilterCriteria. 31 | // This mapping allows the application of multiple, distinct filtering criteria to different cryptocurrency pairs. 32 | // It enables users to specify conditions under which a trade or aggregate data point should be considered relevant or ignored. 33 | // 34 | // Dynamic Criterion Evaluation: 35 | // The meets_criterion method dynamically evaluates whether a given trade meets the specified criteria. 36 | // This evaluation is based on the trade's attributes, such as price and size, and the criterion's operation (e.g., greater than, less than, equal to). This dynamic evaluation supports various operations and makes the filter highly adaptable to different filtering needs. 37 | // 38 | // Generic Filter Application: 39 | // Through the implementation of the FilterFunction trait, ParameterizedFilter provides a generic apply method. 40 | // This method determines applicability of the filter to any event within the data stream, enabling seamless integration into the data processing pipeline. It checks if the event matches the filter's criteria and applies the filter accordingly. 41 | // 42 | // */ 43 | // pub struct ParameterizedFilter { 44 | // criteria_by_pair: HashMap>, 45 | // } 46 | // 47 | // impl ParameterizedFilter { 48 | // pub fn new(criteria_by_pair: HashMap>) -> Self { 49 | // ParameterizedFilter { criteria_by_pair } 50 | // } 51 | // // fn meets_criterion(&self, trade: &PolygonCryptoTradeData, criterion: &FilterCriteria) -> bool { 52 | // // match &criterion.value { 53 | // // FilterValue::Number(num_val) => match criterion.field.as_str() { 54 | // // "price" => { 55 | // // self.compare_numeric(trade.clone().price, &criterion.operation, num_val.clone()) 56 | // // } 57 | // // "size" => { 58 | // // self.compare_numeric(trade.clone().size, &criterion.operation, num_val.clone()) 59 | // // } 60 | // // _ => false, 61 | // // }, 62 | // // FilterValue::Text(text_val) => match criterion.field.as_str() { 63 | // // "pair" => self.compare_text(&trade.pair, &criterion.operation, text_val), 64 | // // _ => false, 65 | // // }, 66 | // // } 67 | // // } 68 | // 69 | // fn compare_numeric(&self, field_value: f64, operation: &str, criterion_value: f64) -> bool { 70 | // match operation { 71 | // ">" => field_value > criterion_value, 72 | // "<" => field_value < criterion_value, 73 | // "=" => (field_value - criterion_value).abs() < f64::EPSILON, 74 | // _ => false, 75 | // } 76 | // } 77 | // 78 | // fn compare_text(&self, field_value: &str, operation: &str, criterion_value: &str) -> bool { 79 | // match operation { 80 | // "=" => field_value == criterion_value, 81 | // "!=" => field_value != criterion_value, 82 | // _ => false, 83 | // } 84 | // } 85 | // } 86 | // 87 | // /// Any struct implementing this trait must provide an apply method, enabling a consistent way to apply filters to data events. 88 | // pub trait FilterFunction { 89 | // fn apply(&self, event: &PolygonEventTypes) -> bool; 90 | // } 91 | // // 92 | // // impl FilterFunction for ParameterizedFilter { 93 | // // fn apply(&self, event: &PolygonEventTypes) -> bool { 94 | // // match event { 95 | // // PolygonEventTypes::XtTrade(trade) => { 96 | // // if let Some(criteria) = self.criteria_by_pair.get(&trade.pair) { 97 | // // criteria 98 | // // .iter() 99 | // // .all(|criterion| self.meets_criterion(trade, criterion)) 100 | // // } else { 101 | // // true // If no criteria for the pair, pass the trade through 102 | // // } 103 | // // } 104 | // // _ => false, 105 | // // } 106 | // // } 107 | // // } 108 | // 109 | // pub struct PriceMovementFilter { 110 | // threshold_percentage: f64, 111 | // } 112 | // 113 | // // impl PriceMovementFilter { 114 | // // pub fn new(threshold_percentage: f64) -> Self { 115 | // // PriceMovementFilter { 116 | // // threshold_percentage, 117 | // // } 118 | // // } 119 | // // 120 | // // fn apply_to_aggregate(&self, aggregate: &PolygonCryptoAggregateData) -> bool { 121 | // // let price_movement = (aggregate.close - aggregate.open).abs(); 122 | // // let percentage_movement = (price_movement / aggregate.open) * 100.0; 123 | // // percentage_movement > self.threshold_percentage 124 | // // } 125 | // // } 126 | // 127 | // impl FilterFunction for PriceMovementFilter { 128 | // fn apply(&self, event: &PolygonEventTypes) -> bool { 129 | // match event { 130 | // PolygonEventTypes::XaAggregateMinute(aggregate) => self.apply_to_aggregate(aggregate), 131 | // _ => false, // This filter does not apply to other event types 132 | // } 133 | // } 134 | // } 135 | // 136 | // /** 137 | // The EventFilters sruct serves as a container for a collection of filters that are applied to incoming data streams. 138 | // Each filter is encapsulated within an Arc, allowing for shared ownership across threads and dynamic dispatch of the apply method defined by the FilterFunction trait. 139 | // */ 140 | // 141 | // pub struct EventFilters { 142 | // pub(crate) filters: Vec>, 143 | // } 144 | // 145 | // impl EventFilters { 146 | // ///Creates a new instance of EventFilters with an empty vector of filters. Inits the filter system before any data processing begins. 147 | // pub fn new() -> Self { 148 | // Self { 149 | // filters: Vec::new(), 150 | // } 151 | // } 152 | // 153 | // ///Allows adding new filters to the EventFilters instance. By taking an Arc, it supports adding filters that implement the FilterFunction trait, enabling polymorphism. This design choice allows for different types of filters (e.g., price movement, trade size) to be applied without changing the underlying system architecture. 154 | // pub fn add_filter(&mut self, filter: Arc) { 155 | // self.filters.push(filter); 156 | // } 157 | // } 158 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod event_filters; 2 | pub mod serde_helper; 3 | -------------------------------------------------------------------------------- /src/util/serde_helper.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde_json::Value; 3 | use std::error::Error; 4 | 5 | pub fn deserialize_into(value: &Value) -> Result> { 6 | 7 | serde_json::from_value(value.clone()).map_err(|e| e.into()) 8 | } --------------------------------------------------------------------------------